mirror of
https://github.com/docmost/docmost.git
synced 2026-08-29 18:14:58 +08:00
refactor(base): rename baseId to pageId across WS, processors, tasks, formula, events
Renames baseId → pageId in: - Domain event types (BaseEventBase) and all derived event types - WS Zod schemas (wire-protocol field names now emit pageId) - BaseWsService, BaseWsConsumers, BasePresenceService - FormulaLockService, FormulaService - BullMQ job interfaces (IBaseTypeConversionJob, IBaseCellGcJob, IBaseFormulaRecomputeJob) - BaseQueueProcessor and all task functions - Service emit-payload keys in base.service, base-property.service, base-row.service, base-view.service, base-csv-export.service - Formula spec test fixtures
This commit is contained in:
@@ -8,7 +8,7 @@ import { BaseProperty, BaseRow, BaseView } from '@docmost/db/types/entity.types'
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
type BaseEventBase = {
|
type BaseEventBase = {
|
||||||
baseId: string;
|
pageId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
actorId?: string | null;
|
actorId?: string | null;
|
||||||
requestId?: string | null;
|
requestId?: string | null;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ const mkProp = (
|
|||||||
id: string, type: string, typeOptions: any = {},
|
id: string, type: string, typeOptions: any = {},
|
||||||
name = id,
|
name = id,
|
||||||
): BaseProperty => ({
|
): BaseProperty => ({
|
||||||
id, baseId: "base_1", name, type: type as any, position: "a",
|
id, pageId: "base_1", name, type: type as any, position: "a",
|
||||||
typeOptions, isPrimary: false, workspaceId: "ws_1",
|
typeOptions, isPrimary: false, workspaceId: "ws_1",
|
||||||
createdAt: new Date(), updatedAt: new Date(),
|
createdAt: new Date(), updatedAt: new Date(),
|
||||||
schemaVersion: 0, pendingType: null, pendingTypeOptions: null,
|
schemaVersion: 0, pendingType: null, pendingTypeOptions: null,
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ export class FormulaLockService {
|
|||||||
* Returns a release token on success, or null if the lock is held. Callers
|
* Returns a release token on success, or null if the lock is held. Callers
|
||||||
* must pass the token back to release() to prevent cross-holder releases.
|
* must pass the token back to release() to prevent cross-holder releases.
|
||||||
*/
|
*/
|
||||||
async acquire(baseId: string): Promise<string | null> {
|
async acquire(pageId: string): Promise<string | null> {
|
||||||
const token = `${Date.now()}-${Math.random()}`;
|
const token = `${Date.now()}-${Math.random()}`;
|
||||||
const ok = await this.redis.set(
|
const ok = await this.redis.set(
|
||||||
LOCK_PREFIX + baseId,
|
LOCK_PREFIX + pageId,
|
||||||
token,
|
token,
|
||||||
"PX",
|
"PX",
|
||||||
LOCK_TTL_MS,
|
LOCK_TTL_MS,
|
||||||
@@ -30,7 +30,7 @@ export class FormulaLockService {
|
|||||||
return ok === "OK" ? token : null;
|
return ok === "OK" ? token : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async release(baseId: string, token: string): Promise<void> {
|
async release(pageId: string, token: string): Promise<void> {
|
||||||
const lua = `
|
const lua = `
|
||||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||||
return redis.call("DEL", KEYS[1])
|
return redis.call("DEL", KEYS[1])
|
||||||
@@ -38,7 +38,7 @@ export class FormulaLockService {
|
|||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
`;
|
`;
|
||||||
await this.redis.eval(lua, 1, LOCK_PREFIX + baseId, token);
|
await this.redis.eval(lua, 1, LOCK_PREFIX + pageId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -46,11 +46,11 @@ export class FormulaLockService {
|
|||||||
* on timeout. Workers call this at job start — if acquisition times out
|
* on timeout. Workers call this at job start — if acquisition times out
|
||||||
* the job is retried by BullMQ.
|
* the job is retried by BullMQ.
|
||||||
*/
|
*/
|
||||||
async acquireWait(baseId: string, opts: { timeoutMs: number; pollMs?: number }): Promise<string | null> {
|
async acquireWait(pageId: string, opts: { timeoutMs: number; pollMs?: number }): Promise<string | null> {
|
||||||
const deadline = Date.now() + opts.timeoutMs;
|
const deadline = Date.now() + opts.timeoutMs;
|
||||||
const poll = opts.pollMs ?? 500;
|
const poll = opts.pollMs ?? 500;
|
||||||
while (Date.now() < deadline) {
|
while (Date.now() < deadline) {
|
||||||
const t = await this.acquire(baseId);
|
const t = await this.acquire(pageId);
|
||||||
if (t) return t;
|
if (t) return t;
|
||||||
await new Promise((r) => setTimeout(r, poll));
|
await new Promise((r) => setTimeout(r, poll));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,13 +113,13 @@ export class FormulaService {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Enqueue a full recompute for the given formula property IDs on the given
|
* Enqueue a full recompute for the given formula property IDs on the given
|
||||||
* base. Reasons let the worker log why the job ran. Job ID includes baseId
|
* base. Reasons let the worker log why the job ran. Job ID includes pageId
|
||||||
* so BullMQ will dedupe when the same base has multiple edits in flight —
|
* so BullMQ will dedupe when the same base has multiple edits in flight —
|
||||||
* see FormulaLock for the per-base Redis serialization.
|
* see FormulaLock for the per-base Redis serialization.
|
||||||
*/
|
*/
|
||||||
async enqueueRecompute(args: IBaseFormulaRecomputeJob): Promise<void> {
|
async enqueueRecompute(args: IBaseFormulaRecomputeJob): Promise<void> {
|
||||||
await this.queue.add(QueueJob.BASE_FORMULA_RECOMPUTE, args, {
|
await this.queue.add(QueueJob.BASE_FORMULA_RECOMPUTE, args, {
|
||||||
jobId: `formula-recompute:${args.baseId}:${Date.now()}`,
|
jobId: `formula-recompute:${args.pageId}:${Date.now()}`,
|
||||||
removeOnComplete: 1000,
|
removeOnComplete: 1000,
|
||||||
removeOnFail: 1000,
|
removeOnFail: 1000,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export class BaseQueueProcessor
|
|||||||
trx,
|
trx,
|
||||||
);
|
);
|
||||||
await this.basePropertyRepo.bumpSchemaVersion(data.propertyId, trx);
|
await this.basePropertyRepo.bumpSchemaVersion(data.propertyId, trx);
|
||||||
const v = await this.baseRepo.bumpSchemaVersion(data.baseId, trx);
|
const v = await this.baseRepo.bumpSchemaVersion(data.pageId, trx);
|
||||||
return { summary: s, schemaVersion: v };
|
return { summary: s, schemaVersion: v };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -80,7 +80,7 @@ export class BaseQueueProcessor
|
|||||||
const updated = await this.basePropertyRepo.findById(data.propertyId);
|
const updated = await this.basePropertyRepo.findById(data.propertyId);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const event: BasePropertyUpdatedEvent = {
|
const event: BasePropertyUpdatedEvent = {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: data.actorId ?? null,
|
actorId: data.actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -90,7 +90,7 @@ export class BaseQueueProcessor
|
|||||||
this.eventEmitter.emit(EventName.BASE_PROPERTY_UPDATED, event);
|
this.eventEmitter.emit(EventName.BASE_PROPERTY_UPDATED, event);
|
||||||
}
|
}
|
||||||
this.emitSchemaBumped(
|
this.emitSchemaBumped(
|
||||||
data.baseId,
|
data.pageId,
|
||||||
data.workspaceId,
|
data.workspaceId,
|
||||||
schemaVersion,
|
schemaVersion,
|
||||||
data.actorId,
|
data.actorId,
|
||||||
@@ -106,24 +106,24 @@ export class BaseQueueProcessor
|
|||||||
data,
|
data,
|
||||||
);
|
);
|
||||||
const schemaVersion = await this.baseRepo.bumpSchemaVersion(
|
const schemaVersion = await this.baseRepo.bumpSchemaVersion(
|
||||||
data.baseId,
|
data.pageId,
|
||||||
);
|
);
|
||||||
this.emitSchemaBumped(data.baseId, data.workspaceId, schemaVersion);
|
this.emitSchemaBumped(data.pageId, data.workspaceId, schemaVersion);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
case QueueJob.BASE_FORMULA_RECOMPUTE: {
|
case QueueJob.BASE_FORMULA_RECOMPUTE: {
|
||||||
const data = job.data as IBaseFormulaRecomputeJob;
|
const data = job.data as IBaseFormulaRecomputeJob;
|
||||||
const token = await this.formulaLock.acquireWait(data.baseId, {
|
const token = await this.formulaLock.acquireWait(data.pageId, {
|
||||||
timeoutMs: 30_000,
|
timeoutMs: 30_000,
|
||||||
});
|
});
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`formula recompute: lock acquire timeout for base ${data.baseId}`,
|
`formula recompute: lock acquire timeout for base ${data.pageId}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
this.eventEmitter.emit(EventName.BASE_FORMULA_RECOMPUTE_STARTED, {
|
this.eventEmitter.emit(EventName.BASE_FORMULA_RECOMPUTE_STARTED, {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: data.actorId ?? null,
|
actorId: data.actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -140,7 +140,7 @@ export class BaseQueueProcessor
|
|||||||
progress: (processed) => job.updateProgress({ processed }),
|
progress: (processed) => job.updateProgress({ processed }),
|
||||||
onBatch: async (batch) => {
|
onBatch: async (batch) => {
|
||||||
this.eventEmitter.emit(EventName.BASE_ROWS_UPDATED, {
|
this.eventEmitter.emit(EventName.BASE_ROWS_UPDATED, {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: null,
|
actorId: null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -152,10 +152,10 @@ export class BaseQueueProcessor
|
|||||||
);
|
);
|
||||||
|
|
||||||
const schemaVersion = await this.baseRepo.bumpSchemaVersion(
|
const schemaVersion = await this.baseRepo.bumpSchemaVersion(
|
||||||
data.baseId,
|
data.pageId,
|
||||||
);
|
);
|
||||||
this.eventEmitter.emit(EventName.BASE_SCHEMA_BUMPED, {
|
this.eventEmitter.emit(EventName.BASE_SCHEMA_BUMPED, {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: data.actorId ?? null,
|
actorId: data.actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -163,7 +163,7 @@ export class BaseQueueProcessor
|
|||||||
} satisfies BaseSchemaBumpedEvent);
|
} satisfies BaseSchemaBumpedEvent);
|
||||||
|
|
||||||
this.eventEmitter.emit(EventName.BASE_FORMULA_RECOMPUTE_COMPLETED, {
|
this.eventEmitter.emit(EventName.BASE_FORMULA_RECOMPUTE_COMPLETED, {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: data.actorId ?? null,
|
actorId: data.actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -175,7 +175,7 @@ export class BaseQueueProcessor
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
await this.formulaLock.release(data.baseId, token);
|
await this.formulaLock.release(data.pageId, token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
@@ -184,13 +184,13 @@ export class BaseQueueProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
private emitSchemaBumped(
|
private emitSchemaBumped(
|
||||||
baseId: string,
|
pageId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
schemaVersion: number,
|
schemaVersion: number,
|
||||||
actorId?: string,
|
actorId?: string,
|
||||||
): void {
|
): void {
|
||||||
const event: BaseSchemaBumpedEvent = {
|
const event: BaseSchemaBumpedEvent = {
|
||||||
baseId,
|
pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -220,7 +220,7 @@ export class BaseQueueProcessor
|
|||||||
const reverted = await this.basePropertyRepo.findById(data.propertyId);
|
const reverted = await this.basePropertyRepo.findById(data.propertyId);
|
||||||
if (reverted) {
|
if (reverted) {
|
||||||
const event: BasePropertyUpdatedEvent = {
|
const event: BasePropertyUpdatedEvent = {
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
workspaceId: data.workspaceId,
|
workspaceId: data.workspaceId,
|
||||||
actorId: data.actorId ?? null,
|
actorId: data.actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export type PresenceEntry = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Ephemeral per-base presence. No DB. `presence:base:{baseId}` is a Redis
|
* Ephemeral per-base presence. No DB. `presence:base:{pageId}` is a Redis
|
||||||
* HASH keyed by userId with a JSON-serialised entry. Entries older than
|
* HASH keyed by userId with a JSON-serialised entry. Entries older than
|
||||||
* PRESENCE_ENTRY_TTL_MS are filtered on read; the key itself is refreshed
|
* PRESENCE_ENTRY_TTL_MS are filtered on read; the key itself is refreshed
|
||||||
* with a longer Redis EXPIRE on every write so unused rooms drain on
|
* with a longer Redis EXPIRE on every write so unused rooms drain on
|
||||||
@@ -30,10 +30,10 @@ export class BasePresenceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async setPresence(
|
async setPresence(
|
||||||
baseId: string,
|
pageId: string,
|
||||||
entry: PresenceEntry,
|
entry: PresenceEntry,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const key = PRESENCE_KEY_PREFIX + baseId;
|
const key = PRESENCE_KEY_PREFIX + pageId;
|
||||||
await this.redis
|
await this.redis
|
||||||
.multi()
|
.multi()
|
||||||
.hset(key, entry.userId, JSON.stringify(entry))
|
.hset(key, entry.userId, JSON.stringify(entry))
|
||||||
@@ -41,13 +41,13 @@ export class BasePresenceService {
|
|||||||
.exec();
|
.exec();
|
||||||
}
|
}
|
||||||
|
|
||||||
async leave(baseId: string, userId: string): Promise<void> {
|
async leave(pageId: string, userId: string): Promise<void> {
|
||||||
const key = PRESENCE_KEY_PREFIX + baseId;
|
const key = PRESENCE_KEY_PREFIX + pageId;
|
||||||
await this.redis.hdel(key, userId);
|
await this.redis.hdel(key, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async snapshot(baseId: string): Promise<PresenceEntry[]> {
|
async snapshot(pageId: string): Promise<PresenceEntry[]> {
|
||||||
const key = PRESENCE_KEY_PREFIX + baseId;
|
const key = PRESENCE_KEY_PREFIX + pageId;
|
||||||
const raw = await this.redis.hgetall(key);
|
const raw = await this.redis.hgetall(key);
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const out: PresenceEntry[] = [];
|
const out: PresenceEntry[] = [];
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* In-process listeners that forward base domain events onto the
|
* In-process listeners that forward base domain events onto the
|
||||||
* `base-{baseId}` socket.io room. Originating clients suppress their own
|
* `base-{pageId}` socket.io room. Originating clients suppress their own
|
||||||
* echoes via `requestId`.
|
* echoes via `requestId`.
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -34,9 +34,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_ROW_CREATED)
|
@OnEvent(EventName.BASE_ROW_CREATED)
|
||||||
onRowCreated(e: BaseRowCreatedEvent) {
|
onRowCreated(e: BaseRowCreatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:row:created',
|
operation: 'base:row:created',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
row: e.row,
|
row: e.row,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -45,9 +45,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_ROW_UPDATED)
|
@OnEvent(EventName.BASE_ROW_UPDATED)
|
||||||
onRowUpdated(e: BaseRowUpdatedEvent) {
|
onRowUpdated(e: BaseRowUpdatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:row:updated',
|
operation: 'base:row:updated',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
rowId: e.rowId,
|
rowId: e.rowId,
|
||||||
patch: e.patch,
|
patch: e.patch,
|
||||||
updatedCells: e.updatedCells,
|
updatedCells: e.updatedCells,
|
||||||
@@ -58,9 +58,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_ROW_DELETED)
|
@OnEvent(EventName.BASE_ROW_DELETED)
|
||||||
onRowDeleted(e: BaseRowDeletedEvent) {
|
onRowDeleted(e: BaseRowDeletedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:row:deleted',
|
operation: 'base:row:deleted',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
rowId: e.rowId,
|
rowId: e.rowId,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -69,9 +69,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_ROWS_DELETED)
|
@OnEvent(EventName.BASE_ROWS_DELETED)
|
||||||
onRowsDeleted(e: BaseRowsDeletedEvent) {
|
onRowsDeleted(e: BaseRowsDeletedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:rows:deleted',
|
operation: 'base:rows:deleted',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
rowIds: e.rowIds,
|
rowIds: e.rowIds,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -80,9 +80,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_ROW_REORDERED)
|
@OnEvent(EventName.BASE_ROW_REORDERED)
|
||||||
onRowReordered(e: BaseRowReorderedEvent) {
|
onRowReordered(e: BaseRowReorderedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:row:reordered',
|
operation: 'base:row:reordered',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
rowId: e.rowId,
|
rowId: e.rowId,
|
||||||
position: e.position,
|
position: e.position,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
@@ -92,9 +92,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_PROPERTY_CREATED)
|
@OnEvent(EventName.BASE_PROPERTY_CREATED)
|
||||||
onPropertyCreated(e: BasePropertyCreatedEvent) {
|
onPropertyCreated(e: BasePropertyCreatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:property:created',
|
operation: 'base:property:created',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
property: e.property,
|
property: e.property,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -103,9 +103,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_PROPERTY_UPDATED)
|
@OnEvent(EventName.BASE_PROPERTY_UPDATED)
|
||||||
onPropertyUpdated(e: BasePropertyUpdatedEvent) {
|
onPropertyUpdated(e: BasePropertyUpdatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:property:updated',
|
operation: 'base:property:updated',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
property: e.property,
|
property: e.property,
|
||||||
schemaVersion: e.schemaVersion,
|
schemaVersion: e.schemaVersion,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
@@ -115,9 +115,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_PROPERTY_DELETED)
|
@OnEvent(EventName.BASE_PROPERTY_DELETED)
|
||||||
onPropertyDeleted(e: BasePropertyDeletedEvent) {
|
onPropertyDeleted(e: BasePropertyDeletedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:property:deleted',
|
operation: 'base:property:deleted',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
propertyId: e.propertyId,
|
propertyId: e.propertyId,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -126,9 +126,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_PROPERTY_REORDERED)
|
@OnEvent(EventName.BASE_PROPERTY_REORDERED)
|
||||||
onPropertyReordered(e: BasePropertyReorderedEvent) {
|
onPropertyReordered(e: BasePropertyReorderedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:property:reordered',
|
operation: 'base:property:reordered',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
propertyId: e.propertyId,
|
propertyId: e.propertyId,
|
||||||
position: e.position,
|
position: e.position,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
@@ -138,9 +138,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_VIEW_CREATED)
|
@OnEvent(EventName.BASE_VIEW_CREATED)
|
||||||
onViewCreated(e: BaseViewCreatedEvent) {
|
onViewCreated(e: BaseViewCreatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:view:created',
|
operation: 'base:view:created',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
view: e.view,
|
view: e.view,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -149,9 +149,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_VIEW_UPDATED)
|
@OnEvent(EventName.BASE_VIEW_UPDATED)
|
||||||
onViewUpdated(e: BaseViewUpdatedEvent) {
|
onViewUpdated(e: BaseViewUpdatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:view:updated',
|
operation: 'base:view:updated',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
view: e.view,
|
view: e.view,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -160,9 +160,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_VIEW_DELETED)
|
@OnEvent(EventName.BASE_VIEW_DELETED)
|
||||||
onViewDeleted(e: BaseViewDeletedEvent) {
|
onViewDeleted(e: BaseViewDeletedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:view:deleted',
|
operation: 'base:view:deleted',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
viewId: e.viewId,
|
viewId: e.viewId,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
requestId: e.requestId ?? null,
|
requestId: e.requestId ?? null,
|
||||||
@@ -171,18 +171,18 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_SCHEMA_BUMPED)
|
@OnEvent(EventName.BASE_SCHEMA_BUMPED)
|
||||||
onSchemaBumped(e: BaseSchemaBumpedEvent) {
|
onSchemaBumped(e: BaseSchemaBumpedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:schema:bumped',
|
operation: 'base:schema:bumped',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
schemaVersion: e.schemaVersion,
|
schemaVersion: e.schemaVersion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent(EventName.BASE_ROWS_UPDATED)
|
@OnEvent(EventName.BASE_ROWS_UPDATED)
|
||||||
onRowsUpdated(e: BaseRowsUpdatedEvent) {
|
onRowsUpdated(e: BaseRowsUpdatedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:rows:updated',
|
operation: 'base:rows:updated',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
rowIds: e.rowIds,
|
rowIds: e.rowIds,
|
||||||
propertyIds: e.propertyIds,
|
propertyIds: e.propertyIds,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
@@ -192,9 +192,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_FORMULA_RECOMPUTE_STARTED)
|
@OnEvent(EventName.BASE_FORMULA_RECOMPUTE_STARTED)
|
||||||
onFormulaRecomputeStarted(e: BaseFormulaRecomputeStartedEvent) {
|
onFormulaRecomputeStarted(e: BaseFormulaRecomputeStartedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:formula:recompute:started',
|
operation: 'base:formula:recompute:started',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
propertyIds: e.propertyIds,
|
propertyIds: e.propertyIds,
|
||||||
jobId: e.jobId,
|
jobId: e.jobId,
|
||||||
actorId: e.actorId ?? null,
|
actorId: e.actorId ?? null,
|
||||||
@@ -203,9 +203,9 @@ export class BaseWsConsumers {
|
|||||||
|
|
||||||
@OnEvent(EventName.BASE_FORMULA_RECOMPUTE_COMPLETED)
|
@OnEvent(EventName.BASE_FORMULA_RECOMPUTE_COMPLETED)
|
||||||
onFormulaRecomputeCompleted(e: BaseFormulaRecomputeCompletedEvent) {
|
onFormulaRecomputeCompleted(e: BaseFormulaRecomputeCompletedEvent) {
|
||||||
this.ws.emitToBase(e.baseId, {
|
this.ws.emitToBase(e.pageId, {
|
||||||
operation: 'base:formula:recompute:completed',
|
operation: 'base:formula:recompute:completed',
|
||||||
baseId: e.baseId,
|
pageId: e.pageId,
|
||||||
propertyIds: e.propertyIds,
|
propertyIds: e.propertyIds,
|
||||||
jobId: e.jobId,
|
jobId: e.jobId,
|
||||||
processed: e.processed,
|
processed: e.processed,
|
||||||
|
|||||||
@@ -9,29 +9,29 @@ import { BasePresenceService, PresenceEntry } from './base-presence.service';
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Inbound shapes from untrusted socket clients. Zod-validated at the
|
* Inbound shapes from untrusted socket clients. Zod-validated at the
|
||||||
* boundary so malformed payloads (non-uuid baseId, missing fields,
|
* boundary so malformed payloads (non-uuid pageId, missing fields,
|
||||||
* oversized selection blobs) never reach the permission check or Redis.
|
* oversized selection blobs) never reach the permission check or Redis.
|
||||||
*/
|
*/
|
||||||
const baseSubscribeSchema = z.object({
|
const baseSubscribeSchema = z.object({
|
||||||
operation: z.literal('base:subscribe'),
|
operation: z.literal('base:subscribe'),
|
||||||
baseId: z.uuid(),
|
pageId: z.uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const baseUnsubscribeSchema = z.object({
|
const baseUnsubscribeSchema = z.object({
|
||||||
operation: z.literal('base:unsubscribe'),
|
operation: z.literal('base:unsubscribe'),
|
||||||
baseId: z.uuid(),
|
pageId: z.uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const basePresenceSchema = z.object({
|
const basePresenceSchema = z.object({
|
||||||
operation: z.literal('base:presence'),
|
operation: z.literal('base:presence'),
|
||||||
baseId: z.uuid(),
|
pageId: z.uuid(),
|
||||||
cellId: z.string().max(200).optional().nullable(),
|
cellId: z.string().max(200).optional().nullable(),
|
||||||
selection: z.unknown().optional(),
|
selection: z.unknown().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const basePresenceLeaveSchema = z.object({
|
const basePresenceLeaveSchema = z.object({
|
||||||
operation: z.literal('base:presence:leave'),
|
operation: z.literal('base:presence:leave'),
|
||||||
baseId: z.uuid(),
|
pageId: z.uuid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const inboundSchema = z.union([
|
const inboundSchema = z.union([
|
||||||
@@ -77,23 +77,23 @@ export class BaseWsService {
|
|||||||
const data = parsed.data;
|
const data = parsed.data;
|
||||||
switch (data.operation) {
|
switch (data.operation) {
|
||||||
case 'base:subscribe':
|
case 'base:subscribe':
|
||||||
await this.subscribe(client, data.baseId);
|
await this.subscribe(client, data.pageId);
|
||||||
return;
|
return;
|
||||||
case 'base:unsubscribe':
|
case 'base:unsubscribe':
|
||||||
await this.unsubscribe(client, data.baseId);
|
await this.unsubscribe(client, data.pageId);
|
||||||
return;
|
return;
|
||||||
case 'base:presence':
|
case 'base:presence':
|
||||||
await this.handlePresence(client, data);
|
await this.handlePresence(client, data);
|
||||||
return;
|
return;
|
||||||
case 'base:presence:leave':
|
case 'base:presence:leave':
|
||||||
await this.handlePresenceLeave(client, data.baseId);
|
await this.handlePresenceLeave(client, data.pageId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
emitToBase(baseId: string, payload: BaseOutbound): void {
|
emitToBase(pageId: string, payload: BaseOutbound): void {
|
||||||
if (!this.server) return;
|
if (!this.server) return;
|
||||||
this.server.to(getBaseRoomName(baseId)).emit('message', payload);
|
this.server.to(getBaseRoomName(pageId)).emit('message', payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -106,11 +106,11 @@ export class BaseWsService {
|
|||||||
const userId = client.data?.userId as string | undefined;
|
const userId = client.data?.userId as string | undefined;
|
||||||
const subs = this.subscriptionsFor(client);
|
const subs = this.subscriptionsFor(client);
|
||||||
if (!userId || subs.size === 0) return;
|
if (!userId || subs.size === 0) return;
|
||||||
for (const baseId of subs) {
|
for (const pageId of subs) {
|
||||||
await this.presence.leave(baseId, userId);
|
await this.presence.leave(pageId, userId);
|
||||||
this.emitToBase(baseId, {
|
this.emitToBase(pageId, {
|
||||||
operation: 'base:presence:leave',
|
operation: 'base:presence:leave',
|
||||||
baseId,
|
pageId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -119,22 +119,22 @@ export class BaseWsService {
|
|||||||
|
|
||||||
// --- private -------------------------------------------------------
|
// --- private -------------------------------------------------------
|
||||||
|
|
||||||
private async subscribe(client: Socket, baseId: string): Promise<void> {
|
private async subscribe(client: Socket, pageId: string): Promise<void> {
|
||||||
const userId = client.data?.userId as string | undefined;
|
const userId = client.data?.userId as string | undefined;
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
client.emit('message', {
|
client.emit('message', {
|
||||||
operation: 'base:subscribe:error',
|
operation: 'base:subscribe:error',
|
||||||
baseId,
|
pageId,
|
||||||
reason: 'unauthenticated',
|
reason: 'unauthenticated',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const base = await this.baseRepo.findById(baseId);
|
const base = await this.baseRepo.findById(pageId);
|
||||||
if (!base) {
|
if (!base) {
|
||||||
client.emit('message', {
|
client.emit('message', {
|
||||||
operation: 'base:subscribe:error',
|
operation: 'base:subscribe:error',
|
||||||
baseId,
|
pageId,
|
||||||
reason: 'not_found',
|
reason: 'not_found',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -144,36 +144,36 @@ export class BaseWsService {
|
|||||||
if (!canRead) {
|
if (!canRead) {
|
||||||
client.emit('message', {
|
client.emit('message', {
|
||||||
operation: 'base:subscribe:error',
|
operation: 'base:subscribe:error',
|
||||||
baseId,
|
pageId,
|
||||||
reason: 'forbidden',
|
reason: 'forbidden',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
client.join(getBaseRoomName(baseId));
|
client.join(getBaseRoomName(pageId));
|
||||||
this.subscriptionsFor(client).add(baseId);
|
this.subscriptionsFor(client).add(pageId);
|
||||||
|
|
||||||
// Send the current presence snapshot to just this client so their UI
|
// Send the current presence snapshot to just this client so their UI
|
||||||
// can paint who's already editing what.
|
// can paint who's already editing what.
|
||||||
const snapshot = await this.presence.snapshot(baseId);
|
const snapshot = await this.presence.snapshot(pageId);
|
||||||
client.emit('message', {
|
client.emit('message', {
|
||||||
operation: 'base:presence:snapshot',
|
operation: 'base:presence:snapshot',
|
||||||
baseId,
|
pageId,
|
||||||
entries: snapshot,
|
entries: snapshot,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unsubscribe(client: Socket, baseId: string): Promise<void> {
|
private async unsubscribe(client: Socket, pageId: string): Promise<void> {
|
||||||
const userId = client.data?.userId as string | undefined;
|
const userId = client.data?.userId as string | undefined;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
client.leave(getBaseRoomName(baseId));
|
client.leave(getBaseRoomName(pageId));
|
||||||
this.subscriptionsFor(client).delete(baseId);
|
this.subscriptionsFor(client).delete(pageId);
|
||||||
|
|
||||||
await this.presence.leave(baseId, userId);
|
await this.presence.leave(pageId, userId);
|
||||||
this.emitToBase(baseId, {
|
this.emitToBase(pageId, {
|
||||||
operation: 'base:presence:leave',
|
operation: 'base:presence:leave',
|
||||||
baseId,
|
pageId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ export class BaseWsService {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const userId = client.data?.userId as string | undefined;
|
const userId = client.data?.userId as string | undefined;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
if (!client.rooms.has(getBaseRoomName(data.baseId))) return;
|
if (!client.rooms.has(getBaseRoomName(data.pageId))) return;
|
||||||
|
|
||||||
const entry: PresenceEntry = {
|
const entry: PresenceEntry = {
|
||||||
userId,
|
userId,
|
||||||
@@ -192,25 +192,25 @@ export class BaseWsService {
|
|||||||
selection: data.selection ?? null,
|
selection: data.selection ?? null,
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
};
|
};
|
||||||
await this.presence.setPresence(data.baseId, entry);
|
await this.presence.setPresence(data.pageId, entry);
|
||||||
|
|
||||||
this.emitToBase(data.baseId, {
|
this.emitToBase(data.pageId, {
|
||||||
operation: 'base:presence',
|
operation: 'base:presence',
|
||||||
baseId: data.baseId,
|
pageId: data.pageId,
|
||||||
...entry,
|
...entry,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handlePresenceLeave(
|
private async handlePresenceLeave(
|
||||||
client: Socket,
|
client: Socket,
|
||||||
baseId: string,
|
pageId: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const userId = client.data?.userId as string | undefined;
|
const userId = client.data?.userId as string | undefined;
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
await this.presence.leave(baseId, userId);
|
await this.presence.leave(pageId, userId);
|
||||||
this.emitToBase(baseId, {
|
this.emitToBase(pageId, {
|
||||||
operation: 'base:presence:leave',
|
operation: 'base:presence:leave',
|
||||||
baseId,
|
pageId,
|
||||||
userId,
|
userId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,16 +31,16 @@ export class BaseCsvExportService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async streamBaseAsCsv(
|
async streamBaseAsCsv(
|
||||||
baseId: string,
|
pageId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
reply: FastifyReply,
|
reply: FastifyReply,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const base = await this.baseRepo.findById(baseId);
|
const base = await this.baseRepo.findById(pageId);
|
||||||
if (!base || base.workspaceId !== workspaceId) {
|
if (!base || base.workspaceId !== workspaceId) {
|
||||||
throw new NotFoundException('Base not found');
|
throw new NotFoundException('Base not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
const properties = await this.basePropertyRepo.findByPageId(baseId);
|
const properties = await this.basePropertyRepo.findByPageId(pageId);
|
||||||
|
|
||||||
const fileName = sanitize(base.title || 'base') + '.csv';
|
const fileName = sanitize(base.title || 'base') + '.csv';
|
||||||
|
|
||||||
@@ -85,7 +85,7 @@ export class BaseCsvExportService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const chunk of this.baseRowRepo.streamByPageId(baseId, {
|
for await (const chunk of this.baseRowRepo.streamByPageId(pageId, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
chunkSize: CHUNK_SIZE,
|
chunkSize: CHUNK_SIZE,
|
||||||
})) {
|
})) {
|
||||||
@@ -129,7 +129,7 @@ export class BaseCsvExportService {
|
|||||||
// trigger Nest's exception filter to try to send another
|
// trigger Nest's exception filter to try to send another
|
||||||
// response, which Fastify rejects. Destroying the stringifier
|
// response, which Fastify rejects. Destroying the stringifier
|
||||||
// cascades to `out` and signals EOF to the client.
|
// cascades to `out` and signals EOF to the client.
|
||||||
this.logger.error(`csv export failed base=${baseId}`, err);
|
this.logger.error(`csv export failed base=${pageId}`, err);
|
||||||
stringifier.destroy(err as Error);
|
stringifier.destroy(err as Error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export class BasePropertyService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BasePropertyCreatedEvent = {
|
const event: BasePropertyCreatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -144,7 +144,7 @@ export class BasePropertyService {
|
|||||||
|
|
||||||
if (created.type === 'formula') {
|
if (created.type === 'formula') {
|
||||||
await this.formulaService.enqueueRecompute({
|
await this.formulaService.enqueueRecompute({
|
||||||
baseId: created.pageId,
|
pageId: created.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
propertyIds: [created.id],
|
propertyIds: [created.id],
|
||||||
reason: 'formula_created',
|
reason: 'formula_created',
|
||||||
@@ -278,7 +278,7 @@ export class BasePropertyService {
|
|||||||
|
|
||||||
if (newType === 'formula' && (isTypeChange || sourceChanged)) {
|
if (newType === 'formula' && (isTypeChange || sourceChanged)) {
|
||||||
await this.formulaService.enqueueRecompute({
|
await this.formulaService.enqueueRecompute({
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
propertyIds: [dto.propertyId],
|
propertyIds: [dto.propertyId],
|
||||||
reason: isTypeChange ? 'formula_created' : 'formula_edited',
|
reason: isTypeChange ? 'formula_created' : 'formula_edited',
|
||||||
@@ -292,7 +292,7 @@ export class BasePropertyService {
|
|||||||
const affected = graph.affectedFormulas([dto.propertyId]);
|
const affected = graph.affectedFormulas([dto.propertyId]);
|
||||||
if (affected.length > 0) {
|
if (affected.length > 0) {
|
||||||
await this.formulaService.enqueueRecompute({
|
await this.formulaService.enqueueRecompute({
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
propertyIds: affected,
|
propertyIds: affected,
|
||||||
reason: 'dep_type_changed',
|
reason: 'dep_type_changed',
|
||||||
@@ -306,7 +306,7 @@ export class BasePropertyService {
|
|||||||
|
|
||||||
// --- Path 2 or 3: cell rewrite needed -------------------------------
|
// --- Path 2 or 3: cell rewrite needed -------------------------------
|
||||||
const conversionPayload: IBaseTypeConversionJob = {
|
const conversionPayload: IBaseTypeConversionJob = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
propertyId: dto.propertyId,
|
propertyId: dto.propertyId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
fromType: oldType,
|
fromType: oldType,
|
||||||
@@ -360,7 +360,7 @@ export class BasePropertyService {
|
|||||||
});
|
});
|
||||||
tick('inline-tx-done');
|
tick('inline-tx-done');
|
||||||
const bumpEvent: BaseSchemaBumpedEvent = {
|
const bumpEvent: BaseSchemaBumpedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -454,7 +454,7 @@ export class BasePropertyService {
|
|||||||
const updated = await this.basePropertyRepo.findById(dto.propertyId);
|
const updated = await this.basePropertyRepo.findById(dto.propertyId);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const event: BasePropertyUpdatedEvent = {
|
const event: BasePropertyUpdatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -515,7 +515,7 @@ export class BasePropertyService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const payload: IBaseCellGcJob = {
|
const payload: IBaseCellGcJob = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
propertyId: dto.propertyId,
|
propertyId: dto.propertyId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
};
|
};
|
||||||
@@ -542,7 +542,7 @@ export class BasePropertyService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event: BasePropertyDeletedEvent = {
|
const event: BasePropertyDeletedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -552,7 +552,7 @@ export class BasePropertyService {
|
|||||||
|
|
||||||
if (affected.length > 0) {
|
if (affected.length > 0) {
|
||||||
await this.formulaService.enqueueRecompute({
|
await this.formulaService.enqueueRecompute({
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
propertyIds: affected,
|
propertyIds: affected,
|
||||||
reason: 'dep_deleted',
|
reason: 'dep_deleted',
|
||||||
@@ -580,7 +580,7 @@ export class BasePropertyService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BasePropertyReorderedEvent = {
|
const event: BasePropertyReorderedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: actorId ?? null,
|
actorId: actorId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class BaseRowService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BaseRowCreatedEvent = {
|
const event: BaseRowCreatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -162,7 +162,7 @@ export class BaseRowService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const event: BaseRowUpdatedEvent = {
|
const event: BaseRowUpdatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -187,7 +187,7 @@ export class BaseRowService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BaseRowDeletedEvent = {
|
const event: BaseRowDeletedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -215,7 +215,7 @@ export class BaseRowService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BaseRowsDeletedEvent = {
|
const event: BaseRowsDeletedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
@@ -310,7 +310,7 @@ export class BaseRowService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BaseRowReorderedEvent = {
|
const event: BaseRowReorderedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: dto.requestId ?? null,
|
requestId: dto.requestId ?? null,
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export class BaseViewService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const event: BaseViewCreatedEvent = {
|
const event: BaseViewCreatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId,
|
actorId: userId,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -101,7 +101,7 @@ export class BaseViewService {
|
|||||||
|
|
||||||
if (updated) {
|
if (updated) {
|
||||||
const event: BaseViewUpdatedEvent = {
|
const event: BaseViewUpdatedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
@@ -133,7 +133,7 @@ export class BaseViewService {
|
|||||||
await this.baseViewRepo.deleteView(dto.viewId, { workspaceId });
|
await this.baseViewRepo.deleteView(dto.viewId, { workspaceId });
|
||||||
|
|
||||||
const event: BaseViewDeletedEvent = {
|
const event: BaseViewDeletedEvent = {
|
||||||
baseId: dto.pageId,
|
pageId: dto.pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: userId ?? null,
|
actorId: userId ?? null,
|
||||||
requestId: null,
|
requestId: null,
|
||||||
|
|||||||
@@ -107,13 +107,13 @@ export class BaseService {
|
|||||||
return this.baseRepo.findById(dto.pageId);
|
return this.baseRepo.findById(dto.pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(baseId: string) {
|
async delete(pageId: string) {
|
||||||
const base = await this.baseRepo.findById(baseId);
|
const base = await this.baseRepo.findById(pageId);
|
||||||
if (!base) {
|
if (!base) {
|
||||||
throw new NotFoundException('Base not found');
|
throw new NotFoundException('Base not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.baseRepo.softDelete(baseId);
|
await this.baseRepo.softDelete(pageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async listBySpaceId(spaceId: string, pagination: PaginationOptions) {
|
async listBySpaceId(spaceId: string, pagination: PaginationOptions) {
|
||||||
|
|||||||
@@ -21,15 +21,15 @@ export async function processBaseCellGc(
|
|||||||
basePropertyRepo: BasePropertyRepo,
|
basePropertyRepo: BasePropertyRepo,
|
||||||
data: IBaseCellGcJob,
|
data: IBaseCellGcJob,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { baseId, propertyId, workspaceId } = data;
|
const { pageId, propertyId, workspaceId } = data;
|
||||||
|
|
||||||
await executeTx(db, async (trx) => {
|
await executeTx(db, async (trx) => {
|
||||||
await baseRowRepo.removeCellKey(baseId, propertyId, {
|
await baseRowRepo.removeCellKey(pageId, propertyId, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
trx,
|
trx,
|
||||||
});
|
});
|
||||||
await basePropertyRepo.hardDelete(propertyId, trx);
|
await basePropertyRepo.hardDelete(propertyId, trx);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.log(`cell-gc complete base=${baseId} prop=${propertyId}`);
|
logger.log(`cell-gc complete base=${pageId} prop=${propertyId}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,8 +30,8 @@ export async function processBaseFormulaRecompute(
|
|||||||
trx?: KyselyTransaction;
|
trx?: KyselyTransaction;
|
||||||
},
|
},
|
||||||
): Promise<{ processed: number; errored: number }> {
|
): Promise<{ processed: number; errored: number }> {
|
||||||
const { baseId, workspaceId, propertyIds, rowIds } = data;
|
const { pageId, workspaceId, propertyIds, rowIds } = data;
|
||||||
const properties = await basePropertyRepo.findByPageId(baseId);
|
const properties = await basePropertyRepo.findByPageId(pageId);
|
||||||
const targets = properties.filter(
|
const targets = properties.filter(
|
||||||
(p) => p.type === "formula" && propertyIds.includes(p.id),
|
(p) => p.type === "formula" && propertyIds.includes(p.id),
|
||||||
);
|
);
|
||||||
@@ -46,7 +46,7 @@ export async function processBaseFormulaRecompute(
|
|||||||
let processed = 0;
|
let processed = 0;
|
||||||
let errored = 0;
|
let errored = 0;
|
||||||
|
|
||||||
for await (const chunk of baseRowRepo.streamByPageId(baseId, {
|
for await (const chunk of baseRowRepo.streamByPageId(pageId, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
chunkSize: CHUNK_SIZE,
|
chunkSize: CHUNK_SIZE,
|
||||||
trx: opts?.trx,
|
trx: opts?.trx,
|
||||||
@@ -90,7 +90,7 @@ export async function processBaseFormulaRecompute(
|
|||||||
// so passing actorId: undefined preserves last_updated_by_id while still
|
// so passing actorId: undefined preserves last_updated_by_id while still
|
||||||
// bumping updated_at — matches spec "only lastEditedAt moves".
|
// bumping updated_at — matches spec "only lastEditedAt moves".
|
||||||
await baseRowRepo.batchUpdateCells(updates, {
|
await baseRowRepo.batchUpdateCells(updates, {
|
||||||
pageId: baseId,
|
pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId: undefined,
|
actorId: undefined,
|
||||||
trx: opts?.trx,
|
trx: opts?.trx,
|
||||||
@@ -102,7 +102,7 @@ export async function processBaseFormulaRecompute(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
`formula-recompute base=${baseId} props=${propertyIds.join(",")} processed=${processed} errored=${errored}`,
|
`formula-recompute base=${pageId} props=${propertyIds.join(",")} processed=${processed} errored=${errored}`,
|
||||||
);
|
);
|
||||||
return { processed, errored };
|
return { processed, errored };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export async function processBaseTypeConversion(
|
|||||||
},
|
},
|
||||||
): Promise<{ converted: number; cleared: number; total: number }> {
|
): Promise<{ converted: number; cleared: number; total: number }> {
|
||||||
const {
|
const {
|
||||||
baseId,
|
pageId,
|
||||||
propertyId,
|
propertyId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
fromType,
|
fromType,
|
||||||
@@ -59,7 +59,7 @@ export async function processBaseTypeConversion(
|
|||||||
// rewriting — everything else is already consistent with the new type
|
// rewriting — everything else is already consistent with the new type
|
||||||
// (empty value → empty value). Skips the full-table scan on bases
|
// (empty value → empty value). Skips the full-table scan on bases
|
||||||
// where the property was only ever set on a few rows.
|
// where the property was only ever set on a few rows.
|
||||||
for await (const chunk of baseRowRepo.streamByPageId(baseId, {
|
for await (const chunk of baseRowRepo.streamByPageId(pageId, {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
chunkSize: CHUNK_SIZE,
|
chunkSize: CHUNK_SIZE,
|
||||||
trx,
|
trx,
|
||||||
@@ -105,7 +105,7 @@ export async function processBaseTypeConversion(
|
|||||||
|
|
||||||
if (updates.length > 0) {
|
if (updates.length > 0) {
|
||||||
await baseRowRepo.batchUpdateCells(updates, {
|
await baseRowRepo.batchUpdateCells(updates, {
|
||||||
pageId: baseId,
|
pageId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actorId,
|
actorId,
|
||||||
trx,
|
trx,
|
||||||
@@ -116,7 +116,7 @@ export async function processBaseTypeConversion(
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger.log(
|
logger.log(
|
||||||
`type-conversion ${fromType}→${toType} base=${baseId} prop=${propertyId} total=${total} converted=${converted} cleared=${cleared}`,
|
`type-conversion ${fromType}→${toType} base=${pageId} prop=${propertyId} total=${total} converted=${converted} cleared=${cleared}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { converted, cleared, total };
|
return { converted, cleared, total };
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ export interface IApprovalRejectedNotificationJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IBaseTypeConversionJob {
|
export interface IBaseTypeConversionJob {
|
||||||
baseId: string;
|
pageId: string;
|
||||||
propertyId: string;
|
propertyId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
fromType: string;
|
fromType: string;
|
||||||
@@ -133,13 +133,13 @@ export interface IBaseTypeConversionJob {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface IBaseCellGcJob {
|
export interface IBaseCellGcJob {
|
||||||
baseId: string;
|
pageId: string;
|
||||||
propertyId: string;
|
propertyId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IBaseFormulaRecomputeJob {
|
export interface IBaseFormulaRecomputeJob {
|
||||||
baseId: string;
|
pageId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
propertyIds: string[]; // formula properties to recompute
|
propertyIds: string[]; // formula properties to recompute
|
||||||
reason:
|
reason:
|
||||||
|
|||||||
Reference in New Issue
Block a user