@rebasepro/server-postgresql 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -76,6 +76,21 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
76
76
  hasMore: boolean;
77
77
  };
78
78
  }
79
+ export type FilterOperator = WhereFilterOpShort;
80
+ /**
81
+ * Fluent Query Builder Interface supported on both client and server accessors.
82
+ * @group Data
83
+ */
84
+ export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
85
+ where(column: keyof M & string, operator: FilterOperator, value: unknown): this;
86
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): this;
87
+ limit(count: number): this;
88
+ offset(count: number): this;
89
+ search(searchString: string): this;
90
+ include(...relations: string[]): this;
91
+ find(): Promise<FindResponse<M>>;
92
+ listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
93
+ }
79
94
  /**
80
95
  * A single collection's CRUD accessor.
81
96
  *
@@ -124,6 +139,12 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
124
139
  * Count the number of records matching the given filter.
125
140
  */
126
141
  count?(params?: FindParams): Promise<number>;
142
+ where(column: keyof M & string, operator: FilterOperator, value: unknown): QueryBuilderInterface<M>;
143
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilderInterface<M>;
144
+ limit(count: number): QueryBuilderInterface<M>;
145
+ offset(count: number): QueryBuilderInterface<M>;
146
+ search(searchString: string): QueryBuilderInterface<M>;
147
+ include(...relations: string[]): QueryBuilderInterface<M>;
127
148
  }
