@proteinjs/db 1.56.0 → 1.57.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@proteinjs/db",
3
- "version": "1.56.0",
3
+ "version": "1.57.0",
4
4
  "main": "./dist/generated/index.js",
5
5
  "types": "./dist/generated/index.d.ts",
6
6
  "exports": {
@@ -66,5 +66,5 @@
66
66
  "ts-jest": "29.1.1",
67
67
  "typescript": "5.2.2"
68
68
  },
69
- "gitHead": "469b64193ef5d4fc0faa74ebe1f6c2af04dfd6cb"
69
+ "gitHead": "c33ab303e81e366f73bb2cfac13f818567c37306"
70
70
  }
package/src/Db.ts CHANGED
@@ -167,6 +167,16 @@ export class Db<R extends Record = Record> implements DbService<R> {
167
167
  : this.getDefaultTransactionContextFactory();
168
168
  }
169
169
 
170
+ /**
171
+ * Whether this Db writes as the SYSTEM (`getDbAsSystem()`: no table-auth gate, no scope) rather
172
+ * than as the session's user. The Db a write hands its hooks ({@link newHookDb}) answers for the
173
+ * invoking write, so a watcher that must tell a user's write from a system one (archive on a
174
+ * user's delete; stand aside on a purge) reads the write's mode here instead of the session.
175
+ */
176
+ isSystem(): boolean {
177
+ return this.runAsSystem;
178
+ }
179
+
170
180
  static getDefaultDbDriver(): DbDriver {
171
181
  if (!getGlobal()[DEFAULT_DB_DRIVER_GLOBAL_KEY]) {
172
182
  const defaultDbDriverFactory = SourceRepository.get().object<DefaultDbDriverFactory>(
@@ -0,0 +1,173 @@
1
+ import { SourceRepository } from '@proteinjs/reflection';
2
+ import { Statement } from '@proteinjs/db-query';
3
+ import { Db, DbDriver, DbDriverDmlStatementConfig, getDb, getDbAsSystem } from '../src/Db';
4
+ import { Table } from '../src/Table';
5
+ import { Record, withRecordColumns } from '../src/Record';
6
+ import { StringColumn } from '../src/Columns';
7
+ import { TableWatcher } from '../src/TableWatcher';
8
+ import { TableWatcherRunner } from '../src/TableWatcherRunner';
9
+ import { DefaultTransactionContextFactory, TransactionContextData } from '../src/transaction/TransactionContextFactory';
10
+
11
+ /**
12
+ * `Db.isSystem()` — the public read of a write's MODE. A hook that must tell a user's write from
13
+ * the system's (a watcher that archives on a user's delete and stands aside on a purge) reads it
14
+ * off the Db the write hands it, instead of guessing from the session. Pinned both ways on the
15
+ * public factories, and — the pin that matters — on the Db the hook runner hands each watcher:
16
+ * that Db answers for the INVOKING write, whatever the session holds.
17
+ */
18
+
19
+ interface Doc extends Record {
20
+ title: string;
21
+ }
22
+
23
+ class DocTable extends Table<Doc> {
24
+ public name = 'is_system_doc';
25
+ /** Every door open to everyone: a session Db reaches the write with no user to resolve. */
26
+ public auth: Table<Doc>['auth'] = { db: { all: 'public' } };
27
+ public columns = withRecordColumns<Doc>({
28
+ title: new StringColumn('title'),
29
+ });
30
+ }
31
+
32
+ const docTable = new DocTable() as Table<Doc>;
33
+
34
+ /** What each hook was handed: the mode of its trailing Db. */
35
+ const handed: { phase: string; mode: boolean | undefined }[] = [];
36
+
37
+ class ModeRecordingWatcher implements TableWatcher<Doc> {
38
+ name(): string {
39
+ return 'ModeRecordingWatcher';
40
+ }
41
+
42
+ table(): Table<Doc> {
43
+ return docTable;
44
+ }
45
+
46
+ async beforeInsert<T extends Doc>(record: Omit<T, keyof Doc>, db?: Db): Promise<Omit<T, keyof Doc>> {
47
+ handed.push({ phase: 'beforeInsert', mode: db?.isSystem() });
48
+ return record;
49
+ }
50
+
51
+ async afterInsert<T extends Doc>(_record: T, db?: Db): Promise<void> {
52
+ handed.push({ phase: 'afterInsert', mode: db?.isSystem() });
53
+ }
54
+ }
55
+
56
+ class TestTransactionContextFactory implements DefaultTransactionContextFactory {
57
+ private context: TransactionContextData = {};
58
+
59
+ getTransactionContext(): TransactionContextData {
60
+ return this.context;
61
+ }
62
+
63
+ async runInContext<T>(context: TransactionContextData, fn: () => Promise<T>): Promise<T> {
64
+ const previous = this.context;
65
+ this.context = context;
66
+ try {
67
+ return await fn();
68
+ } finally {
69
+ this.context = previous;
70
+ }
71
+ }
72
+ }
73
+
74
+ /** Accepts every statement; no database behind it. */
75
+ class AcceptingDriver implements DbDriver {
76
+ getDbName(): string {
77
+ return 'test';
78
+ }
79
+
80
+ async createDbIfNotExists(): Promise<void> {}
81
+
82
+ async createDb(): Promise<void> {}
83
+
84
+ async dropDb(): Promise<void> {}
85
+
86
+ getTableManager(): never {
87
+ throw new Error('not used by these tests');
88
+ }
89
+
90
+ async runQuery(): Promise<never[]> {
91
+ return [];
92
+ }
93
+
94
+ async runDml(generateStatement: (config: DbDriverDmlStatementConfig) => Statement): Promise<number> {
95
+ generateStatement({
96
+ useParams: true,
97
+ useNamedParams: true,
98
+ prefixTablesWithDb: false,
99
+ getDriverColumnType: () => 'STRING(MAX)',
100
+ });
101
+ return 1;
102
+ }
103
+
104
+ async runTransaction<T>(fn: (transaction: unknown) => Promise<T>): Promise<T> {
105
+ return await fn({ id: 'transaction' });
106
+ }
107
+
108
+ getOperationDeadlineMs(): number {
109
+ return 1000;
110
+ }
111
+ }
112
+
113
+ type SourceRepositoryInternals = { objectCache: { [qualifiedName: string]: unknown[] } };
114
+ type TableWatcherRunnerStatics = { tableWatcherMap?: unknown };
115
+ const objectCache = () => (SourceRepository.get() as unknown as SourceRepositoryInternals).objectCache;
116
+ const DEFAULT_DRIVER_GLOBAL = '__proteinjs_db_defaultDbDriver';
117
+
118
+ const driver = new AcceptingDriver();
119
+ const transactions = new TestTransactionContextFactory();
120
+ const modesHandedBy = async (db: Db<Doc>): Promise<(boolean | undefined)[]> => {
121
+ handed.length = 0;
122
+ await db.insert(docTable, { title: 'row' });
123
+ expect(handed.map(({ phase }) => phase)).toEqual(['beforeInsert', 'afterInsert']);
124
+ return handed.map(({ mode }) => mode);
125
+ };
126
+
127
+ describe('Db.isSystem', () => {
128
+ let originalWatchers: unknown[] | undefined;
129
+ let originalFactories: unknown[] | undefined;
130
+ let originalTransactionFactories: unknown[] | undefined;
131
+
132
+ beforeAll(() => {
133
+ originalWatchers = objectCache()['@proteinjs/db/TableWatcher'];
134
+ originalFactories = objectCache()['@proteinjs/db/DefaultDbDriverFactory'];
135
+ originalTransactionFactories = objectCache()['@proteinjs/db/DefaultTransactionContextFactory'];
136
+ objectCache()['@proteinjs/db/TableWatcher'] = [new ModeRecordingWatcher()];
137
+ // The public factories resolve their driver and transaction context through reflection.
138
+ objectCache()['@proteinjs/db/DefaultDbDriverFactory'] = [{ getDbDriver: () => driver }];
139
+ objectCache()['@proteinjs/db/DefaultTransactionContextFactory'] = [transactions];
140
+ delete (globalThis as { [key: string]: unknown })[DEFAULT_DRIVER_GLOBAL];
141
+ (TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
142
+ });
143
+
144
+ afterAll(() => {
145
+ const restore = (name: string, original: unknown[] | undefined) => {
146
+ if (original) {
147
+ objectCache()[name] = original;
148
+ } else {
149
+ delete objectCache()[name];
150
+ }
151
+ };
152
+ restore('@proteinjs/db/TableWatcher', originalWatchers);
153
+ restore('@proteinjs/db/DefaultDbDriverFactory', originalFactories);
154
+ restore('@proteinjs/db/DefaultTransactionContextFactory', originalTransactionFactories);
155
+ delete (globalThis as { [key: string]: unknown })[DEFAULT_DRIVER_GLOBAL];
156
+ (TableWatcherRunner as unknown as TableWatcherRunnerStatics).tableWatcherMap = undefined;
157
+ });
158
+
159
+ test('a system Db answers true, a session Db false — on the public factories and on the constructor', () => {
160
+ expect(getDbAsSystem().isSystem()).toBe(true);
161
+ expect(getDb().isSystem()).toBe(false);
162
+ expect(new Db<Doc>(driver, () => docTable, transactions, true).isSystem()).toBe(true);
163
+ expect(new Db<Doc>(driver, () => docTable, transactions).isSystem()).toBe(false);
164
+ });
165
+
166
+ test("the Db a write hands its watchers answers for THAT write: a system write's hooks read true", async () => {
167
+ expect(await modesHandedBy(getDbAsSystem<Doc>())).toEqual([true, true]);
168
+ });
169
+
170
+ test("the Db a write hands its watchers answers for THAT write: a session write's hooks read false", async () => {
171
+ expect(await modesHandedBy(getDb<Doc>())).toEqual([false, false]);
172
+ });
173
+ });