@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.
- package/dist/common/src/data/query_builder.d.ts +51 -0
- package/dist/common/src/index.d.ts +1 -0
- package/dist/index.es.js +280 -131
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +280 -131
- package/dist/index.umd.js.map +1 -1
- package/dist/types/src/controllers/data.d.ts +21 -0
- package/package.json +7 -6
- package/src/PostgresBackendDriver.ts +9 -5
- package/src/cli.ts +5 -0
- package/src/schema/default-collections.ts +1 -0
- package/src/schema/doctor.ts +82 -41
- package/test/postgresDataDriver.test.ts +130 -1
|
@@ -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.
|
|
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/
|
|
71
|
-
"@rebasepro/
|
|
72
|
-
"@rebasepro/types": "0.2.
|
|
73
|
-
"@rebasepro/
|
|
74
|
-
"@rebasepro/
|
|
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
|
|
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);
|
package/src/schema/doctor.ts
CHANGED
|
@@ -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
|
|
293
|
-
const tablesResult = await pool.query<{ table_name: string }>(
|
|
294
|
-
|
|
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) =>
|
|
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 =
|
|
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
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
353
|
-
|
|
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(
|
|
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(
|
|
396
|
+
if (!existingTables.has(fullTableName)) {
|
|
367
397
|
issues.push({
|
|
368
398
|
severity: "error",
|
|
369
399
|
category: "missing_table",
|
|
370
|
-
table:
|
|
371
|
-
message: `Table "${
|
|
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(
|
|
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:
|
|
425
|
+
table: fullTableName,
|
|
396
426
|
column: fkColName,
|
|
397
|
-
message: `Foreign key column "${fkColName}" for relation "${propName}" is missing from table "${
|
|
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(
|
|
404
|
-
|
|
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:
|
|
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:
|
|
472
|
+
table: fullTableName,
|
|
434
473
|
column: colName,
|
|
435
|
-
message: `Column "${colName}" is defined in collection "${collection.slug}" but missing from table "${
|
|
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:
|
|
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 "${
|
|
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:
|
|
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:
|
|
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 "${
|
|
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
|
-
|
|
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:
|
|
518
|
-
message: `Junction table "${
|
|
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
|
-
|
|
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
|
|