128
149
  /**
129
150
  * The unified data access object.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rebasepro/server-postgresql",
3
3
  "type": "module",
4
- "version": "0.2.1",
4
+ "version": "0.2.3",
5
5
  "description": "PostgreSQL data source backend implementation for Rebase with Drizzle ORM",
6
6
  "funding": {
7
7
  "url": "https://github.com/sponsors/rebaseco"
@@ -43,6 +43,7 @@
43
43
  "node"
44
44
  ],
45
45
  "moduleNameMapper": {
46
+ "^@rebasepro/client$": "<rootDir>/../client/src/index.ts",
46
47
  "^@rebasepro/common$": "<rootDir>/../common/src/index.ts",
47
48
  "^@rebasepro/types$": "<rootDir>/../types/src/index.ts",
48
49
  "^@rebasepro/utils$": "<rootDir>/../utils/src/index.ts"
@@ -67,11 +68,11 @@
67
68
  "hono": "^4.12.21",
68
69
  "pg": "^8.21.0",
69
70
  "ws": "^8.20.1",
70
- "@rebasepro/sdk-generator": "0.2.1",
71
- "@rebasepro/common": "0.2.1",
72
- "@rebasepro/types": "0.2.1",
73
- "@rebasepro/server-core": "0.2.1",
74
- "@rebasepro/utils": "0.2.1"
71
+ "@rebasepro/common": "0.2.3",
72
+ "@rebasepro/server-core": "0.2.3",
73
+ "@rebasepro/types": "0.2.3",
74
+ "@rebasepro/utils": "0.2.3",
75
+ "@rebasepro/sdk-generator": "0.2.3"
75
76
  },
76
77
  "devDependencies": {
77
78
  "@types/jest": "^29.5.14",
@@ -117,7 +117,7 @@ export class PostgresBackendDriver implements DataDriver {
117
117
  if (!collection && !path) return { collection: undefined,
118
118
  callbacks: undefined,
119
119
  propertyCallbacks: undefined };
120
- const registryCollection = this.registry.getCollectionByPath(path);
120
+ const registryCollection = this.registry?.getCollectionByPath(path);
121
121
  const resolvedCollection = registryCollection
122
122
  ? { ...collection,
123
123
  ...registryCollection } as EntityCollection<M>
@@ -166,7 +166,8 @@ propertyCallbacks: undefined };
166
166
  user: this.user,
167
167
  driver: this,
168
168
  data: this.data,
169
- client: this.client
169
+ client: this.client,
170
+ storageSource: this.client?.storage
170
171
  } as unknown as RebaseCallContext; // Backend context
171
172
  return Promise.all(entities.map(async (entity) => {
172
173
  let fetched = entity;
@@ -276,7 +277,8 @@ propertyCallbacks: undefined };
276
277
  user: this.user,
277
278
  driver: this,
278
279
  data: this.data,
279
- client: this.client
280
+ client: this.client,
281
+ storageSource: this.client?.storage
280
282
  } as unknown as RebaseCallContext; // Backend context
281
283
  if (callbacks?.afterRead) {
282
284
  entity = await callbacks.afterRead({
@@ -359,7 +361,8 @@ propertyCallbacks: undefined };
359
361
  user: this.user,
360
362
  driver: this,
361
363
  data: this.data,
362
- client: this.client
364
+ client: this.client,
365
+ storageSource: this.client?.storage
363
366
  } as unknown as RebaseCallContext;
364
367
 
365
368
  // Fetch previous values for callbacks AND history recording
@@ -534,7 +537,8 @@ propertyCallbacks: undefined };
534
537
  user: this.user,
535
538
  driver: this,
536
539
  data: this.data,
537
- client: this.client
540
+ client: this.client,
541
+ storageSource: this.client?.storage
538
542
  } as unknown as RebaseCallContext;
539
543
 
540
544
  if (callbacks?.beforeDelete || propertyCallbacks?.beforeDelete) {
package/src/cli.ts CHANGED
@@ -478,11 +478,16 @@ async function runDrizzleKit(action: string, _rawArgs: string[]): Promise<void>
478
478
  const errorOutput = stderr || stdout;
479
479
  if (errorOutput) {
480
480
  const lines = errorOutput.split("\n").filter((l: string) => l.trim());
481
+ let printedCount = 0;
481
482
  for (const line of lines) {
482
483
  if (line.toLowerCase().includes("error") || line.includes("cannot") || line.includes("already exists") || line.includes("does not exist") || line.includes("violates") || line.includes("permission denied")) {
483
484
  console.error(chalk.red(` ${line.trim()}`));
485
+ printedCount++;
484
486
  }
485
487
  }
488
+ if (printedCount === 0) {
489
+ lines.slice(0, 10).forEach(line => console.error(chalk.red(` ${line.trim()}`)));
490
+ }
486
491
  }
487
492
  console.error("");
488
493
  process.exit(1);
@@ -5,6 +5,7 @@ export const defaultUsersCollection: PostgresCollection = {
5
5
  singularName: "User",
6
6
  slug: "users",
7
7
  table: "users",
8
+ schema: "rebase",
8
9
  icon: "Users",
9
10
  group: "Settings",
10
11
  properties: {
@@ -265,6 +265,8 @@ export async function checkCollectionsVsSdk(
265
265
  // ── Phase 2: Collections ↔ Database ──────────────────────────────────────
266
266
 
267
267
  interface DbColumn {
268
+ table_schema: string;
269
+ table_name: string;
268
270
  column_name: string;
269
271
  data_type: string;
270
272
  is_nullable: string;
@@ -288,27 +290,45 @@ export async function checkCollectionsVsDatabase(
288
290
  const { Pool } = pgModule.default ?? pgModule;
289
291
  const pool = new Pool({ connectionString: databaseUrl });
290
292
 
293
+ // Determine all schemas defined by the collections, plus public and rebase
294
+ const schemas = Array.from(new Set([
295
+ "public",
296
+ "rebase",
297
+ ...collections
298
+ .filter(isPostgresCollection)
299
+ .map(c => c.schema)
300
+ .filter((s): s is string => !!s)
301
+ ]));
302
+
291
303
  try {
292
- // Fetch all tables in the public schema
293
- const tablesResult = await pool.query<{ table_name: string }>(
294
- "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'"
304
+ // Fetch all tables in the defined schemas
305
+ const tablesResult = await pool.query<{ table_schema: string; table_name: string }>(
306
+ `SELECT table_schema, table_name
307
+ FROM information_schema.tables
308
+ WHERE table_schema = ANY($1) AND table_type = 'BASE TABLE'`,
309
+ [schemas]
295
310
  );
296
- const existingTables = new Set(tablesResult.rows.map((r) => r.table_name));
311
+ const existingTables = new Set(tablesResult.rows.map((r) =>
312
+ r.table_schema === "public" ? r.table_name : `${r.table_schema}.${r.table_name}`
313
+ ));
297
314
 
298
- // Fetch all columns
315
+ // Fetch all columns in the defined schemas
299
316
  const columnsResult = await pool.query<DbColumn>(
300
- `SELECT table_name, column_name, data_type, is_nullable, udt_name
317
+ `SELECT table_schema, table_name, column_name, data_type, is_nullable, udt_name
301
318
  FROM information_schema.columns
302
- WHERE table_schema = 'public'
303
- ORDER BY table_name, ordinal_position`
319
+ WHERE table_schema = ANY($1)
320
+ ORDER BY table_schema, table_name, ordinal_position`,
321
+ [schemas]
304
322
  );
305
323
  const columnsByTable = new Map<string, DbColumn[]>();
306
324
  for (const row of columnsResult.rows) {
307
- const tableName = (row as unknown as Record<string, string>).table_name;
308
- if (!columnsByTable.has(tableName)) {
309
- columnsByTable.set(tableName, []);
325
+ const tableSchema = row.table_schema;
326
+ const tableName = row.table_name;
327
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
328
+ if (!columnsByTable.has(key)) {
329
+ columnsByTable.set(key, []);
310
330
  }
311
- columnsByTable.get(tableName)!.push(row);
331
+ columnsByTable.get(key)!.push(row);
312
332
  }
313
333
 
314
334
  // Fetch enums
@@ -326,18 +346,22 @@ export async function checkCollectionsVsDatabase(
326
346
  enumsByName.get(row.enum_name)!.push(row.enum_value);
327
347
  }
328
348
 
329
- // Fetch foreign key constraints
349
+ // Fetch foreign key constraints in the defined schemas
330
350
  const fksResult = await pool.query<{
331
351
  constraint_name: string;
352
+ table_schema: string;
332
353
  table_name: string;
333
354
  column_name: string;
355
+ foreign_table_schema: string;
334
356
  foreign_table_name: string;
335
357
  foreign_column_name: string;
336
358
  }>(
337
359
  `SELECT
338
360
  tc.constraint_name,
361
+ tc.table_schema,
339
362
  tc.table_name,
340
363
  kcu.column_name,
364
+ ccu.table_schema AS foreign_table_schema,
341
365
  ccu.table_name AS foreign_table_name,
342
366
  ccu.column_name AS foreign_column_name
343
367
  FROM information_schema.table_constraints AS tc
@@ -345,14 +369,18 @@ export async function checkCollectionsVsDatabase(
345
369
  ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
346
370
  JOIN information_schema.constraint_column_usage AS ccu
347
371
  ON ccu.constraint_name = tc.constraint_name AND ccu.table_schema = tc.table_schema
348
- WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = 'public'`
372
+ WHERE tc.constraint_type = 'FOREIGN KEY' AND tc.table_schema = ANY($1)`,
373
+ [schemas]
349
374
  );
350
375
  const fksByTable = new Map<string, typeof fksResult.rows>();
351
376
  for (const row of fksResult.rows) {
352
- if (!fksByTable.has(row.table_name)) {
353
- fksByTable.set(row.table_name, []);
377
+ const tableSchema = row.table_schema;
378
+ const tableName = row.table_name;
379
+ const key = tableSchema === "public" ? tableName : `${tableSchema}.${tableName}`;
380
+ if (!fksByTable.has(key)) {
381
+ fksByTable.set(key, []);
354
382
  }
355
- fksByTable.get(row.table_name)!.push(row);
383
+ fksByTable.get(key)!.push(row);
356
384
  }
357
385
 
358
386
  // ── Compare each collection against the database ─────────────────
@@ -361,20 +389,22 @@ export async function checkCollectionsVsDatabase(
361
389
 
362
390
  for (const collection of postgresCollections) {
363
391
  const tableName = getTableName(collection);
392
+ const schemaName = collection.schema || "public";
393
+ const fullTableName = schemaName === "public" ? tableName : `${schemaName}.${tableName}`;
364
394
 
365
395
  // Check table existence
366
- if (!existingTables.has(tableName)) {
396
+ if (!existingTables.has(fullTableName)) {
367
397
  issues.push({
368
398
  severity: "error",
369
399
  category: "missing_table",
370
- table: tableName,
371
- message: `Table "${tableName}" does not exist in the database.`,
400
+ table: fullTableName,
401
+ message: `Table "${fullTableName}" does not exist in the database.`,
372
402
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
373
403
  });
374
404
  continue; // Skip column checks for missing tables
375
405
  }
376
406
 
377
- const dbColumns = columnsByTable.get(tableName) ?? [];
407
+ const dbColumns = columnsByTable.get(fullTableName) ?? [];
378
408
  const dbColumnMap = new Map(dbColumns.map((c) => [c.column_name, c]));
379
409
 
380
410
  // System columns that Rebase always creates
@@ -392,27 +422,36 @@ export async function checkCollectionsVsDatabase(
392
422
  issues.push({
393
423
  severity: "error",
394
424
  category: "missing_column",
395
- table: tableName,
425
+ table: fullTableName,
396
426
  column: fkColName,
397
- message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${tableName}".`,
427
+ message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${fullTableName}".`,
398
428
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
399
429
  });
400
430
  }
401
431
 
402
432
  // Check FK constraint exists
403
- const tableFks = fksByTable.get(tableName) ?? [];
404
- const hasFk = tableFks.some((fk) => fk.column_name === fkColName);
433
+ const tableFks = fksByTable.get(fullTableName) ?? [];
434
+ let targetTableName = "unknown";
435
+ let targetSchemaName = "public";
436
+ try {
437
+ const targetColl = relation.target();
438
+ targetTableName = getTableName(targetColl);
439
+ targetSchemaName = targetColl.schema || "public";
440
+ } catch { /* ignore */ }
441
+
442
+ const hasFk = tableFks.some((fk) =>
443
+ fk.column_name === fkColName &&
444
+ fk.foreign_table_name === targetTableName &&
445
+ fk.foreign_table_schema === targetSchemaName
446
+ );
447
+
405
448
  if (dbColumnMap.has(fkColName) && !hasFk) {
406
- let targetTableName = "unknown";
407
- try {
408
- targetTableName = getTableName(relation.target());
409
- } catch { /* ignore */ }
410
449
  issues.push({
411
450
  severity: "warning",
412
451
  category: "missing_foreign_key",
413
- table: tableName,
452
+ table: fullTableName,
414
453
  column: fkColName,
415
- message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetTableName}".`,
454
+ message: `Column "${fkColName}" exists but has no FOREIGN KEY constraint referencing "${targetSchemaName === "public" ? targetTableName : `${targetSchemaName}.${targetTableName}`}".`,
416
455
  fix: "Run `rebase db push` or add the constraint manually"
417
456
  });
