@proteinjs/db 1.57.0 → 1.58.1
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 +22 -0
- package/dist/generated/index.js +1 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/generated/source/index.d.ts +9 -0
- package/dist/generated/source/index.d.ts.map +1 -0
- package/dist/generated/source/index.js +63 -0
- package/dist/generated/source/index.js.map +1 -0
- package/dist/generated/test/index.js +1 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -5
- package/dist/index.js.map +1 -1
- package/dist/{src/source → source}/SourceRecordSyncRunner.d.ts +17 -28
- package/dist/source/SourceRecordSyncRunner.d.ts.map +1 -0
- package/dist/{src/source → source}/SourceRecordSyncRunner.js +62 -36
- package/dist/source/SourceRecordSyncRunner.js.map +1 -0
- package/dist/source/index.d.ts +10 -0
- package/dist/source/index.d.ts.map +1 -0
- package/dist/source/index.js +26 -0
- package/dist/source/index.js.map +1 -0
- package/dist/src/CursorWindowPager.d.ts +9 -0
- package/dist/src/CursorWindowPager.d.ts.map +1 -1
- package/dist/src/CursorWindowPager.js +26 -0
- package/dist/src/CursorWindowPager.js.map +1 -1
- package/dist/src/Db.d.ts +3 -1
- package/dist/src/Db.d.ts.map +1 -1
- package/dist/src/Db.js +5 -3
- package/dist/src/Db.js.map +1 -1
- package/dist/src/MigrationRunner.js +2 -2
- package/dist/src/MigrationRunner.js.map +1 -1
- package/dist/src/source/SourceRecordSync.d.ts +44 -0
- package/dist/src/source/SourceRecordSync.d.ts.map +1 -0
- package/dist/src/source/SourceRecordSync.js +18 -0
- package/dist/src/source/SourceRecordSync.js.map +1 -0
- package/dist/test/BrowserIndexNoNodeBuiltins.test.d.ts +2 -0
- package/dist/test/BrowserIndexNoNodeBuiltins.test.d.ts.map +1 -0
- package/dist/test/BrowserIndexNoNodeBuiltins.test.js +185 -0
- package/dist/test/BrowserIndexNoNodeBuiltins.test.js.map +1 -0
- package/dist/test/CursorWindowPager.test.js +74 -1
- package/dist/test/CursorWindowPager.test.js.map +1 -1
- package/dist/test/SourceRecordLoaderQualifiedName.test.js +4 -3
- package/dist/test/SourceRecordLoaderQualifiedName.test.js.map +1 -1
- package/dist/test/SourceRecordSyncRunner.test.js +1 -1
- package/dist/test/SourceRecordSyncRunner.test.js.map +1 -1
- package/dist/test/SourceRecordSyncSeam.test.d.ts +2 -0
- package/dist/test/SourceRecordSyncSeam.test.d.ts.map +1 -0
- package/dist/test/SourceRecordSyncSeam.test.js +245 -0
- package/dist/test/SourceRecordSyncSeam.test.js.map +1 -0
- package/dist/test/reusable/SourceRecordSyncTests.js +1 -1
- package/dist/test/reusable/SourceRecordSyncTests.js.map +1 -1
- package/generated/index.ts +1 -1
- package/generated/source/index.ts +59 -0
- package/generated/test/index.ts +1 -1
- package/index.ts +5 -3
- package/package.json +12 -4
- package/{src/source → source}/SourceRecordSyncRunner.ts +42 -61
- package/source/index.ts +9 -0
- package/source.d.ts +2 -0
- package/source.js +4 -0
- package/src/CursorWindowPager.ts +17 -0
- package/src/Db.ts +5 -3
- package/src/MigrationRunner.ts +2 -2
- package/src/source/SourceRecordSync.ts +54 -0
- package/test/BrowserIndexNoNodeBuiltins.test.ts +123 -0
- package/test/CursorWindowPager.test.ts +48 -1
- package/test/SourceRecordLoaderQualifiedName.test.ts +4 -3
- package/test/SourceRecordSyncRunner.test.ts +1 -1
- package/test/SourceRecordSyncSeam.test.ts +140 -0
- package/test/reusable/SourceRecordSyncTests.ts +1 -1
- package/tsconfig.json +3 -2
- package/dist/src/source/SourceRecordSyncRunner.d.ts.map +0 -1
- package/dist/src/source/SourceRecordSyncRunner.js.map +0 -1
package/index.ts
CHANGED
|
@@ -17,9 +17,11 @@ export * from './src/RecordIterator';
|
|
|
17
17
|
export * from './src/CursorWindowPager';
|
|
18
18
|
export * from './src/source/SourceRecord';
|
|
19
19
|
export * from './src/source/SourceRecordRepo';
|
|
20
|
-
// The sync
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// The source-record sync's seam: its summary types and `getSourceRecordSync`. The runner itself is
|
|
21
|
+
// server-only code (it reads package.json files off the booting process) behind the
|
|
22
|
+
// `@proteinjs/db/source` subpath, which a server's boot imports once; a consumer test harness that
|
|
23
|
+
// runs one boot of the sync directly imports it from there too.
|
|
24
|
+
export * from './src/source/SourceRecordSync';
|
|
23
25
|
export * from './src/MigrationRunner';
|
|
24
26
|
export * from './src/QueryBuilderFactory';
|
|
25
27
|
export * from './src/TableWatcher';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@proteinjs/db",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.58.1",
|
|
4
4
|
"main": "./dist/generated/index.js",
|
|
5
5
|
"types": "./dist/generated/index.d.ts",
|
|
6
6
|
"exports": {
|
|
@@ -11,12 +11,19 @@
|
|
|
11
11
|
"./test": {
|
|
12
12
|
"types": "./dist/generated/test/index.d.ts",
|
|
13
13
|
"default": "./dist/generated/test/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./source": {
|
|
16
|
+
"types": "./dist/generated/source/index.d.ts",
|
|
17
|
+
"default": "./dist/generated/source/index.js"
|
|
14
18
|
}
|
|
15
19
|
},
|
|
16
20
|
"typesVersions": {
|
|
17
21
|
"*": {
|
|
18
22
|
"test": [
|
|
19
23
|
"dist/generated/test/index.d.ts"
|
|
24
|
+
],
|
|
25
|
+
"source": [
|
|
26
|
+
"dist/generated/source/index.d.ts"
|
|
20
27
|
]
|
|
21
28
|
}
|
|
22
29
|
},
|
|
@@ -36,7 +43,7 @@
|
|
|
36
43
|
"license": "MIT",
|
|
37
44
|
"scripts": {
|
|
38
45
|
"clean": "rm -rf dist/ node_modules/ generated/",
|
|
39
|
-
"build": "reflection-build && REFLECTION_SOURCE_DIRS=test,src REFLECTION_EXPORT_FROM=test/index.ts reflection-build && tsc",
|
|
46
|
+
"build": "reflection-build && REFLECTION_SOURCE_DIRS=test,src REFLECTION_EXPORT_FROM=test/index.ts reflection-build && REFLECTION_SOURCE_DIRS=source,src REFLECTION_EXPORT_FROM=source/index.ts reflection-build && tsc",
|
|
40
47
|
"watch": "reflection-watch",
|
|
41
48
|
"test": "jest --passWithNoTests"
|
|
42
49
|
},
|
|
@@ -64,7 +71,8 @@
|
|
|
64
71
|
"eslint-plugin-prettier": "5.1.3",
|
|
65
72
|
"jest": "29.7.0",
|
|
66
73
|
"ts-jest": "29.1.1",
|
|
67
|
-
"typescript": "5.2.2"
|
|
74
|
+
"typescript": "5.2.2",
|
|
75
|
+
"webpack": "5.88.2"
|
|
68
76
|
},
|
|
69
|
-
"gitHead": "
|
|
77
|
+
"gitHead": "0c9fe556af1052933bb3e6fef08f9e33c129c2a6"
|
|
70
78
|
}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
1
3
|
import { Logger } from '@proteinjs/logger';
|
|
2
4
|
import { QueryBuilder } from '@proteinjs/db-query';
|
|
3
|
-
import { getSourceRecordLoaders, SourceRecord, getSourceRecordTables } from '
|
|
4
|
-
import { Table } from '../Table';
|
|
5
|
-
import { Db, getDbAsSystem } from '../Db';
|
|
6
|
-
import { SourceRecordRepo } from '
|
|
7
|
-
import { RecordSerializer } from '../Record';
|
|
5
|
+
import { getSourceRecordLoaders, SourceRecord, getSourceRecordTables } from '../src/source/SourceRecord';
|
|
6
|
+
import { Table } from '../src/Table';
|
|
7
|
+
import { Db, getDbAsSystem } from '../src/Db';
|
|
8
|
+
import { SourceRecordRepo } from '../src/source/SourceRecordRepo';
|
|
9
|
+
import { RecordSerializer } from '../src/Record';
|
|
10
|
+
import { SourceRecordLoadSummary, SourceRecordSync } from '../src/source/SourceRecordSync';
|
|
8
11
|
|
|
9
12
|
type DeclaredRecord = {
|
|
10
13
|
/** The owning package (the declaring loader's package) — the grain the sync prunes within. */
|
|
@@ -19,29 +22,12 @@ type SourceRecordsMap = {
|
|
|
19
22
|
[tableName: string]: { table: Table<any>; records: DeclaredRecord[] };
|
|
20
23
|
};
|
|
21
24
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Declarations whose natural key is held by a row the sync does not own (written at runtime —
|
|
30
|
-
* a person's own row, say): refused for this boot, that row left exactly as it is (see
|
|
31
|
-
* {@link SourceRecordOptions.naturalKey}).
|
|
32
|
-
*/
|
|
33
|
-
refused: number;
|
|
34
|
-
deletes: number;
|
|
35
|
-
removedUpdates: number;
|
|
36
|
-
/** Rows stamped by a newer version of their declaring package — left exactly as they are. */
|
|
37
|
-
skippedNewer: number;
|
|
38
|
-
/** Source-loaded rows with no owner stamp that no declaration in this build claims. */
|
|
39
|
-
unowned: number;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export type SourceRecordLoadSummary = { [tableName: string]: SourceRecordTableLoadSummary };
|
|
43
|
-
|
|
44
|
-
export class SourceRecordSyncRunner {
|
|
25
|
+
/**
|
|
26
|
+
* The one {@link SourceRecordSync}: server-only code (it reads package.json files off the booting
|
|
27
|
+
* process) behind the `@proteinjs/db/source` subpath — a server's boot imports that subpath once,
|
|
28
|
+
* and reflection hands this runner to `Db.init` through `getSourceRecordSync`.
|
|
29
|
+
*/
|
|
30
|
+
export class SourceRecordSyncRunner implements SourceRecordSync {
|
|
45
31
|
private logger = new Logger({ name: this.constructor.name });
|
|
46
32
|
|
|
47
33
|
/** Memo of resolved package versions — one resolution (and at most one warning) per source. */
|
|
@@ -627,44 +613,39 @@ export class SourceRecordSyncRunner {
|
|
|
627
613
|
* `<pkg>/package.json` is usually not requireable), then the nearest package.json whose
|
|
628
614
|
* `name` matches is read walking up from it.
|
|
629
615
|
*
|
|
630
|
-
* Returns undefined where neither road resolves —
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
*
|
|
616
|
+
* Returns undefined where neither road resolves — a package not resolvable from cwd. The sync
|
|
617
|
+
* then has no place in the version order for that package: it never touches its
|
|
618
|
+
* version-stamped rows, and what it writes is unversioned (see {@link isNewerStamp}). Logged
|
|
619
|
+
* once per boot so a misconfigured cwd is visible.
|
|
620
|
+
*
|
|
621
|
+
* Plain Node here — `fs`, `path` and the CJS `require.resolve` — because this module is
|
|
622
|
+
* server-only code behind the `@proteinjs/db/source` subpath: the package's browser index never
|
|
623
|
+
* imports it (test/BrowserIndexNoNodeBuiltins.test.ts holds that line), so nothing here hides
|
|
624
|
+
* from a bundler.
|
|
634
625
|
*/
|
|
635
626
|
private resolveSourceVersion(source: string): string | undefined {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const cwd = typeof process !== 'undefined' && typeof process.cwd === 'function' ? process.cwd() : undefined;
|
|
643
|
-
if (nodeRequire && cwd) {
|
|
644
|
-
try {
|
|
645
|
-
const path = nodeRequire('path');
|
|
646
|
-
const fs = nodeRequire('fs');
|
|
647
|
-
const ownVersion = this.packageVersionAt(fs, path.join(cwd, 'package.json'), source);
|
|
648
|
-
if (ownVersion) {
|
|
649
|
-
return ownVersion;
|
|
650
|
-
}
|
|
627
|
+
const cwd = process.cwd();
|
|
628
|
+
try {
|
|
629
|
+
const ownVersion = this.packageVersionAt(path.join(cwd, 'package.json'), source);
|
|
630
|
+
if (ownVersion) {
|
|
631
|
+
return ownVersion;
|
|
632
|
+
}
|
|
651
633
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
634
|
+
const entryPath = require.resolve(source, { paths: [cwd] });
|
|
635
|
+
// Walk up from the entry file to the package's own package.json (the name check skips
|
|
636
|
+
// nested stubs like a dist/package.json); stop at the filesystem root.
|
|
637
|
+
for (let directory = path.dirname(entryPath); ; directory = path.dirname(directory)) {
|
|
638
|
+
const version = this.packageVersionAt(path.join(directory, 'package.json'), source);
|
|
639
|
+
if (version) {
|
|
640
|
+
return version;
|
|
641
|
+
}
|
|
660
642
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
643
|
+
if (path.dirname(directory) === directory) {
|
|
644
|
+
break;
|
|
664
645
|
}
|
|
665
|
-
} catch {
|
|
666
|
-
// Unresolvable from cwd — reported below.
|
|
667
646
|
}
|
|
647
|
+
} catch {
|
|
648
|
+
// Unresolvable from cwd — reported below.
|
|
668
649
|
}
|
|
669
650
|
|
|
670
651
|
this.logger.warn({
|
|
@@ -674,7 +655,7 @@ export class SourceRecordSyncRunner {
|
|
|
674
655
|
}
|
|
675
656
|
|
|
676
657
|
/** The `version` of the package.json at `packageJsonPath` when it exists and names `source`. */
|
|
677
|
-
private packageVersionAt(
|
|
658
|
+
private packageVersionAt(packageJsonPath: string, source: string): string | undefined {
|
|
678
659
|
if (!fs.existsSync(packageJsonPath)) {
|
|
679
660
|
return undefined;
|
|
680
661
|
}
|
package/source/index.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@proteinjs/db/source` — the server-only half of the source-record sync: the runner behind
|
|
3
|
+
* `Db.init`'s source-record leg. It reads package.json files off the booting process (the
|
|
4
|
+
* version stamp on every row it writes), so it never rides the package's browser index; a
|
|
5
|
+
* server's boot imports this subpath once, which registers the runner as the package's
|
|
6
|
+
* `SourceRecordSync` — the seam `Db.init` and the pre-schema-sync migration phase resolve through
|
|
7
|
+
* reflection. A test harness that runs one boot of the sync directly imports the runner from here.
|
|
8
|
+
*/
|
|
9
|
+
export * from './SourceRecordSyncRunner';
|
package/source.d.ts
ADDED
package/source.js
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// Node10/classic-resolution stub for the `./source` 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/source/index.js');
|
package/src/CursorWindowPager.ts
CHANGED
|
@@ -76,6 +76,23 @@ export class CursorWindowPager<T extends Record> {
|
|
|
76
76
|
return { rows, nextAnchor: this.deriveNextAnchor(rows, windowSize) };
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* THE ROWS PAST AN ANCHOR, COUNTED: how many rows the windows after `anchor` would serve — under this pager's
|
|
81
|
+
* own conditions and its lexicographic continuation (the same WHERE the windows read with, never a second
|
|
82
|
+
* filter), with no sort and no pagination: one COUNT at the database. A null anchor counts the whole data
|
|
83
|
+
* set. A consumer that sizes room for the rows a reader has not loaded yet (a transcript's rows above its
|
|
84
|
+
* loaded start, sized before the first paint) reads this beside the window it just loaded and holds an exact
|
|
85
|
+
* count where a guess would refill or collapse later.
|
|
86
|
+
*/
|
|
87
|
+
async countPast(anchor: CursorAnchor | null): Promise<number> {
|
|
88
|
+
const qb = this.createQuery();
|
|
89
|
+
if (anchor) {
|
|
90
|
+
qb.or(this.anchorConditions(anchor));
|
|
91
|
+
}
|
|
92
|
+
const db = this.options?.db ?? getDb();
|
|
93
|
+
return await db.getRowCount(this.table, qb);
|
|
94
|
+
}
|
|
95
|
+
|
|
79
96
|
static encodeAnchor(anchor: CursorAnchor): string {
|
|
80
97
|
return Serializer.serialize(anchor);
|
|
81
98
|
}
|
package/src/Db.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
import { ReverseCascadeEdgeIndex } from './ReverseCascadeEdgeIndex';
|
|
13
13
|
import { Record, RecordSerializer, SerializedRecord } from './Record';
|
|
14
14
|
import { Logger } from '@proteinjs/logger';
|
|
15
|
-
import {
|
|
15
|
+
import { getSourceRecordSync } from './source/SourceRecordSync';
|
|
16
16
|
import { ParameterizationConfig, ParamType, QueryBuilder, Statement, StatementFactory } from '@proteinjs/db-query';
|
|
17
17
|
import { QueryBuilderFactory } from './QueryBuilderFactory';
|
|
18
18
|
import { StatementConfigFactory } from './StatementConfigFactory';
|
|
@@ -211,7 +211,9 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
211
211
|
* backfill, which fails loudly over violating data); an immediate no-op unless a migration
|
|
212
212
|
* declares {@link Migration.preSchemaSync};
|
|
213
213
|
* 3. schema sync — create/alter every registered table (the DDL);
|
|
214
|
-
* 4. source-record sync
|
|
214
|
+
* 4. source-record sync — through {@link getSourceRecordSync}: the runner is server-only code
|
|
215
|
+
* behind the `@proteinjs/db/source` subpath (the browser index never carries it), registered
|
|
216
|
+
* by the server's one import of that subpath; a boot without it fails here, loudly.
|
|
215
217
|
*/
|
|
216
218
|
async init(): Promise<void> {
|
|
217
219
|
await this.dbDriver.createDbIfNotExists();
|
|
@@ -225,7 +227,7 @@ export class Db<R extends Record = Record> implements DbService<R> {
|
|
|
225
227
|
const { MigrationRunner } = await import('./MigrationRunner');
|
|
226
228
|
await new MigrationRunner().runPreSchemaSyncMigrations(this.dbDriver.getTableManager());
|
|
227
229
|
await this.dbDriver.getTableManager().loadTables();
|
|
228
|
-
await
|
|
230
|
+
await getSourceRecordSync().load();
|
|
229
231
|
}
|
|
230
232
|
|
|
231
233
|
async tableExists<T extends R>(table: Table<T>): Promise<boolean> {
|
package/src/MigrationRunner.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { Moment, moment } from './opt/moment';
|
|
|
2
2
|
import { Db, getDb, getDbAsSystem } from './Db';
|
|
3
3
|
import { Table } from './Table';
|
|
4
4
|
import { SourceRecordRepo } from './source/SourceRecordRepo';
|
|
5
|
-
import {
|
|
5
|
+
import { getSourceRecordSync } from './source/SourceRecordSync';
|
|
6
6
|
import { getSourceRecordLoaders } from './source/SourceRecord';
|
|
7
7
|
import { MigrationRunnerService, getMigrationRunnerService } from './services/MigrationRunnerService';
|
|
8
8
|
import { Migration, MigrationEstimate, MigrationTable } from './tables/MigrationTable';
|
|
@@ -195,7 +195,7 @@ export class MigrationRunner implements MigrationRunnerService {
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
await tableManager.loadTable(migrationTable);
|
|
198
|
-
await
|
|
198
|
+
await getSourceRecordSync().load(migrationTable);
|
|
199
199
|
this.logger.info({
|
|
200
200
|
message: `Running ${flagged.length} pre-schema-sync migration${flagged.length === 1 ? '' : 's'} before schema sync`,
|
|
201
201
|
obj: { ids: flagged.map((migration) => migration.id) },
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { Loadable, SourceRepository } from '@proteinjs/reflection';
|
|
2
|
+
import type { Table } from '../Table';
|
|
3
|
+
|
|
4
|
+
/** What one boot of the sync did to one source-record table. */
|
|
5
|
+
export type SourceRecordTableLoadSummary = {
|
|
6
|
+
inserts: number;
|
|
7
|
+
updates: number;
|
|
8
|
+
unchanged: number;
|
|
9
|
+
adopted: number;
|
|
10
|
+
/**
|
|
11
|
+
* Declarations whose natural key is held by a row the sync does not own (written at runtime —
|
|
12
|
+
* a person's own row, say): refused for this boot, that row left exactly as it is (see
|
|
13
|
+
* {@link SourceRecordOptions.naturalKey}).
|
|
14
|
+
*/
|
|
15
|
+
refused: number;
|
|
16
|
+
deletes: number;
|
|
17
|
+
removedUpdates: number;
|
|
18
|
+
/** Rows stamped by a newer version of their declaring package — left exactly as they are. */
|
|
19
|
+
skippedNewer: number;
|
|
20
|
+
/** Source-loaded rows with no owner stamp that no declaration in this build claims. */
|
|
21
|
+
unowned: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SourceRecordLoadSummary = { [tableName: string]: SourceRecordTableLoadSummary };
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The source-record sync a boot runs — the seam `Db.init` (and the pre-schema-sync migration
|
|
28
|
+
* phase) reach it through. Its one implementation, `SourceRecordSyncRunner`, is server-only code:
|
|
29
|
+
* it reads package.json files off the booting process to version-stamp every row, so it lives
|
|
30
|
+
* behind the `@proteinjs/db/source` subpath and this package's browser index never carries it
|
|
31
|
+
* (test/BrowserIndexNoNodeBuiltins.test.ts holds that line). A server imports
|
|
32
|
+
* `@proteinjs/db/source` once at boot, which registers the runner here through reflection; the
|
|
33
|
+
* browser index declares only this interface.
|
|
34
|
+
*/
|
|
35
|
+
export interface SourceRecordSync extends Loadable {
|
|
36
|
+
/** One boot of the sync, for every source-record table in the build — or for `onlyTable`. */
|
|
37
|
+
load(onlyTable?: Table<any>): Promise<SourceRecordLoadSummary>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The registered sync — or a loud refusal naming the subpath that registers it. A boot never
|
|
42
|
+
* silently skips its source records: a server that has not imported `@proteinjs/db/source` fails
|
|
43
|
+
* here, at `Db.init`, with the fix in the message.
|
|
44
|
+
*/
|
|
45
|
+
export const getSourceRecordSync = (): SourceRecordSync => {
|
|
46
|
+
const sync = SourceRepository.get().object<SourceRecordSync>('@proteinjs/db/SourceRecordSync');
|
|
47
|
+
if (!sync) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Unable to find a @proteinjs/db/SourceRecordSync implementation. The source-record sync is server-only code behind the '@proteinjs/db/source' subpath: import '@proteinjs/db/source' once in the server's boot (it registers SourceRecordSyncRunner), and the boot's source records load.`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return sync;
|
|
54
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import webpack from 'webpack';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE LAW: this package's browser-facing index never reaches a Node builtin. `@proteinjs/db` is
|
|
8
|
+
* one package for both halves — the browser's Db talks through DbService, the server's through a
|
|
9
|
+
* driver — and its public entry (`dist/generated/index.js`, what a consumer's webpack bundles)
|
|
10
|
+
* must bundle for the browser as it stands; a consumer app has no business patching the
|
|
11
|
+
* library's reach with a `resolve.fallback`. The source-record sync (it reads package.json files
|
|
12
|
+
* off the booting process) is the server-only code that broke this line in db 1.55–1.58 — a
|
|
13
|
+
* webpack 5 client bundle met its `fs` and booted to nothing — and now lives behind the
|
|
14
|
+
* `@proteinjs/db/source` subpath. This guard holds the line for every module to come.
|
|
15
|
+
*
|
|
16
|
+
* Mechanics: webpack 5.88.2 — the consumer's bundler, at app-ui's version — bundles the built
|
|
17
|
+
* browser index for `target: 'web'` with this package's own dependencies external, so the only
|
|
18
|
+
* resolution left to fail is a Node builtin's, which webpack 5 refuses by name ("Module not
|
|
19
|
+
* found: Error: Can't resolve 'fs'"). It refuses as a WARNING when the reach sits in a try/catch
|
|
20
|
+
* (an optional dependency) — exactly how 1.58.0's runner reached `fs`, through a renamed
|
|
21
|
+
* `require` that esbuild never sees but webpack's parser tracks — so errors AND warnings must be
|
|
22
|
+
* empty. The module list then pins the subpath out of the graph by path.
|
|
23
|
+
*
|
|
24
|
+
* The one named exception: four encryption modules import `crypto` (since before 1.54.1), and
|
|
25
|
+
* consumers substitute a browser build for it (app-ui's webpack: `fallback.crypto:
|
|
26
|
+
* crypto-browserify`). Pinned to exactly those four files — a fifth reader of `crypto`, or any
|
|
27
|
+
* other builtin anywhere, is red.
|
|
28
|
+
*/
|
|
29
|
+
const packageDir = path.join(__dirname, '..');
|
|
30
|
+
const BROWSER_INDEX = 'dist/generated/index.js';
|
|
31
|
+
const SERVER_ONLY_SUBPATH = /(^|\/)dist\/source\//;
|
|
32
|
+
const CRYPTO_READERS = [
|
|
33
|
+
'dist/src/encryption/DataKeyStore.js',
|
|
34
|
+
'dist/src/encryption/EncryptionEnvelope.js',
|
|
35
|
+
'dist/src/encryption/InMemoryMasterKeyProvider.js',
|
|
36
|
+
'dist/src/encryption/SearchTokenizer.js',
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/** What one browser bundle of the index came to. */
|
|
40
|
+
type Bundle = { errors: string[]; warnings: string[]; modules: string[]; cryptoReaders: string[] };
|
|
41
|
+
|
|
42
|
+
/** `fs.rmSync` (Node 14.14+; this package's @types/node predates it). */
|
|
43
|
+
const removeDirectory = (directory: string) =>
|
|
44
|
+
(fs as unknown as { rmSync(target: string, options: { recursive: true; force: true }): void }).rmSync(directory, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
force: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
/** This package's dependencies: a consumer resolves those itself; never this bundle's concern. */
|
|
50
|
+
const dependencies = (): string[] =>
|
|
51
|
+
Object.keys(JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8')).dependencies);
|
|
52
|
+
|
|
53
|
+
const bundleBrowserIndex = (): Promise<Bundle> =>
|
|
54
|
+
new Promise((resolve, reject) => {
|
|
55
|
+
const names = dependencies();
|
|
56
|
+
const cryptoReaders = new Set<string>();
|
|
57
|
+
const outDir = fs.mkdtempSync(path.join(os.tmpdir(), 'db-browser-index-'));
|
|
58
|
+
webpack(
|
|
59
|
+
{
|
|
60
|
+
mode: 'none',
|
|
61
|
+
target: 'web',
|
|
62
|
+
context: packageDir,
|
|
63
|
+
entry: path.join(packageDir, BROWSER_INDEX),
|
|
64
|
+
output: { path: outDir, filename: 'browser.js' },
|
|
65
|
+
externals: [
|
|
66
|
+
({ request, contextInfo }, callback) => {
|
|
67
|
+
if (names.some((name) => request === name || !!request?.startsWith(`${name}/`))) {
|
|
68
|
+
return callback(undefined, `commonjs ${request}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// The crypto exception, pinned to its readers; every other builtin stays webpack's own refusal.
|
|
72
|
+
if (request && /^(node:)?crypto$/.test(request)) {
|
|
73
|
+
const issuer = path.relative(packageDir, contextInfo?.issuer ?? '');
|
|
74
|
+
cryptoReaders.add(issuer);
|
|
75
|
+
if (CRYPTO_READERS.includes(issuer)) {
|
|
76
|
+
return callback(undefined, `commonjs ${request}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
callback();
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
(error, stats) => {
|
|
85
|
+
removeDirectory(outDir);
|
|
86
|
+
if (error || !stats) {
|
|
87
|
+
return reject(error ?? new Error('webpack produced no stats'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const json = stats.toJson({ all: false, errors: true, warnings: true, modules: true });
|
|
91
|
+
const line = ({ message, moduleName }: { message: string; moduleName?: string }) =>
|
|
92
|
+
`${message.split('\n')[0]} (${moduleName})`;
|
|
93
|
+
resolve({
|
|
94
|
+
errors: (json.errors ?? []).map(line),
|
|
95
|
+
warnings: (json.warnings ?? []).map(line),
|
|
96
|
+
modules: (json.modules ?? []).map((module) => module.name ?? ''),
|
|
97
|
+
cryptoReaders: Array.from(cryptoReaders).sort(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('the browser index (dist/generated/index.js) bundles for the browser', () => {
|
|
104
|
+
let bundle: Bundle;
|
|
105
|
+
|
|
106
|
+
beforeAll(async () => {
|
|
107
|
+
bundle = await bundleBrowserIndex();
|
|
108
|
+
}, 60000);
|
|
109
|
+
|
|
110
|
+
it('reaches no Node builtin: webpack resolved every module with only the dependencies external', () => {
|
|
111
|
+
expect(bundle.errors).toEqual([]);
|
|
112
|
+
expect(bundle.warnings).toEqual([]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('never imports the server-only source-record sync (the @proteinjs/db/source subpath)', () => {
|
|
116
|
+
expect(bundle.modules.length).toBeGreaterThan(0);
|
|
117
|
+
expect(bundle.modules.filter((module) => SERVER_ONLY_SUBPATH.test(module))).toEqual([]);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('pins the crypto exception to exactly its four readers', () => {
|
|
121
|
+
expect(bundle.cryptoReaders).toEqual(CRYPTO_READERS.slice().sort());
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -23,13 +23,18 @@ type Row = {
|
|
|
23
23
|
const table = { name: 'pager_test' } as Table<Row & { created: moment.Moment; updated: moment.Moment }>;
|
|
24
24
|
type PagerRow = Row & { created: moment.Moment; updated: moment.Moment };
|
|
25
25
|
|
|
26
|
-
const staged: { rows: Row[] } = { rows: [] };
|
|
26
|
+
const staged: { rows: Row[]; count: number } = { rows: [], count: 0 };
|
|
27
27
|
const executed: QueryBuilder<PagerRow>[] = [];
|
|
28
|
+
const counted: QueryBuilder<PagerRow>[] = [];
|
|
28
29
|
const stubDb = {
|
|
29
30
|
query: async (_table: unknown, qb: QueryBuilder<PagerRow>) => {
|
|
30
31
|
executed.push(qb);
|
|
31
32
|
return staged.rows;
|
|
32
33
|
},
|
|
34
|
+
getRowCount: async (_table: unknown, qb: QueryBuilder<PagerRow>) => {
|
|
35
|
+
counted.push(qb);
|
|
36
|
+
return staged.count;
|
|
37
|
+
},
|
|
33
38
|
} as unknown as Db;
|
|
34
39
|
|
|
35
40
|
const createQuery = () => new QueryBuilder<PagerRow>(table.name);
|
|
@@ -183,3 +188,45 @@ describe('loadWindow', () => {
|
|
|
183
188
|
await expect(pager().loadWindow(null, 0)).rejects.toThrow(/window size/i);
|
|
184
189
|
});
|
|
185
190
|
});
|
|
191
|
+
|
|
192
|
+
describe('countPast — the rows past an anchor, counted under the windows’ own WHERE', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
counted.length = 0;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('an anchored count carries the lexicographic continuation and nothing else: no sort, no pagination; the db’s COUNT is the answer', async () => {
|
|
198
|
+
staged.count = 26;
|
|
199
|
+
const remaining = await pager([{ field: 'seq', desc: true }]).countPast({ values: [40, 'row-4'] });
|
|
200
|
+
expect(remaining).toBe(26);
|
|
201
|
+
expect(counted).toHaveLength(1);
|
|
202
|
+
const qb = counted[0];
|
|
203
|
+
// The same continuation a window after this anchor reads with: seq < 40 OR (seq = 40 AND id > 'row-4').
|
|
204
|
+
expect(conditionNodes(qb)).toEqual(
|
|
205
|
+
expect.arrayContaining([
|
|
206
|
+
expect.objectContaining({ field: 'seq', operator: '<', value: 40 }),
|
|
207
|
+
expect.objectContaining({ field: 'seq', operator: '=', value: 40 }),
|
|
208
|
+
expect.objectContaining({ field: 'id', operator: '>', value: 'row-4' }),
|
|
209
|
+
])
|
|
210
|
+
);
|
|
211
|
+
expect(conditionNodes(qb)).toHaveLength(3);
|
|
212
|
+
expect(paginationNode(qb)).toBeUndefined();
|
|
213
|
+
expect(sortNodes(qb)).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('a null anchor counts the whole data set: no conditions beyond the consumer’s own', async () => {
|
|
217
|
+
staged.count = 3;
|
|
218
|
+
expect(await pager([{ field: 'seq', desc: true }]).countPast(null)).toBe(3);
|
|
219
|
+
expect(counted).toHaveLength(1);
|
|
220
|
+
expect(conditionNodes(counted[0])).toHaveLength(0);
|
|
221
|
+
expect(paginationNode(counted[0])).toBeUndefined();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('every count builds on a FRESH query — a window’s cursor conditions never leak into it', async () => {
|
|
225
|
+
const instance = pager([{ field: 'seq', desc: true }]);
|
|
226
|
+
staged.rows = [];
|
|
227
|
+
await instance.loadWindow({ values: [40, 'row-4'] }, 10);
|
|
228
|
+
staged.count = 0;
|
|
229
|
+
await instance.countPast(null);
|
|
230
|
+
expect(conditionNodes(counted[0])).toHaveLength(0);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
@@ -10,11 +10,12 @@ import { graphSerializer } from '@proteinjs/util';
|
|
|
10
10
|
* name, so a second declaration of the name anywhere in the package's reflected sources leaves
|
|
11
11
|
* which one the artifact records to the build's file order — and the loser disappears without
|
|
12
12
|
* a trace. These tests read the artifacts the build wrote (`generated/index.ts` for the package
|
|
13
|
-
* entry, `generated/test/index.ts` for the `./test` subpath,
|
|
14
|
-
*
|
|
13
|
+
* entry, `generated/test/index.ts` for the `./test` subpath, `generated/source/index.ts` for the
|
|
14
|
+
* `./source` subpath — each built over `src/`) and assert the name holds the declaration
|
|
15
|
+
* interface, not some other declaration that shares it.
|
|
15
16
|
*/
|
|
16
17
|
const QUALIFIED_NAME = '@proteinjs/db/SourceRecordLoader';
|
|
17
|
-
const ARTIFACTS = ['generated/index.ts', 'generated/test/index.ts'];
|
|
18
|
+
const ARTIFACTS = ['generated/index.ts', 'generated/test/index.ts', 'generated/source/index.ts'];
|
|
18
19
|
|
|
19
20
|
/** The fields of an emitted node value these tests read. */
|
|
20
21
|
type EmittedDeclaration = { packageName: string; name: string; qualifiedName: string; sourceType: SourceType };
|