@jarenjs/db 0.34.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/ARCHITECTURE.md +397 -0
- package/README.md +218 -0
- package/dist/types/algebra.d.ts +133 -0
- package/dist/types/app.d.ts +49 -0
- package/dist/types/capture.d.ts +85 -0
- package/dist/types/cli.d.ts +2 -0
- package/dist/types/dag-job.d.ts +40 -0
- package/dist/types/ddl.d.ts +170 -0
- package/dist/types/dialect.d.ts +130 -0
- package/dist/types/dialects/sqlite.d.ts +9 -0
- package/dist/types/driver.d.ts +128 -0
- package/dist/types/drivers/bun.d.ts +47 -0
- package/dist/types/drivers/node.d.ts +37 -0
- package/dist/types/drivers/wasm.d.ts +65 -0
- package/dist/types/emit-model.d.ts +44 -0
- package/dist/types/emit.d.ts +72 -0
- package/dist/types/entity.d.ts +23 -0
- package/dist/types/errors.d.ts +165 -0
- package/dist/types/graph.d.ts +28 -0
- package/dist/types/index.d.ts +35 -0
- package/dist/types/jobs.d.ts +134 -0
- package/dist/types/live.d.ts +62 -0
- package/dist/types/migrate.d.ts +163 -0
- package/dist/types/model.d.ts +36 -0
- package/dist/types/patch-sql.d.ts +37 -0
- package/dist/types/plan.d.ts +119 -0
- package/dist/types/profile.d.ts +80 -0
- package/dist/types/query.d.ts +100 -0
- package/dist/types/residual.d.ts +50 -0
- package/dist/types/store.d.ts +53 -0
- package/dist/types/tracker.d.ts +43 -0
- package/dist/types/typed.d.ts +15 -0
- package/dist/types/types.d.ts +26 -0
- package/dist/types/udf.d.ts +70 -0
- package/dist/types/window.d.ts +52 -0
- package/docs/JOBS-FORMAT.md +218 -0
- package/docs/LIVE-FORMAT.md +348 -0
- package/docs/MIGRATION-FORMAT.md +302 -0
- package/docs/MODEL-FORMAT.md +928 -0
- package/package.json +81 -0
- package/schemas/jaren-migration.draft-07.schema.json +144 -0
- package/schemas/jaren-migration.schema.json +144 -0
- package/schemas/jaren-model.draft-07.schema.json +149 -0
- package/schemas/jaren-model.schema.json +149 -0
- package/src/algebra.js +105 -0
- package/src/app.js +108 -0
- package/src/capture.js +584 -0
- package/src/cli.js +264 -0
- package/src/dag-job.js +86 -0
- package/src/ddl.js +588 -0
- package/src/dialect.js +297 -0
- package/src/dialects/sqlite.js +175 -0
- package/src/driver.js +419 -0
- package/src/drivers/bun.js +101 -0
- package/src/drivers/node.js +93 -0
- package/src/drivers/wasm.js +178 -0
- package/src/emit-model.js +208 -0
- package/src/emit.js +393 -0
- package/src/entity.js +367 -0
- package/src/errors.js +173 -0
- package/src/graph.js +101 -0
- package/src/index.js +64 -0
- package/src/jobs.js +507 -0
- package/src/live.js +899 -0
- package/src/migrate.js +1411 -0
- package/src/model.js +476 -0
- package/src/patch-sql.js +150 -0
- package/src/plan.js +1038 -0
- package/src/profile.js +131 -0
- package/src/query.js +1010 -0
- package/src/residual.js +91 -0
- package/src/store.js +1422 -0
- package/src/tracker.js +776 -0
- package/src/typed.js +19 -0
- package/src/types.js +36 -0
- package/src/udf.js +132 -0
- package/src/window.js +125 -0
- package/types/app.d.ts +36 -0
- package/types/bun.d.ts +9 -0
- package/types/index.d.ts +592 -0
- package/types/node.d.ts +15 -0
- package/types/typed.d.ts +108 -0
- package/types/wasm.d.ts +5 -0
package/src/algebra.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The Plan algebra: the dialect-neutral middle stage between the
|
|
4
|
+
* engine's AST and a dialect's SQL. A plan is a plain JSON value —
|
|
5
|
+
* inspectable, golden-testable without a database — and it carries NO
|
|
6
|
+
* SQL text: every string in a plan is a member name, a type tag, an
|
|
7
|
+
* external name or a reason sentence, never a fragment of any query
|
|
8
|
+
* language. `assertNoSqlText` is the tripwire the tests run over every
|
|
9
|
+
* golden.
|
|
10
|
+
*
|
|
11
|
+
* One plan shape covers this version: a guarded selection over ONE
|
|
12
|
+
* collection with optional ordering, window, aggregate and a
|
|
13
|
+
* whole-document projection. Constructs beyond it are residuals by
|
|
14
|
+
* design (see ARCHITECTURE.md's deliberate-residual table).
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** The plan format version, carried on every plan. */
|
|
18
|
+
export const PLAN_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{ segments: ({ name: string } | { index: number })[],
|
|
22
|
+
* type: string, column: string | null }} PlanRef
|
|
23
|
+
* A typed reference into the stored document: `type` is the
|
|
24
|
+
* schema-declared type or `'unknown'`; `column` is the generated
|
|
25
|
+
* column name when the collection indexes this path.
|
|
26
|
+
*
|
|
27
|
+
* @typedef {{ lit: unknown } | { ext: string }} PlanOperand
|
|
28
|
+
*
|
|
29
|
+
* @typedef {(
|
|
30
|
+
* { p: 'and' | 'or', items: PlanPredicate[] } |
|
|
31
|
+
* { p: 'not', item: PlanPredicate } |
|
|
32
|
+
* { p: 'cmp', op: 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge',
|
|
33
|
+
* ref: PlanRef, operand: PlanOperand } |
|
|
34
|
+
* { p: 'typeIs', ref: PlanRef, types: string[], positive: boolean } |
|
|
35
|
+
* { p: 'strop', kind: 'starts' | 'ends' | 'contains',
|
|
36
|
+
* ref: PlanRef, operand: PlanOperand } |
|
|
37
|
+
* { p: 'const', value: boolean } |
|
|
38
|
+
* { p: 'udf', name: string, key: string }
|
|
39
|
+
* )} PlanPredicate
|
|
40
|
+
*
|
|
41
|
+
* @typedef {{ ref: PlanRef, desc: boolean, emptyGreatest: boolean }} PlanOrderTerm
|
|
42
|
+
*
|
|
43
|
+
* @typedef {{
|
|
44
|
+
* planVersion: number,
|
|
45
|
+
* alg: 'select',
|
|
46
|
+
* collection: string,
|
|
47
|
+
* filter: PlanPredicate | null,
|
|
48
|
+
* order: PlanOrderTerm[] | null,
|
|
49
|
+
* window: { offset: number, limit: number | null } | null,
|
|
50
|
+
* aggregate: { fn: 'count' | 'sum' | 'avg' | 'min' | 'max',
|
|
51
|
+
* ref: PlanRef | null } | null,
|
|
52
|
+
* project: 'document',
|
|
53
|
+
* }} Plan
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A fresh select plan over one collection.
|
|
58
|
+
* @param {string} collection
|
|
59
|
+
* @returns {Plan}
|
|
60
|
+
*/
|
|
61
|
+
export function selectPlan(collection) {
|
|
62
|
+
return {
|
|
63
|
+
planVersion: PLAN_VERSION,
|
|
64
|
+
alg: 'select',
|
|
65
|
+
collection,
|
|
66
|
+
filter: null,
|
|
67
|
+
order: null,
|
|
68
|
+
window: null,
|
|
69
|
+
aggregate: null,
|
|
70
|
+
project: 'document',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Conjoin a predicate onto a plan's filter.
|
|
76
|
+
* @param {PlanPredicate | null} filter
|
|
77
|
+
* @param {PlanPredicate} predicate
|
|
78
|
+
* @returns {PlanPredicate}
|
|
79
|
+
*/
|
|
80
|
+
export function conjoin(filter, predicate) {
|
|
81
|
+
if (filter === null) return predicate;
|
|
82
|
+
if (filter.p === 'and') return { p: 'and', items: [...filter.items, predicate] };
|
|
83
|
+
return { p: 'and', items: [filter, predicate] };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Tokens that must never appear anywhere in a plan: if one does, SQL
|
|
88
|
+
* text leaked out of the dialect layer into the neutral algebra.
|
|
89
|
+
*/
|
|
90
|
+
const SQL_TOKENS = [
|
|
91
|
+
'SELECT', 'WHERE', 'ORDER BY', 'LIMIT ', 'INSERT', 'FROM ',
|
|
92
|
+
'jsonb_extract', 'json_type', 'substr(', 'instr(', '"doc"', '@p1', ' AS ',
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Throw when a plan value carries anything that smells like SQL.
|
|
97
|
+
* @param {unknown} plan
|
|
98
|
+
*/
|
|
99
|
+
export function assertNoSqlText(plan) {
|
|
100
|
+
const text = JSON.stringify(plan);
|
|
101
|
+
for (const token of SQL_TOKENS) {
|
|
102
|
+
if (text.includes(token))
|
|
103
|
+
throw new Error(`plan carries SQL text: found '${token}'`);
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//@ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file The app binding for live queries (LIVE-FORMAT §10): GENERATED
|
|
4
|
+
* documents plus a handler factory — the `fsmToApp` precedent. The db
|
|
5
|
+
* package never imports `@jarenjs/app`; the app document declares a
|
|
6
|
+
* subscription (`APP-FORMAT §5.3`) whose registered handler is
|
|
7
|
+
* `createLiveSubscription(store)`, and a two-line action whose whole
|
|
8
|
+
* body is `{ patch: '$payload' }` — the handler prefixes every op
|
|
9
|
+
* with the declared state path, so the app loop applies live patches
|
|
10
|
+
* with the machinery it already has.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Prefix every op path in a live patch with the state slot.
|
|
15
|
+
* @param {any[]} patch
|
|
16
|
+
* @param {string} statePath - JSON Pointer to the slot holding the
|
|
17
|
+
* live result document
|
|
18
|
+
*/
|
|
19
|
+
export function prefixLivePatch(patch, statePath) {
|
|
20
|
+
return patch.map((op) => ({
|
|
21
|
+
...op,
|
|
22
|
+
path: statePath + op.path,
|
|
23
|
+
...(op.from !== undefined ? { from: statePath + op.from } : {}),
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The generated documents (§10): a subscription entry and the
|
|
29
|
+
* patch-forwarding action, both plain data for the app document.
|
|
30
|
+
* @param {{ run?: string, action?: string, statePath: string,
|
|
31
|
+
* collection?: string, query: any, externals?: any, mode?: string,
|
|
32
|
+
* when?: any }} options
|
|
33
|
+
* @returns {{ subscription: any, actions: any }}
|
|
34
|
+
*/
|
|
35
|
+
export function liveAppBinding(options) {
|
|
36
|
+
if (typeof options?.statePath !== 'string' || !options.statePath.startsWith('/')) {
|
|
37
|
+
throw new TypeError('liveAppBinding needs a statePath JSON Pointer');
|
|
38
|
+
}
|
|
39
|
+
if (options.query === undefined) {
|
|
40
|
+
throw new TypeError('liveAppBinding needs the query document');
|
|
41
|
+
}
|
|
42
|
+
const run = options.run ?? 'db/live';
|
|
43
|
+
const action = options.action ?? 'db/liveChanged';
|
|
44
|
+
return {
|
|
45
|
+
subscription: {
|
|
46
|
+
run,
|
|
47
|
+
with: {
|
|
48
|
+
action,
|
|
49
|
+
statePath: options.statePath,
|
|
50
|
+
...(options.collection !== undefined ? { collection: options.collection } : {}),
|
|
51
|
+
query: options.query,
|
|
52
|
+
...(options.externals !== undefined ? { externals: options.externals } : {}),
|
|
53
|
+
...(options.mode !== undefined ? { mode: options.mode } : {}),
|
|
54
|
+
},
|
|
55
|
+
...(options.when !== undefined ? { when: options.when } : {}),
|
|
56
|
+
},
|
|
57
|
+
actions: { [action]: { patch: '$payload' } },
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* The subscription handler factory: registers the live query when the
|
|
63
|
+
* subscription starts, dispatches ONE initializing patch (a `replace`
|
|
64
|
+
* of the whole slot), forwards each emission prefixed, and closes on
|
|
65
|
+
* cleanup. An emission error surfaces as a dispatch of
|
|
66
|
+
* `<action>/error` so the app can render it — silence is not an
|
|
67
|
+
* option the format allows.
|
|
68
|
+
* @param {any} store - an open store with capture
|
|
69
|
+
* @returns {(props: any, dispatch: Function) => Function}
|
|
70
|
+
*/
|
|
71
|
+
export function createLiveSubscription(store) {
|
|
72
|
+
return (props, dispatch) => {
|
|
73
|
+
const { action, statePath, collection, query, externals, mode } = props;
|
|
74
|
+
let closed = false;
|
|
75
|
+
/** @type {any} */
|
|
76
|
+
let live = null;
|
|
77
|
+
const registration = collection !== undefined
|
|
78
|
+
? store.collection(collection).live(query, { externals, mode })
|
|
79
|
+
: store.live(query, { externals, mode });
|
|
80
|
+
registration.then((handle) => {
|
|
81
|
+
if (closed) {
|
|
82
|
+
handle.close();
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
live = handle;
|
|
86
|
+
dispatch(action, [{ op: 'replace', path: statePath, value: handle.result }]);
|
|
87
|
+
handle.subscribe((event) => {
|
|
88
|
+
if (event.error !== undefined) {
|
|
89
|
+
dispatch(`${action}/error`, {
|
|
90
|
+
code: event.error.code, message: String(event.error.message ?? event.error),
|
|
91
|
+
});
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
dispatch(action, prefixLivePatch(event.patch, statePath));
|
|
95
|
+
});
|
|
96
|
+
}, (error) => {
|
|
97
|
+
if (!closed) {
|
|
98
|
+
dispatch(`${action}/error`, {
|
|
99
|
+
code: error.code, message: String(error.message ?? error),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
return () => {
|
|
104
|
+
closed = true;
|
|
105
|
+
if (live !== null) live.close();
|
|
106
|
+
};
|
|
107
|
+
};
|
|
108
|
+
}
|