418
457
  }
@@ -430,9 +469,9 @@ export async function checkCollectionsVsDatabase(
430
469
  issues.push({
431
470
  severity: "error",
432
471
  category: "missing_column",
433
- table: tableName,
472
+ table: fullTableName,
434
473
  column: colName,
435
- message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${tableName}".`,
474
+ message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${fullTableName}".`,
436
475
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
437
476
  });
438
477
  continue;
@@ -450,11 +489,11 @@ export async function checkCollectionsVsDatabase(
450
489
  issues.push({
451
490
  severity: "warning",
452
491
  category: "type_mismatch",
453
- table: tableName,
492
+ table: fullTableName,
454
493
  column: colName,
455
494
  expected: prop.type === "vector" ? "vector" : expectedType,
456
495
  actual: dbCol.udt_name === "vector" ? "vector" : actualType,
457
- message: `Column "${colName}" in table "${tableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
496
+ message: `Column "${colName}" in table "${fullTableName}": expected type "${prop.type === "vector" ? "vector" : expectedType}" but found "${dbCol.udt_name === "vector" ? "vector" : actualType}".`,
458
497
  fix: "Review collection property type or run a migration"
459
498
  });
460
499
  }
@@ -470,7 +509,7 @@ export async function checkCollectionsVsDatabase(
470
509
  issues.push({
471
510
  severity: "warning",
472
511
  category: "missing_enum",
473
- table: tableName,
512
+ table: fullTableName,
474
513
  column: colName,
475
514
  expected: enumName,
476
515
  message: `Enum type "${enumName}" is defined in collection but not found in the database.`,
@@ -492,11 +531,11 @@ export async function checkCollectionsVsDatabase(
492
531
  issues.push({
493
532
  severity: "warning",
494
533
  category: "enum_value_mismatch",
495
- table: tableName,
534
+ table: fullTableName,
496
535
  column: colName,
497
536
  expected: expectedValues.join(", "),
498
537
  actual: dbEnumValues.join(", "),
499
- message: `Enum values for "${colName}" in table "${tableName}" are out of sync (${parts.join("; ")}).`,
538
+ message: `Enum values for "${colName}" in table "${fullTableName}" are out of sync (${parts.join("; ")}).`,
500
539
  fix: "Run `rebase db push` to update the enum"
501
540
  });
502
541
  }
@@ -510,12 +549,14 @@ export async function checkCollectionsVsDatabase(
510
549
  for (const relation of Object.values(resolvedRelations)) {
511
550
  if (relation.cardinality === "many" && relation.direction === "owning" && relation.through) {
512
551
  const junctionTable = relation.through.table;
513
- if (!existingTables.has(junctionTable)) {
552
+ const junctionSchema = collection.schema || "public";
553
+ const fullJunctionTable = junctionSchema === "public" ? junctionTable : `${junctionSchema}.${junctionTable}`;
554
+ if (!existingTables.has(fullJunctionTable)) {
514
555
  issues.push({
515
556
  severity: "error",
516
557
  category: "missing_table",
517
- table: junctionTable,
518
- message: `Junction table "${junctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
558
+ table: fullJunctionTable,
559
+ message: `Junction table "${fullJunctionTable}" for many-to-many relation "${relation.relationName}" is missing.`,
519
560
  fix: "Run `rebase db push` or `rebase db generate && rebase db migrate`"
520
561
  });
521
562
  }
@@ -35,7 +35,12 @@ describe("PostgresBackendDriver", () => {
35
35
 
36
36
  beforeEach(() => {
37
37
  jest.clearAllMocks();
38
- delegate = new PostgresBackendDriver(mockDb, mockRealtimeService);
38
+ const mockRegistry = {
39
+ getCollectionByPath: jest.fn().mockReturnValue({ slug: "test_coll", properties: {} }),
40
+ getCollections: jest.fn().mockReturnValue([]),
41
+ getTable: jest.fn().mockReturnValue({})
42
+ } as any;
43
+ delegate = new PostgresBackendDriver(mockDb, mockRealtimeService, mockRegistry);
39
44
  });
40
45
 
41
46
  it("should initialize correctly", () => {
@@ -661,5 +666,129 @@ status: "new" });
661
666
  executeSqlSpy.mockRestore();
662
667
  });
663
668
  });
