@proteinjs/db 1.34.1 → 1.34.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/CHANGELOG.md +24 -0
- package/dist/generated/index.d.ts +0 -4
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +1 -5
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/test/index.d.ts +0 -4
- package/dist/generated/test/index.d.ts.map +1 -1
- package/dist/generated/test/index.js +1 -5
- package/dist/generated/test/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/src/Db.d.ts +12 -1
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +146 -25
- package/dist/src/Db.js.map +1 -1
- package/dist/src/RecordAccessError.d.ts +18 -0
- package/dist/src/RecordAccessError.d.ts.map +1 -0
- package/dist/src/RecordAccessError.js +46 -0
- package/dist/src/RecordAccessError.js.map +1 -0
- package/dist/src/Table.d.ts +28 -1
- package/dist/src/Table.d.ts.map +1 -1
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.d.ts +2 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.d.ts.map +1 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.js +62 -0
- package/dist/test/DbDefaultDriverDuplicateModuleState.test.js.map +1 -0
- package/dist/test/util/tables/cascadeDeleteTestTables.d.ts +15 -115
- package/dist/test/util/tables/cascadeDeleteTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/cascadeDeleteTestTables.js.map +1 -1
- package/dist/test/util/tables/crudTestTables.d.ts +2 -22
- package/dist/test/util/tables/crudTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/crudTestTables.js.map +1 -1
- package/dist/test/util/tables/dynamicReferenceColumnTestTables.d.ts +4 -36
- package/dist/test/util/tables/dynamicReferenceColumnTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/dynamicReferenceColumnTestTables.js.map +1 -1
- package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts +4 -34
- package/dist/test/util/tables/sourceRecordSyncTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/sourceRecordSyncTestTables.js.map +1 -1
- package/dist/test/util/tables/tableManagerTestTables.d.ts +3 -38
- package/dist/test/util/tables/tableManagerTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/tableManagerTestTables.js.map +1 -1
- package/dist/test/util/tables/transactionTestTables.d.ts +2 -22
- package/dist/test/util/tables/transactionTestTables.d.ts.map +1 -1
- package/dist/test/util/tables/transactionTestTables.js.map +1 -1
- package/generated/index.ts +1 -5
- package/generated/test/index.ts +1 -5
- package/index.ts +1 -0
- package/package.json +5 -5
- package/src/Db.ts +81 -6
- package/src/RecordAccessError.ts +22 -0
- package/src/Table.ts +33 -1
- package/test/DbDefaultDriverDuplicateModuleState.test.ts +70 -0
- package/test/util/tables/cascadeDeleteTestTables.ts +15 -15
- package/test/util/tables/crudTestTables.ts +2 -2
- package/test/util/tables/dynamicReferenceColumnTestTables.ts +4 -4
- package/test/util/tables/sourceRecordSyncTestTables.ts +4 -4
- package/test/util/tables/tableManagerTestTables.ts +3 -3
- package/test/util/tables/transactionTestTables.ts +2 -2
- package/test.d.ts +2 -0
- package/test.js +4 -0
- package/tsconfig.json +2 -1
package/index.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.34.
|
|
3
|
+
"version": "1.34.3",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -41,9 +41,9 @@
|
|
|
41
41
|
"test": "jest --passWithNoTests"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@proteinjs/db-query": "^1.7.
|
|
44
|
+
"@proteinjs/db-query": "^1.7.1",
|
|
45
45
|
"@proteinjs/logger": "^1.0.21",
|
|
46
|
-
"@proteinjs/reflection": "^1.1.
|
|
46
|
+
"@proteinjs/reflection": "^1.1.14",
|
|
47
47
|
"@proteinjs/serializer": "^1.1.10",
|
|
48
48
|
"@proteinjs/server-api": "^3.0.11",
|
|
49
49
|
"@proteinjs/service": "^1.5.1",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"uuid": "8.3.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@proteinjs/reflection-build": "^
|
|
56
|
+
"@proteinjs/reflection-build": "^2.0.1",
|
|
57
57
|
"@types/jest": "29.5.5",
|
|
58
58
|
"@types/node": "14.0.27",
|
|
59
59
|
"@types/uuid": "8.3.0",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"ts-jest": "29.1.1",
|
|
67
67
|
"typescript": "5.2.2"
|
|
68
68
|
},
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "d3a98c4cf7140cc2d93e588d89011037b3b8987c"
|
|
70
70
|
}
|
package/src/Db.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DbService, Query, QueryOptions, getDbService } from './services/DbService';
|
|
1
|
+
import { DbService, ObjectQuery, Query, QueryOptions, getDbService } from './services/DbService';
|
|
2
2
|
import { Service } from '@proteinjs/service';
|
|
3
3
|
import { Loadable, SourceRepository } from '@proteinjs/reflection';
|
|
4
4
|
import {
|
|
@@ -41,6 +41,19 @@ export const getDbAsSystem = <R extends Record = Record>() => new Db<R>(undefine
|
|
|
41
41
|
const getEnvVar = (key: string): string | undefined =>
|
|
42
42
|
typeof process !== 'undefined' && process.env ? process.env[key] : undefined;
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The lazily-resolved default driver lives on the global object (window in browser, globalThis
|
|
46
|
+
* elsewhere) rather than on a class static: per-package installs can put multiple live copies of
|
|
47
|
+
* @proteinjs/db in one process (each sibling package's nested node_modules hosts its own copy),
|
|
48
|
+
* and a per-copy static means every copy asks the DefaultDbDriverFactory for its own driver —
|
|
49
|
+
* multiple live db clients per process (two Spanner clients holding gRPC channels open was the
|
|
50
|
+
* 2026-08-14 thought-ui jest-never-exits incident). Anchoring on the global gives every copy the
|
|
51
|
+
* one driver — same pattern as reflection's SourceRepository and this package's ReferenceCache.
|
|
52
|
+
*/
|
|
53
|
+
const DEFAULT_DB_DRIVER_GLOBAL_KEY = '__proteinjs_db_defaultDbDriver';
|
|
54
|
+
|
|
55
|
+
const getGlobal = (): any => (typeof window !== 'undefined' ? window : globalThis);
|
|
56
|
+
|
|
44
57
|
export type DbDriverQueryStatementConfig = ParameterizationConfig & {
|
|
45
58
|
prefixTablesWithDb?: boolean;
|
|
46
59
|
getDriverColumnType?: (tableName: string, columnName: string) => string;
|
|
@@ -79,7 +92,6 @@ export interface DbDriver {
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
export class Db<R extends Record = Record> implements DbService<R> {
|
|
82
|
-
private static defaultDbDriver: DbDriver;
|
|
83
95
|
private dbDriver: DbDriver;
|
|
84
96
|
private getTable: (tableName: string) => Table<any>;
|
|
85
97
|
private logger = new Logger({ name: this.constructor.name, logLevel: getEnvVar('DB_LOG_LEVEL') as any });
|
|
@@ -108,7 +120,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
108
120
|
}
|
|
109
121
|
|
|
110
122
|
static getDefaultDbDriver(): DbDriver {
|
|
111
|
-
if (!
|
|
123
|
+
if (!getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY]) {
|
|
112
124
|
const defaultDbDriverFactory = SourceRepository.get().object<DefaultDbDriverFactory>(
|
|
113
125
|
'@proteinjs/db/DefaultDbDriverFactory'
|
|
114
126
|
);
|
|
@@ -118,10 +130,10 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
118
130
|
);
|
|
119
131
|
}
|
|
120
132
|
|
|
121
|
-
|
|
133
|
+
getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY] = defaultDbDriverFactory.getDbDriver();
|
|
122
134
|
}
|
|
123
135
|
|
|
124
|
-
return
|
|
136
|
+
return getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY];
|
|
125
137
|
}
|
|
126
138
|
|
|
127
139
|
private getDefaultTransactionContextFactory(): DefaultTransactionContextFactory {
|
|
@@ -165,6 +177,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
165
177
|
this.statementConfigFactory.getStatementConfig(config)
|
|
166
178
|
);
|
|
167
179
|
await this.dbDriver.runDml(generateInsert, this.transactionForDriver());
|
|
180
|
+
await this.runColumnAfterInsertHooks(table, recordCopy);
|
|
168
181
|
await this.tableWatcherRunner.runAfterInsertTableWatchers(table, recordCopy as T);
|
|
169
182
|
return recordCopy as T;
|
|
170
183
|
}
|
|
@@ -208,6 +221,12 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
208
221
|
this.statementConfigFactory.getStatementConfig(config)
|
|
209
222
|
);
|
|
210
223
|
const recordUpdateCount = await this.dbDriver.runDml(generateUpdate, this.transactionForDriver());
|
|
224
|
+
if (!this.runAsSystem && recordUpdateCount === 0) {
|
|
225
|
+
const id = this.singleRowIdTarget(record, query);
|
|
226
|
+
if (id !== undefined) {
|
|
227
|
+
await this.runZeroRowFilteredWriteHooks(table, id, 'write');
|
|
228
|
+
}
|
|
229
|
+
}
|
|
211
230
|
await this.tableWatcherRunner.runAfterUpdateTableWatchers(table, recordUpdateCount, recordCopy, qb);
|
|
212
231
|
return recordUpdateCount;
|
|
213
232
|
}
|
|
@@ -242,6 +261,13 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
242
261
|
qb.condition({ field: 'id', operator: '=', value: update.recordId as T[keyof T] });
|
|
243
262
|
const rows = await this._query(table, qb);
|
|
244
263
|
if (rows.length === 0) {
|
|
264
|
+
// Same id-targeted single-row refusal path as `update`/`delete`: 0 rows for a non-system
|
|
265
|
+
// caller can mean "row filtered out because the caller lacks write" — surface that as a
|
|
266
|
+
// typed refusal rather than a silent 0 (an existence-honest no-op still returns 0, since the
|
|
267
|
+
// hook only throws when the row exists AND the caller lacks write on its permission source).
|
|
268
|
+
if (!this.runAsSystem && typeof update.recordId === 'string') {
|
|
269
|
+
await this.runZeroRowFilteredWriteHooks(table, update.recordId, 'write');
|
|
270
|
+
}
|
|
245
271
|
return 0;
|
|
246
272
|
}
|
|
247
273
|
|
|
@@ -286,6 +312,11 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
286
312
|
qb.condition({ field: 'id', operator: '=', value: record.id as T[keyof T] });
|
|
287
313
|
const rows = await this._query(table, qb);
|
|
288
314
|
if (rows.length === 0) {
|
|
315
|
+
// See `updateArrayMembership`: fire the id-targeted zero-row refusal so a capability-denied
|
|
316
|
+
// preserving write surfaces a typed error instead of a silent 0.
|
|
317
|
+
if (!this.runAsSystem && typeof record.id === 'string') {
|
|
318
|
+
await this.runZeroRowFilteredWriteHooks(table, record.id, 'write');
|
|
319
|
+
}
|
|
289
320
|
return 0;
|
|
290
321
|
}
|
|
291
322
|
|
|
@@ -311,6 +342,12 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
311
342
|
await this.addColumnQueries(table, qb, 'delete');
|
|
312
343
|
const recordsToDelete = await this._query(table, qb);
|
|
313
344
|
if (recordsToDelete.length == 0) {
|
|
345
|
+
if (!this.runAsSystem) {
|
|
346
|
+
const id = this.singleRowIdTarget(undefined, query);
|
|
347
|
+
if (id !== undefined) {
|
|
348
|
+
await this.runZeroRowFilteredWriteHooks(table, id, 'delete');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
314
351
|
return 0;
|
|
315
352
|
}
|
|
316
353
|
|
|
@@ -651,7 +688,45 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
651
688
|
for (const columnPropertyName in table.columns) {
|
|
652
689
|
const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
|
|
653
690
|
if (column.options?.onBeforeInsert) {
|
|
654
|
-
await column.options.onBeforeInsert(record, this.runAsSystem);
|
|
691
|
+
await column.options.onBeforeInsert(table, record, this.runAsSystem);
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Post-DML mirror of {@link addColumnInsertHooks} — see `ColumnOptions.onAfterInsert`. */
|
|
697
|
+
private async runColumnAfterInsertHooks(table: Table<any>, record: any) {
|
|
698
|
+
for (const columnPropertyName in table.columns) {
|
|
699
|
+
const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
|
|
700
|
+
if (column.options?.onAfterInsert) {
|
|
701
|
+
await column.options.onAfterInsert(table, record, this.runAsSystem);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* The single-row id a filtered write targets, or undefined when the write is not a single-row
|
|
708
|
+
* id target. Only these qualify for the `onZeroRowFilteredWrite` hook, so a multi-row filtered
|
|
709
|
+
* write that matches nothing is never mistaken for a capability denial. An arbitrary
|
|
710
|
+
* `QueryBuilder` is deliberately NOT introspected (returns undefined) — the id-target paths that
|
|
711
|
+
* matter (`update(table, { id, ... })` and `delete(table, { id })`) come through `record.id` or
|
|
712
|
+
* an `ObjectQuery`.
|
|
713
|
+
*/
|
|
714
|
+
private singleRowIdTarget<T extends R>(record?: Partial<T>, query?: Query<T>): string | undefined {
|
|
715
|
+
if (query === undefined) {
|
|
716
|
+
return typeof record?.id === 'string' ? record.id : undefined;
|
|
717
|
+
}
|
|
718
|
+
if (isInstanceOf(query, QueryBuilder)) {
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
const objectQuery = query as ObjectQuery<T>;
|
|
722
|
+
return typeof objectQuery.id === 'string' ? objectQuery.id : undefined;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
private async runZeroRowFilteredWriteHooks(table: Table<any>, id: string, operation: 'write' | 'delete') {
|
|
726
|
+
for (const columnPropertyName in table.columns) {
|
|
727
|
+
const column = (table.columns as any)[columnPropertyName] as Column<any, any>;
|
|
728
|
+
if (column.options?.onZeroRowFilteredWrite) {
|
|
729
|
+
await column.options.onZeroRowFilteredWrite(table, id, operation, this.runAsSystem);
|
|
655
730
|
}
|
|
656
731
|
}
|
|
657
732
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A ROW-LEVEL capability denial (distinct from `TableAuthError`, which denies at the table door).
|
|
3
|
+
*
|
|
4
|
+
* Raised when a caller passes the table's door auth but lacks the per-row capability a
|
|
5
|
+
* capability-scoped column enforces (e.g. a `SharedRecord`'s permission grants): inserting into a
|
|
6
|
+
* permission scope the caller cannot write, or an id-targeted single-row write that matched zero
|
|
7
|
+
* rows purely because the caller's grant level is insufficient. The point is to turn what used to
|
|
8
|
+
* be a SILENT zero-row result — which a caller (and an agent tool acting for them) cannot tell
|
|
9
|
+
* apart from a genuine no-op — into a signal a client can surface as "you don't have access".
|
|
10
|
+
*
|
|
11
|
+
* Name-tagged rather than relying on `instanceof`: the prototype chain is unreliable across
|
|
12
|
+
* package compile targets (same reason `TableAuthError` / `ServiceError` check `name`).
|
|
13
|
+
*/
|
|
14
|
+
export class RecordAccessError extends Error {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'RecordAccessError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const isRecordAccessError = (error: unknown): error is RecordAccessError =>
|
|
22
|
+
!!error && typeof error === 'object' && (error as { name?: string }).name === 'RecordAccessError';
|
package/src/Table.ts
CHANGED
|
@@ -169,7 +169,39 @@ export type ColumnOptions = {
|
|
|
169
169
|
immutable?: boolean | ((runAsSystem: boolean) => boolean);
|
|
170
170
|
/** Add conditions to query; called on every query of this table */
|
|
171
171
|
addToQuery?: (qb: QueryBuilder, runAsSystem: boolean, operation: 'read' | 'write' | 'delete') => Promise<void>;
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Called before the row's insert DML runs, after defaults and before-insert table watchers.
|
|
174
|
+
* Runs where inserts execute (`Db.insert` — the server; a browser only ever proxies or queues),
|
|
175
|
+
* so guards here hold regardless of where column DEFAULTS were applied (the client `Transaction`
|
|
176
|
+
* path applies them in the browser). Throw to refuse the insert.
|
|
177
|
+
*/
|
|
178
|
+
onBeforeInsert?: (table: Table<any>, insertObj: any & Record, runAsSystem: boolean) => Promise<void>;
|
|
179
|
+
/**
|
|
180
|
+
* Called after the row's insert DML SUCCEEDS, before after-insert table watchers. The seam for
|
|
181
|
+
* side-effect writes that are part of the row's birth (e.g. `SharedRecord`'s platform-conferred
|
|
182
|
+
* owner grant): running server-side keeps column defaults pure enough for driverless client
|
|
183
|
+
* contexts, and running after the DML means a failed insert (duplicate id, refused guard) can
|
|
184
|
+
* never leave the side effect behind.
|
|
185
|
+
*/
|
|
186
|
+
onAfterInsert?: (table: Table<any>, insertObj: any & Record, runAsSystem: boolean) => Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* Called after an id-targeted SINGLE-ROW filtered write (update or delete) on this table matched
|
|
189
|
+
* ZERO rows in a NON-system context. A column that narrows row visibility by capability (e.g.
|
|
190
|
+
* `SharedRecord`'s permission subquery) implements this to turn a silent 0 — which the caller
|
|
191
|
+
* cannot distinguish from a genuine capability denial — into a typed error. Return normally to
|
|
192
|
+
* leave the 0 as a legitimate no-op (row genuinely absent, or value unchanged for a caller who
|
|
193
|
+
* DOES hold the capability); throw (a `RecordAccessError`) to surface the refusal.
|
|
194
|
+
*
|
|
195
|
+
* Scope is deliberately narrow: only single-row id targets fire it, so a legitimate multi-row
|
|
196
|
+
* filtered write that matches nothing is never mis-flagged, and system-context maintenance
|
|
197
|
+
* writes never reach it.
|
|
198
|
+
*/
|
|
199
|
+
onZeroRowFilteredWrite?: (
|
|
200
|
+
table: Table<any>,
|
|
201
|
+
id: string,
|
|
202
|
+
operation: 'write' | 'delete',
|
|
203
|
+
runAsSystem: boolean
|
|
204
|
+
) => Promise<void>;
|
|
173
205
|
ui?: {
|
|
174
206
|
hidden?: boolean;
|
|
175
207
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duplicate-module-instance guard for the default db driver (task #36).
|
|
3
|
+
*
|
|
4
|
+
* Per-package installs can put two live copies of @proteinjs/db in one process (each sibling
|
|
5
|
+
* package's nested node_modules hosts its own registry copy). `Db.defaultDbDriver` as a class
|
|
6
|
+
* static then splits per copy: every copy that lazily initializes asks the (globally converged,
|
|
7
|
+
* reflection-routed) DefaultDbDriverFactory for a driver, and the factory mints a fresh one per
|
|
8
|
+
* call — two live Spanner clients holding two gRPC channel sets. That is the 2026-08-14
|
|
9
|
+
* thought-ui incident: jest finished green and then never exited, the second universe's Spanner
|
|
10
|
+
* client keeping the process alive.
|
|
11
|
+
*
|
|
12
|
+
* `jest.isolateModules` reproduces the split semantics exactly: each isolated registry gets its
|
|
13
|
+
* own module instance of Db with its own statics, the same way two nested install paths do. The
|
|
14
|
+
* guard asserts every live copy resolves the SAME driver instance and the factory is consulted
|
|
15
|
+
* once — which only holds when the resolved driver is anchored on the process global, not on a
|
|
16
|
+
* per-copy class static.
|
|
17
|
+
*/
|
|
18
|
+
import { SourceRepository } from '@proteinjs/reflection';
|
|
19
|
+
|
|
20
|
+
type DbModule = typeof import('../src/Db');
|
|
21
|
+
|
|
22
|
+
const loadIsolatedCopy = (): DbModule => {
|
|
23
|
+
let copy: DbModule | undefined;
|
|
24
|
+
jest.isolateModules(() => {
|
|
25
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
26
|
+
copy = require('../src/Db');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!copy) {
|
|
30
|
+
throw new Error('Failed to load an isolated copy of Db');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return copy;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
describe('Db default driver under duplicate module instances', () => {
|
|
37
|
+
let driversMinted: number;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
driversMinted = 0;
|
|
41
|
+
// House test pattern: seed the (globalThis-anchored, therefore converged) SourceRepository
|
|
42
|
+
// object cache directly. The factory mints a fresh driver object per call, mirroring real
|
|
43
|
+
// DefaultDbDriverFactory implementations (e.g. the app's SpannerDriver factory).
|
|
44
|
+
(SourceRepository.get() as any).objectCache['@proteinjs/db/DefaultDbDriverFactory'] = [
|
|
45
|
+
{
|
|
46
|
+
getDbDriver: () => {
|
|
47
|
+
driversMinted++;
|
|
48
|
+
return { minted: driversMinted };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
delete (SourceRepository.get() as any).objectCache['@proteinjs/db/DefaultDbDriverFactory'];
|
|
56
|
+
delete (globalThis as any).__proteinjs_db_defaultDbDriver;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('every live copy of Db resolves the same default driver instance', () => {
|
|
60
|
+
const copyA = loadIsolatedCopy();
|
|
61
|
+
const copyB = loadIsolatedCopy();
|
|
62
|
+
expect(copyA).not.toBe(copyB); // two distinct module instances, as under per-package installs
|
|
63
|
+
|
|
64
|
+
const driverA = copyA.Db.getDefaultDbDriver();
|
|
65
|
+
const driverB = copyB.Db.getDefaultDbDriver();
|
|
66
|
+
|
|
67
|
+
expect(driverB).toBe(driverA); // one process, one driver — one live client
|
|
68
|
+
expect(driversMinted).toBe(1); // the factory is consulted exactly once per process
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -118,13 +118,13 @@ const MISSION_TABLE = 'db_test_cd_missions';
|
|
|
118
118
|
// Cascade: ReferenceColumn
|
|
119
119
|
export class MemberRefTable extends Table<MemberRef> {
|
|
120
120
|
name = MEMBER_REF_TABLE;
|
|
121
|
-
columns = withRecordColumns<MemberRef>({
|
|
121
|
+
columns: Table<MemberRef>['columns'] = withRecordColumns<MemberRef>({
|
|
122
122
|
name: new StringColumn('name'),
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
125
|
export class GroupRefTable extends Table<GroupRef> {
|
|
126
126
|
name = GROUP_REF_TABLE;
|
|
127
|
-
columns = withRecordColumns<GroupRef>({
|
|
127
|
+
columns: Table<GroupRef>['columns'] = withRecordColumns<GroupRef>({
|
|
128
128
|
groupName: new StringColumn('group_name'),
|
|
129
129
|
memberRef: new ReferenceColumn<MemberRef>(
|
|
130
130
|
'member_id',
|
|
@@ -137,13 +137,13 @@ export class GroupRefTable extends Table<GroupRef> {
|
|
|
137
137
|
// Cascade: ReferenceArrayColumn
|
|
138
138
|
export class MemberArrTable extends Table<MemberArr> {
|
|
139
139
|
name = MEMBER_ARR_TABLE;
|
|
140
|
-
columns = withRecordColumns<MemberArr>({
|
|
140
|
+
columns: Table<MemberArr>['columns'] = withRecordColumns<MemberArr>({
|
|
141
141
|
name: new StringColumn('name'),
|
|
142
142
|
});
|
|
143
143
|
}
|
|
144
144
|
export class GroupArrTable extends Table<GroupArr> {
|
|
145
145
|
name = GROUP_ARR_TABLE;
|
|
146
|
-
columns = withRecordColumns<GroupArr>({
|
|
146
|
+
columns: Table<GroupArr>['columns'] = withRecordColumns<GroupArr>({
|
|
147
147
|
groupName: new StringColumn('group_name'),
|
|
148
148
|
memberRefs: new ReferenceArrayColumn<MemberArr>(
|
|
149
149
|
'member_ids',
|
|
@@ -156,13 +156,13 @@ export class GroupArrTable extends Table<GroupArr> {
|
|
|
156
156
|
// Cascade: DynamicReferenceColumn
|
|
157
157
|
export class MemberDynTable extends Table<MemberDyn> {
|
|
158
158
|
name = MEMBER_DYN_TABLE;
|
|
159
|
-
columns = withRecordColumns<MemberDyn>({
|
|
159
|
+
columns: Table<MemberDyn>['columns'] = withRecordColumns<MemberDyn>({
|
|
160
160
|
name: new StringColumn('name'),
|
|
161
161
|
});
|
|
162
162
|
}
|
|
163
163
|
export class GroupDynTable extends Table<GroupDyn> {
|
|
164
164
|
name = GROUP_DYN_TABLE;
|
|
165
|
-
columns = withRecordColumns<GroupDyn>({
|
|
165
|
+
columns: Table<GroupDyn>['columns'] = withRecordColumns<GroupDyn>({
|
|
166
166
|
groupName: new StringColumn('group_name'),
|
|
167
167
|
memberDynTableName: new DynamicReferenceTableNameColumn('member_dyn_table_name', 'member_dyn_ref'),
|
|
168
168
|
memberDynRef: new DynamicReferenceColumn<MemberDyn>(
|
|
@@ -176,13 +176,13 @@ export class GroupDynTable extends Table<GroupDyn> {
|
|
|
176
176
|
// Reverse: ReferenceColumn
|
|
177
177
|
export class PostTable extends Table<Post> {
|
|
178
178
|
name = POST_TABLE;
|
|
179
|
-
columns = withRecordColumns<Post>({
|
|
179
|
+
columns: Table<Post>['columns'] = withRecordColumns<Post>({
|
|
180
180
|
title: new StringColumn('title'),
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
183
|
export class CommentTable extends Table<Comment> {
|
|
184
184
|
name = COMMENT_TABLE;
|
|
185
|
-
columns = withRecordColumns<Comment>({
|
|
185
|
+
columns: Table<Comment>['columns'] = withRecordColumns<Comment>({
|
|
186
186
|
text: new StringColumn('text'),
|
|
187
187
|
postRef: new ReferenceColumn<Post>(
|
|
188
188
|
'post_id',
|
|
@@ -196,13 +196,13 @@ export class CommentTable extends Table<Comment> {
|
|
|
196
196
|
// Reverse: ReferenceArrayColumn
|
|
197
197
|
export class MemberArrRevTable extends Table<MemberArrRev> {
|
|
198
198
|
name = MEMBER_ARR_REV_TABLE;
|
|
199
|
-
columns = withRecordColumns<MemberArrRev>({
|
|
199
|
+
columns: Table<MemberArrRev>['columns'] = withRecordColumns<MemberArrRev>({
|
|
200
200
|
name: new StringColumn('name'),
|
|
201
201
|
});
|
|
202
202
|
}
|
|
203
203
|
export class GroupArrRevTable extends Table<GroupArrRev> {
|
|
204
204
|
name = GROUP_ARR_REV_TABLE;
|
|
205
|
-
columns = withRecordColumns<GroupArrRev>({
|
|
205
|
+
columns: Table<GroupArrRev>['columns'] = withRecordColumns<GroupArrRev>({
|
|
206
206
|
groupName: new StringColumn('group_name'),
|
|
207
207
|
memberRefs: new ReferenceArrayColumn<MemberArrRev>(
|
|
208
208
|
'member_ids',
|
|
@@ -216,13 +216,13 @@ export class GroupArrRevTable extends Table<GroupArrRev> {
|
|
|
216
216
|
// Reverse: DynamicReferenceColumn
|
|
217
217
|
export class WorkerTable extends Table<Worker> {
|
|
218
218
|
name = WORKER_TABLE;
|
|
219
|
-
columns = withRecordColumns<Worker>({
|
|
219
|
+
columns: Table<Worker>['columns'] = withRecordColumns<Worker>({
|
|
220
220
|
name: new StringColumn('name'),
|
|
221
221
|
});
|
|
222
222
|
}
|
|
223
223
|
export class TaskTable extends Table<Task> {
|
|
224
224
|
name = TASK_TABLE;
|
|
225
|
-
columns = withRecordColumns<Task>({
|
|
225
|
+
columns: Table<Task>['columns'] = withRecordColumns<Task>({
|
|
226
226
|
title: new StringColumn('title'),
|
|
227
227
|
assigneeTableName: new DynamicReferenceTableNameColumn('assignee_table_name', 'assignee_ref'),
|
|
228
228
|
assigneeRef: new DynamicReferenceColumn<Worker>(
|
|
@@ -237,19 +237,19 @@ export class TaskTable extends Table<Task> {
|
|
|
237
237
|
// Reverse: DynamicReferenceColumn across multiple target tables
|
|
238
238
|
export class PilotTable extends Table<Pilot> {
|
|
239
239
|
name = PILOT_TABLE;
|
|
240
|
-
columns = withRecordColumns<Pilot>({
|
|
240
|
+
columns: Table<Pilot>['columns'] = withRecordColumns<Pilot>({
|
|
241
241
|
name: new StringColumn('name'),
|
|
242
242
|
});
|
|
243
243
|
}
|
|
244
244
|
export class RobotTable extends Table<Robot> {
|
|
245
245
|
name = ROBOT_TABLE;
|
|
246
|
-
columns = withRecordColumns<Robot>({
|
|
246
|
+
columns: Table<Robot>['columns'] = withRecordColumns<Robot>({
|
|
247
247
|
name: new StringColumn('name'),
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
250
|
export class MissionTable extends Table<Mission> {
|
|
251
251
|
name = MISSION_TABLE;
|
|
252
|
-
columns = withRecordColumns<Mission>({
|
|
252
|
+
columns: Table<Mission>['columns'] = withRecordColumns<Mission>({
|
|
253
253
|
title: new StringColumn('title'),
|
|
254
254
|
assigneeTableName: new DynamicReferenceTableNameColumn('assignee_table_name', 'assignee_ref'),
|
|
255
255
|
assigneeRef: new DynamicReferenceColumn<Pilot | Robot>(
|
|
@@ -11,7 +11,7 @@ export interface Employee extends Record {
|
|
|
11
11
|
|
|
12
12
|
export class EmployeeTestTable extends Table<Employee> {
|
|
13
13
|
name = 'db_test_employee';
|
|
14
|
-
columns = withRecordColumns<Employee>({
|
|
14
|
+
columns: Table<Employee>['columns'] = withRecordColumns<Employee>({
|
|
15
15
|
name: new StringColumn('name'),
|
|
16
16
|
department: new StringColumn('department'),
|
|
17
17
|
isRemote: new BooleanColumn('is_remote'),
|
|
@@ -30,7 +30,7 @@ export interface ReservedWordTest extends Record {
|
|
|
30
30
|
|
|
31
31
|
export class ReservedWordTestTable extends Table<ReservedWordTest> {
|
|
32
32
|
name = 'db_test_reserved_word';
|
|
33
|
-
columns = withRecordColumns<ReservedWordTest>({
|
|
33
|
+
columns: Table<ReservedWordTest>['columns'] = withRecordColumns<ReservedWordTest>({
|
|
34
34
|
name: new StringColumn('name'),
|
|
35
35
|
order: new StringColumn('order'),
|
|
36
36
|
select: new StringColumn('select'),
|
|
@@ -34,7 +34,7 @@ export interface ProjectManager extends Record {
|
|
|
34
34
|
|
|
35
35
|
export class ProjectAssignmentTable extends Table<ProjectAssignment> {
|
|
36
36
|
name = 'db_test_dr_project_assignments';
|
|
37
|
-
columns = withRecordColumns<ProjectAssignment>({
|
|
37
|
+
columns: Table<ProjectAssignment>['columns'] = withRecordColumns<ProjectAssignment>({
|
|
38
38
|
projectName: new StringColumn('project_name'),
|
|
39
39
|
employeeTableName: new DynamicReferenceTableNameColumn('employee_table_name', 'employee_ref'),
|
|
40
40
|
employeeRef: new DynamicReferenceColumn<Engineer | Designer | ProjectManager>(
|
|
@@ -57,7 +57,7 @@ export class ProjectAssignmentTable extends Table<ProjectAssignment> {
|
|
|
57
57
|
|
|
58
58
|
export class EngineerTable extends Table<Engineer> {
|
|
59
59
|
name = 'db_test_dr_engineers';
|
|
60
|
-
columns = withRecordColumns<Engineer>({
|
|
60
|
+
columns: Table<Engineer>['columns'] = withRecordColumns<Engineer>({
|
|
61
61
|
name: new StringColumn('name'),
|
|
62
62
|
yearsOfExperience: new StringColumn('years_of_experience'),
|
|
63
63
|
});
|
|
@@ -65,7 +65,7 @@ export class EngineerTable extends Table<Engineer> {
|
|
|
65
65
|
|
|
66
66
|
export class DesignerTable extends Table<Designer> {
|
|
67
67
|
name = 'db_test_dr_designers';
|
|
68
|
-
columns = withRecordColumns<Designer>({
|
|
68
|
+
columns: Table<Designer>['columns'] = withRecordColumns<Designer>({
|
|
69
69
|
name: new StringColumn('name'),
|
|
70
70
|
specialization: new StringColumn('specialization'),
|
|
71
71
|
});
|
|
@@ -73,7 +73,7 @@ export class DesignerTable extends Table<Designer> {
|
|
|
73
73
|
|
|
74
74
|
export class ProjectManagerTable extends Table<ProjectManager> {
|
|
75
75
|
name = 'db_test_dr_project_managers';
|
|
76
|
-
columns = withRecordColumns<ProjectManager>({
|
|
76
|
+
columns: Table<ProjectManager>['columns'] = withRecordColumns<ProjectManager>({
|
|
77
77
|
name: new StringColumn('name'),
|
|
78
78
|
certificate: new StringColumn('certificate'),
|
|
79
79
|
});
|
|
@@ -30,7 +30,7 @@ export interface SyncMachineAccount extends SourceRecord {
|
|
|
30
30
|
/** The machine-account shape: natural-key adoption by email, removed rows flagged not deleted. */
|
|
31
31
|
export class SyncMachineAccountTable extends Table<SyncMachineAccount> {
|
|
32
32
|
name = 'db_test_sync_machine_account';
|
|
33
|
-
columns = withSourceRecordColumns<SyncMachineAccount>({
|
|
33
|
+
columns: Table<SyncMachineAccount>['columns'] = withSourceRecordColumns<SyncMachineAccount>({
|
|
34
34
|
email: new StringColumn('email', {
|
|
35
35
|
unique: { unique: true, indexName: 'db_test_sync_machine_account_email_unique' },
|
|
36
36
|
}),
|
|
@@ -52,7 +52,7 @@ export interface SyncDefaultPolicyRecord extends SourceRecord {
|
|
|
52
52
|
/** Default options: id-keyed, removed source rows are deleted (today's behavior). */
|
|
53
53
|
export class SyncDefaultPolicyTable extends Table<SyncDefaultPolicyRecord> {
|
|
54
54
|
name = 'db_test_sync_default_policy';
|
|
55
|
-
columns = withSourceRecordColumns<SyncDefaultPolicyRecord>({
|
|
55
|
+
columns: Table<SyncDefaultPolicyRecord>['columns'] = withSourceRecordColumns<SyncDefaultPolicyRecord>({
|
|
56
56
|
email: new StringColumn('email'),
|
|
57
57
|
displayName: new StringColumn('display_name'),
|
|
58
58
|
});
|
|
@@ -65,7 +65,7 @@ export interface DupePreflightRecord extends DbRecord {
|
|
|
65
65
|
/** Generation 1: no uniqueness — the table under which duplicate rows accumulate. */
|
|
66
66
|
export class DupePreflightTable extends Table<DupePreflightRecord> {
|
|
67
67
|
name = 'db_test_dupe_preflight';
|
|
68
|
-
columns = withRecordColumns<DupePreflightRecord>({
|
|
68
|
+
columns: Table<DupePreflightRecord>['columns'] = withRecordColumns<DupePreflightRecord>({
|
|
69
69
|
email: new StringColumn('email'),
|
|
70
70
|
});
|
|
71
71
|
}
|
|
@@ -73,7 +73,7 @@ export class DupePreflightTable extends Table<DupePreflightRecord> {
|
|
|
73
73
|
/** Generation 2: same table, email now unique — loading it runs the alter-path preflight. */
|
|
74
74
|
export class DupePreflightUniqueEmailTable extends Table<DupePreflightRecord> {
|
|
75
75
|
name = 'db_test_dupe_preflight';
|
|
76
|
-
columns = withRecordColumns<DupePreflightRecord>({
|
|
76
|
+
columns: Table<DupePreflightRecord>['columns'] = withRecordColumns<DupePreflightRecord>({
|
|
77
77
|
email: new StringColumn('email', { unique: { unique: true, indexName: 'db_test_dupe_preflight_email_unique' } }),
|
|
78
78
|
});
|
|
79
79
|
}
|
|
@@ -22,7 +22,7 @@ export interface User extends Record {
|
|
|
22
22
|
|
|
23
23
|
export class UserTestTable extends Table<User> {
|
|
24
24
|
name = 'db_test_user';
|
|
25
|
-
columns = withRecordColumns<User>({
|
|
25
|
+
columns: Table<User>['columns'] = withRecordColumns<User>({
|
|
26
26
|
name: new StringColumn('name'),
|
|
27
27
|
email: new StringColumn('email'),
|
|
28
28
|
active: new BooleanColumn('active'),
|
|
@@ -41,7 +41,7 @@ export interface MappedIndexUser extends Record {
|
|
|
41
41
|
|
|
42
42
|
export class MappedIndexUserTable extends Table<MappedIndexUser> {
|
|
43
43
|
name = 'db_test_tm_mapped_index_user';
|
|
44
|
-
columns = withRecordColumns<MappedIndexUser>({
|
|
44
|
+
columns: Table<MappedIndexUser>['columns'] = withRecordColumns<MappedIndexUser>({
|
|
45
45
|
emailAddress: new StringColumn('email_address'),
|
|
46
46
|
accountStatus: new StringColumn('account_status'),
|
|
47
47
|
createdOn: new DateColumn('created_on'),
|
|
@@ -75,7 +75,7 @@ export interface ColumnTypes extends Record {
|
|
|
75
75
|
|
|
76
76
|
export class ColumnTypesTable extends Table<ColumnTypes> {
|
|
77
77
|
name = 'db_test_tm_column_types';
|
|
78
|
-
columns = withRecordColumns<ColumnTypes>({
|
|
78
|
+
columns: Table<ColumnTypes>['columns'] = withRecordColumns<ColumnTypes>({
|
|
79
79
|
integer: new IntegerColumn('integer', { nullable: true }),
|
|
80
80
|
bigInteger: new IntegerColumn('big_integer', { nullable: false }, true),
|
|
81
81
|
string: new StringColumn('string', { references: { table: 'db_test_user' } }),
|
|
@@ -11,7 +11,7 @@ export interface TransactionEmployee extends Record {
|
|
|
11
11
|
|
|
12
12
|
export class TransactionEmployeeTestTable extends Table<TransactionEmployee> {
|
|
13
13
|
name = 'db_test_transaction_employee';
|
|
14
|
-
columns = withRecordColumns<TransactionEmployee>({
|
|
14
|
+
columns: Table<TransactionEmployee>['columns'] = withRecordColumns<TransactionEmployee>({
|
|
15
15
|
name: new StringColumn('name'),
|
|
16
16
|
department: new StringColumn('department'),
|
|
17
17
|
isRemote: new BooleanColumn('is_remote'),
|
|
@@ -30,7 +30,7 @@ export interface TransactionReservedWordTest extends Record {
|
|
|
30
30
|
|
|
31
31
|
export class TransactionReservedWordTestTable extends Table<TransactionReservedWordTest> {
|
|
32
32
|
name = 'db_test_transaction_reserved_word';
|
|
33
|
-
columns = withRecordColumns<TransactionReservedWordTest>({
|
|
33
|
+
columns: Table<TransactionReservedWordTest>['columns'] = withRecordColumns<TransactionReservedWordTest>({
|
|
34
34
|
name: new StringColumn('name'),
|
|
35
35
|
order: new StringColumn('order'),
|
|
36
36
|
select: new StringColumn('select'),
|
package/test.d.ts
ADDED
package/test.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Node10/classic-resolution stub for the `./test` subpath, owned by reflection-build.
|
|
2
|
+
// Consumers that bypass the exports map (node10 resolution, tsconfig paths mappings) resolve
|
|
3
|
+
// this file path first, so every install shape loads the built dist — never sources.
|
|
4
|
+
module.exports = require('./dist/generated/test/index.js');
|