669
+
670
+ describe("storageSource in Callbacks", () => {
671
+ it("should inject storageSource: client.storage into contextForCallback in fetchCollection", async () => {
672
+ const mockStorage = { key: "mockStorage" };
673
+ delegate.client = {
674
+ storage: mockStorage
675
+ } as any;
676
+
677
+ const afterReadSpy = jest.fn().mockImplementation(async ({ entity }) => entity);
678
+ const mockCollectionWithCallback = {
679
+ slug: "test_coll",
680
+ callbacks: {
681
+ afterRead: afterReadSpy
682
+ }
683
+ } as any;
684
+
685
+ jest.spyOn(delegate.entityService, "fetchCollection").mockResolvedValueOnce([
686
+ { id: "e1", path: "test_coll", values: {} } as any
687
+ ]);
688
+
689
+ await delegate.fetchCollection({
690
+ path: "test_coll",
691
+ collection: mockCollectionWithCallback
692
+ });
693
+
694
+ expect(afterReadSpy).toHaveBeenCalled();
695
+ const callArgs = afterReadSpy.mock.calls[0][0];
696
+ expect(callArgs.context).toBeDefined();
697
+ expect(callArgs.context.storageSource).toBe(mockStorage);
698
+ });
699
+
700
+ it("should inject storageSource in fetchEntity", async () => {
701
+ const mockStorage = { key: "mockStorage" };
702
+ delegate.client = {
703
+ storage: mockStorage
704
+ } as any;
705
+
706
+ const afterReadSpy = jest.fn().mockImplementation(async ({ entity }) => entity);
707
+ const mockCollectionWithCallback = {
708
+ slug: "test_coll",
709
+ callbacks: {
710
+ afterRead: afterReadSpy
711
+ }
712
+ } as any;
713
+
714
+ jest.spyOn(delegate.entityService, "fetchEntity").mockResolvedValueOnce(
715
+ { id: "e1", path: "test_coll", values: {} } as any
716
+ );
717
+
718
+ await delegate.fetchEntity({
719
+ path: "test_coll",
720
+ entityId: "e1",
721
+ collection: mockCollectionWithCallback
722
+ });
723
+
724
+ expect(afterReadSpy).toHaveBeenCalled();
725
+ const callArgs = afterReadSpy.mock.calls[0][0];
726
+ expect(callArgs.context.storageSource).toBe(mockStorage);
727
+ });
728
+
729
+ it("should inject storageSource in saveEntity beforeSave and afterSave", async () => {
730
+ const mockStorage = { key: "mockStorage" };
731
+ delegate.client = {
732
+ storage: mockStorage
733
+ } as any;
734
+
735
+ const beforeSaveSpy = jest.fn().mockImplementation(async ({ values }) => values);
736
+ const afterSaveSpy = jest.fn();
737
+ const mockCollectionWithCallback = {
738
+ slug: "test_coll",
739
+ callbacks: {
740
+ beforeSave: beforeSaveSpy,
741
+ afterSave: afterSaveSpy
742
+ }
743
+ } as any;
744
+
745
+ jest.spyOn(delegate.entityService, "fetchEntity").mockResolvedValue(undefined);
746
+ jest.spyOn(delegate.entityService, "saveEntity").mockResolvedValueOnce(
747
+ { id: "e1", path: "test_coll", values: { name: "test" } } as any
748
+ );
749
+
750
+ await delegate.saveEntity({
751
+ path: "test_coll",
752
+ entityId: "e1",
753
+ values: { name: "test" },
754
+ collection: mockCollectionWithCallback,
755
+ status: "existing"
756
+ });
757
+
758
+ expect(beforeSaveSpy).toHaveBeenCalled();
759
+ expect(beforeSaveSpy.mock.calls[0][0].context.storageSource).toBe(mockStorage);
760
+ expect(afterSaveSpy).toHaveBeenCalled();
761
+ expect(afterSaveSpy.mock.calls[0][0].context.storageSource).toBe(mockStorage);
762
+ });
763
+
764
+ it("should inject storageSource in deleteEntity beforeDelete and afterDelete", async () => {
765
+ const mockStorage = { key: "mockStorage" };
766
+ delegate.client = {
767
+ storage: mockStorage
768
+ } as any;
769
+
770
+ const beforeDeleteSpy = jest.fn().mockImplementation(async () => true);
771
+ const afterDeleteSpy = jest.fn();
772
+ const mockCollectionWithCallback = {
773
+ slug: "test_coll",
774
+ callbacks: {
775
+ beforeDelete: beforeDeleteSpy,
776
+ afterDelete: afterDeleteSpy
777
+ }
778
+ } as any;
779
+
780
+ jest.spyOn(delegate.entityService, "deleteEntity").mockResolvedValueOnce();
781
+
782
+ await delegate.deleteEntity({
783
+ entity: { id: "e1", path: "test_coll", values: {} } as any,
784
+ collection: mockCollectionWithCallback
785
+ });
786
+
787
+ expect(beforeDeleteSpy).toHaveBeenCalled();
788
+ expect(beforeDeleteSpy.mock.calls[0][0].context.storageSource).toBe(mockStorage);
789
+ expect(afterDeleteSpy).toHaveBeenCalled();
790
+ expect(afterDeleteSpy.mock.calls[0][0].context.storageSource).toBe(mockStorage);
791
+ });
792
+ });
664
793
  });
665
794