@assemora/data 0.1.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/dist/query.js ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Query builder (SPEC.md §19, §20, §30).
3
+ *
4
+ * Every call returns a new builder, so a query is safe to share and to derive from.
5
+ * The builder produces a Query AST and never touches an adapter's own query API.
6
+ */
7
+ import { AssemoraError, currentContext } from '@assemora/core';
8
+ import { comparison, group, jsonContains, jsonEquals, jsonLike, orComparison, UNSPECIFIED_LOCALE, } from '@assemora/database';
9
+ import { execute } from './runtime.js';
10
+ /**
11
+ * A pattern match needs a key to match against: matching a whole document as text is
12
+ * meaningless, and the adapters would disagree about what it even means.
13
+ */
14
+ const jsonPath = (field, path) => {
15
+ if (path === '') {
16
+ throw new AssemoraError('INVALID_QUERY', `A pattern match on "${field}" needs a key inside the document`, { status: 400 });
17
+ }
18
+ return path.split('.');
19
+ };
20
+ /**
21
+ * A row count has to be a whole number that is not negative.
22
+ *
23
+ * Passed through unchecked, a `NaN` reaches the adapter, which drops the clause
24
+ * entirely — the query then returns every row instead of the page that was asked for.
25
+ */
26
+ const countable = (method, count) => {
27
+ if (!Number.isInteger(count) || count < 0) {
28
+ throw new AssemoraError('INVALID_QUERY', `${method}() needs a whole number of rows`, {
29
+ status: 400,
30
+ });
31
+ }
32
+ return count;
33
+ };
34
+ const parsePath = (path) => {
35
+ const [head, ...rest] = path.split('.');
36
+ return {
37
+ relation: head ?? path,
38
+ nested: rest.length > 0 ? [parsePath(rest.join('.'))] : [],
39
+ };
40
+ };
41
+ const mergeLoads = (existing, addition) => {
42
+ const found = existing.find((load) => load.relation === addition.relation);
43
+ if (found === undefined)
44
+ return [...existing, addition];
45
+ return existing.map((load) => load.relation === addition.relation
46
+ ? {
47
+ relation: load.relation,
48
+ nested: addition.nested.reduce(mergeLoads, load.nested),
49
+ }
50
+ : load);
51
+ };
52
+ export const createQuery = (state, runtime) => {
53
+ const derive = (next) => createQuery({ ...state, ...next }, runtime);
54
+ const withCondition = (condition) => derive({ where: [...state.where, condition] });
55
+ /**
56
+ * The language this query reads in, or `undefined` for "every language".
57
+ *
58
+ * `undefined` is also what an application in one language answers, and what a query
59
+ * outside any context answers — a migration, a script, a test. Both mean the same
60
+ * thing here: no language filter, which is exactly what those reads did before §131.
61
+ */
62
+ const readsIn = () => {
63
+ if (!state.table().translatable)
64
+ return undefined;
65
+ if (state.locale === 'all')
66
+ return undefined;
67
+ if (state.locale !== 'context')
68
+ return state.locale.code;
69
+ return currentContext()?.locale;
70
+ };
71
+ /**
72
+ * Reading one language, and what counts as being written in it.
73
+ *
74
+ * The default language matches rows marked with it *and* rows marked with nothing:
75
+ * a row written before the deployment named any languages is in the language it was
76
+ * written in, which is the default. Without that, adding `locales` to a project that
77
+ * already had content would make all of it vanish (`UNSPECIFIED_LOCALE`).
78
+ */
79
+ const localeCondition = () => {
80
+ const code = readsIn();
81
+ if (code === undefined)
82
+ return [];
83
+ return code === currentContext()?.locales?.defaultLocale
84
+ ? [comparison('locale', 'in', [code, UNSPECIFIED_LOCALE])]
85
+ : [comparison('locale', '=', code)];
86
+ };
87
+ /**
88
+ * The state a `where(query => …)` group is built in.
89
+ *
90
+ * Empty conditions, and neither of the two the query adds by itself: a group is the
91
+ * caller's own parentheses and nothing else. `toAst()` is what the group is taken
92
+ * from, and it puts the soft-delete and language filters at the front — so a nested
93
+ * `orWhere` produced `(deletedAt is null AND a) OR b`, which is not the parentheses
94
+ * anybody wrote. It was invisible while the outer query repeated both conditions
95
+ * anyway; the fallback rewrites the language one, and then it is not invisible.
96
+ *
97
+ * Expressed as `trashed: 'with'` and `locale: 'all'` rather than as a third flag,
98
+ * because those are exactly what "add neither" already means.
99
+ */
100
+ const nestedState = () => ({
101
+ ...state,
102
+ where: [],
103
+ trashed: 'with',
104
+ locale: 'all',
105
+ });
106
+ const softDeleteCondition = () => {
107
+ const column = state.table().softDeleteColumn;
108
+ if (column === undefined || state.trashed === 'with')
109
+ return [];
110
+ if (state.trashed === 'only')
111
+ return [comparison(column, 'is not null')];
112
+ return [comparison(column, 'is null')];
113
+ };
114
+ /**
115
+ * The shorthand's default column, or a message that names the problem.
116
+ *
117
+ * Without this the query reached the adapter and came back as "No column is mapped
118
+ * for createdAt", which says nothing about `latest()` having filled it in.
119
+ */
120
+ const timeColumn = (field) => {
121
+ if (field !== undefined)
122
+ return field;
123
+ const table = state.table();
124
+ if (!table.columns.some((column) => column.name === 'createdAt')) {
125
+ throw new AssemoraError('INVALID_QUERY', `"${table.name}" has no createdAt column, so latest() and oldest() need one named: latest('publishedAt')`, { status: 400 });
126
+ }
127
+ return 'createdAt';
128
+ };
129
+ const toAst = () => ({
130
+ model: state.table().name,
131
+ operation: 'select',
132
+ where: [...softDeleteCondition(), ...localeCondition(), ...state.where],
133
+ order: state.order,
134
+ with: state.load,
135
+ ...(state.limit === undefined ? {} : { limit: state.limit }),
136
+ ...(state.offset === undefined ? {} : { offset: state.offset }),
137
+ });
138
+ /**
139
+ * The query that actually runs, with the fallback folded into it (SPEC.md §131).
140
+ *
141
+ * One query and not two, because `order`, `limit` and `offset` decide what a page
142
+ * *is*: two result sets appended would put every untranslated row after every
143
+ * translated one, and page two of a menu would be a different menu.
144
+ *
145
+ * It costs one extra read to build. The rows already written in this language are
146
+ * fetched first — under the caller's own `where`, with no order and no limit — and
147
+ * what comes back is the set of *groups* that need no fallback. The query then asks
148
+ * for those rows, plus the default-language row of every group not among them.
149
+ *
150
+ * The alternative is `distinct on` or a window function, which is a change to the
151
+ * Query AST every adapter would have to implement, and ADR-0013 is why that is not a
152
+ * thing to do lightly. This uses `in` and `not in`, which every adapter already
153
+ * agrees about.
154
+ */
155
+ const astToRun = async () => {
156
+ const base = toAst();
157
+ const code = readsIn();
158
+ const fallbackTo = currentContext()?.locales?.defaultLocale;
159
+ if (!state.fallback ||
160
+ code === undefined ||
161
+ fallbackTo === undefined ||
162
+ // The default language is where a fallback would come *from*. Nothing to do.
163
+ code === fallbackTo) {
164
+ return base;
165
+ }
166
+ const key = state.table().primaryKey;
167
+ // Deliberately without the caller's `order`, `limit` and `offset`: this asks which
168
+ // groups are already written in this language, and a page of that answer would
169
+ // leave the rest of them falling back when they should not.
170
+ const { limit: _limit, offset: _offset, ...whole } = base;
171
+ const written = await execute({ ...whole, order: [], with: [] }, { table: state.table(), related: runtime.related() });
172
+ /**
173
+ * What each row translates, or itself where it translates nothing.
174
+ *
175
+ * A row written directly in a language, with no original behind it, is its own
176
+ * group — otherwise an article first written in Russian would be answered twice.
177
+ */
178
+ const covered = written.map((row) => row.translationOf ?? row[key]);
179
+ return {
180
+ ...base,
181
+ where: [
182
+ ...softDeleteCondition(),
183
+ ...state.where,
184
+ /**
185
+ * A combinator says how a condition joins the one before it, not how its own
186
+ * children join each other — which is why `orComparison` exists at all. So the
187
+ * outer group joins the caller's `where` with `and`, and the inner group joins
188
+ * its sibling with `or`.
189
+ */
190
+ group([
191
+ comparison('locale', '=', code),
192
+ group([
193
+ // The same rule as `localeCondition`: the default language is also where
194
+ // the rows that name no language belong.
195
+ comparison('locale', 'in', [fallbackTo, UNSPECIFIED_LOCALE]),
196
+ ...(covered.length === 0 ? [] : [comparison(key, 'not in', covered)]),
197
+ ], 'or'),
198
+ ], 'and'),
199
+ ],
200
+ };
201
+ };
202
+ const run = async () => {
203
+ const rows = await execute(await astToRun(), {
204
+ table: state.table(),
205
+ related: runtime.related(),
206
+ });
207
+ return rows.map(runtime.hydrate);
208
+ };
209
+ const query = {
210
+ where(first, second, third) {
211
+ if (typeof first === 'function') {
212
+ const nested = first(createQuery(nestedState(), runtime));
213
+ return withCondition(group(nested.toAst().where));
214
+ }
215
+ if (typeof first === 'object' && first !== null) {
216
+ return Object.entries(first).reduce((accumulated, [field, value]) => accumulated.where(field, value), createQuery(state, runtime));
217
+ }
218
+ return third === undefined
219
+ ? withCondition(comparison(String(first), '=', second))
220
+ : withCondition(comparison(String(first), second, third));
221
+ },
222
+ orWhere(first, second, third) {
223
+ if (typeof first === 'function') {
224
+ const nested = first(createQuery(nestedState(), runtime));
225
+ return withCondition(group(nested.toAst().where, 'or'));
226
+ }
227
+ return third === undefined
228
+ ? withCondition(orComparison(String(first), '=', second))
229
+ : withCondition(orComparison(String(first), second, third));
230
+ },
231
+ whereIn: (field, values) => withCondition(comparison(field, 'in', values)),
232
+ whereNotIn: (field, values) => withCondition(comparison(field, 'not in', values)),
233
+ whereNull: (field) => withCondition(comparison(field, 'is null')),
234
+ whereNotNull: (field) => withCondition(comparison(field, 'is not null')),
235
+ whereBetween: (field, range) => withCondition(comparison(field, 'between', range)),
236
+ whereLike: (field, pattern) => withCondition(comparison(field, 'like', pattern)),
237
+ whereJson: (field, path, value) => withCondition(jsonEquals(field, path === '' ? [] : path.split('.'), value)),
238
+ whereJsonContains: (field, fragment) => withCondition(jsonContains(field, fragment)),
239
+ whereJsonLike: (field, path, pattern) => withCondition(jsonLike(field, jsonPath(field, path), pattern)),
240
+ orWhereJsonLike: (field, path, pattern) => withCondition(jsonLike(field, jsonPath(field, path), pattern, 'or')),
241
+ orderBy: (field, direction = 'asc') => derive({ order: [...state.order, { field, direction }] }),
242
+ latest: (field) => derive({ order: [...state.order, { field: timeColumn(field), direction: 'desc' }] }),
243
+ oldest: (field) => derive({ order: [...state.order, { field: timeColumn(field), direction: 'asc' }] }),
244
+ limit: (count) => derive({ limit: countable('limit', count) }),
245
+ offset: (count) => derive({ offset: countable('offset', count) }),
246
+ take: (count) => derive({ limit: countable('take', count) }),
247
+ with: (...relations) => derive({ load: relations.map(parsePath).reduce(mergeLoads, state.load) }),
248
+ withTrashed: () => derive({ trashed: 'with' }),
249
+ onlyTrashed: () => derive({ trashed: 'only' }),
250
+ inLocale: (code) => derive({ locale: { code } }),
251
+ allLocales: () => derive({ locale: 'all' }),
252
+ withoutFallback: () => derive({ fallback: false }),
253
+ get: run,
254
+ async first() {
255
+ const rows = await createQuery({ ...state, limit: 1 }, runtime).get();
256
+ return rows[0] ?? null;
257
+ },
258
+ async firstOrFail() {
259
+ const row = await query.first();
260
+ if (row === null) {
261
+ const { NotFoundError } = await import('@assemora/core');
262
+ throw new NotFoundError(state.table().name);
263
+ }
264
+ return row;
265
+ },
266
+ async count() {
267
+ return execute({ ...(await astToRun()), operation: 'count', order: [], with: [] }, { table: state.table() });
268
+ },
269
+ async exists() {
270
+ return (await query.count()) > 0;
271
+ },
272
+ async paginate(page = 1, perPage = 20) {
273
+ const total = await query.count();
274
+ const data = await createQuery({ ...state, limit: perPage, offset: (page - 1) * perPage }, runtime).get();
275
+ return { data, total, page, perPage, lastPage: Math.max(1, Math.ceil(total / perPage)) };
276
+ },
277
+ async cursorPaginate(perPage = 20, after) {
278
+ const key = state.table().primaryKey;
279
+ const page = createQuery({
280
+ ...state,
281
+ where: after === undefined ? state.where : [...state.where, comparison(key, '>', after)],
282
+ order: [{ field: key, direction: 'asc' }],
283
+ limit: perPage + 1,
284
+ }, runtime);
285
+ const rows = await page.get();
286
+ const data = rows.slice(0, perPage);
287
+ const last = data.at(-1);
288
+ return {
289
+ data,
290
+ nextCursor: rows.length > perPage && last !== undefined ? last[key] : null,
291
+ };
292
+ },
293
+ toAst,
294
+ // biome-ignore lint/suspicious/noThenProperty: SPEC.md §20 asks a query to be awaitable without a terminal method — that is what PromiseLike means
295
+ then(onFulfilled, onRejected) {
296
+ return run().then(onFulfilled, onRejected);
297
+ },
298
+ };
299
+ for (const [name, scope] of Object.entries(runtime.scopes)) {
300
+ Object.defineProperty(query, name, {
301
+ enumerable: false,
302
+ value: () => scope(query),
303
+ });
304
+ }
305
+ return query;
306
+ };
307
+ //# sourceMappingURL=query.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"query.js","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Relations (SPEC.md §23, §24).
3
+ *
4
+ * A relation names the other side through a thunk, so two models may reference each
5
+ * other without either declaration depending on the other's type.
6
+ */
7
+ import type { RelationKind } from '@assemora/database';
8
+ /** The minimum a relation needs to know about its target. */
9
+ export type RelatedModel = {
10
+ readonly table: string;
11
+ readonly primaryKey: string;
12
+ };
13
+ export type RelationOptions = {
14
+ /** The column that holds the reference. Derived from the names when omitted. */
15
+ readonly foreignKey?: string;
16
+ /** The column it points at. The target's primary key when omitted. */
17
+ readonly ownerKey?: string;
18
+ /** The join table, for `belongsToMany` only. Named after both tables when omitted. */
19
+ readonly through?: string;
20
+ /**
21
+ * The join table column holding this model's key, for `belongsToMany` only.
22
+ * Derived from this model's table name when omitted — `users` gives `userId`.
23
+ *
24
+ * A model linked to itself has to name both: `userId` twice is not a link.
25
+ */
26
+ readonly foreignPivotKey?: string;
27
+ /** The join table column holding the target's key, for `belongsToMany` only. */
28
+ readonly relatedPivotKey?: string;
29
+ };
30
+ /**
31
+ * The kind is carried as a literal so a declaration can be told apart by it: only a
32
+ * `belongsToMany` has pivot verbs, and `PivotFields` decides that from the type alone
33
+ * (SPEC.md §24). Defaulted, so `Relation` still names any of them.
34
+ */
35
+ export type Relation<K extends RelationKind = RelationKind> = RelationOptions & {
36
+ readonly node: 'relation';
37
+ readonly kind: K;
38
+ readonly target: () => RelatedModel;
39
+ };
40
+ /**
41
+ * A thunk returning the model on the other side: `() => Post`.
42
+ *
43
+ * Declared as `unknown` on purpose. Two models normally reference each other, and
44
+ * any structural parameter type — `() => RelatedModel`, even `() => unknown` —
45
+ * makes TypeScript resolve the target while the declaration that needs it is still
46
+ * being computed, which is the circular-reference error that would force every
47
+ * mutual relation to be annotated by hand. Erasing the type at this one boundary
48
+ * keeps the API of SPEC.md §9 and §23 working; what the thunk returns is checked at
49
+ * runtime instead, with a clear error (see ADR-0010).
50
+ */
51
+ export type RelationTarget = unknown;
52
+ /** This model holds the foreign key. */
53
+ export declare const belongsTo: (target: RelationTarget, options?: RelationOptions) => Relation<'belongsTo'>;
54
+ /** The other model holds the foreign key, and there is at most one row. */
55
+ export declare const hasOne: (target: RelationTarget, options?: RelationOptions) => Relation<'hasOne'>;
56
+ /** The other model holds the foreign key, and there may be many rows. */
57
+ export declare const hasMany: (target: RelationTarget, options?: RelationOptions) => Relation<'hasMany'>;
58
+ /**
59
+ * Both sides are linked through a join table, and the instance carries the verbs that
60
+ * write to it: `user.roles.attach(id)` (SPEC.md §24).
61
+ */
62
+ export declare const belongsToMany: (target: RelationTarget, options?: RelationOptions) => Relation<'belongsToMany'>;
63
+ export declare const isRelation: (value: unknown) => value is Relation;
64
+ //# sourceMappingURL=relations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.d.ts","sourceRoot":"","sources":["../src/relations.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAEtD,6DAA6D;AAC7D,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC5B,CAAA;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,gFAAgF;IAChF,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,sFAAsF;IACtF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;;;;;OAKG;IACH,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;IACjC,gFAAgF;IAChF,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAClC,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,QAAQ,CAAC,CAAC,SAAS,YAAY,GAAG,YAAY,IAAI,eAAe,GAAG;IAC9E,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAA;IAChB,QAAQ,CAAC,MAAM,EAAE,MAAM,YAAY,CAAA;CACpC,CAAA;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,CAAA;AA0BpC,wCAAwC;AACxC,eAAO,MAAM,SAAS,WACZ,cAAc,YACZ,eAAe,KACxB,QAAQ,CAAC,WAAW,CAA2C,CAAA;AAElE,2EAA2E;AAC3E,eAAO,MAAM,MAAM,WAAY,cAAc,YAAY,eAAe,KAAG,QAAQ,CAAC,QAAQ,CACvD,CAAA;AAErC,yEAAyE;AACzE,eAAO,MAAM,OAAO,WAAY,cAAc,YAAY,eAAe,KAAG,QAAQ,CAAC,SAAS,CACxD,CAAA;AAEtC;;;GAGG;AACH,eAAO,MAAM,aAAa,WAChB,cAAc,YACZ,eAAe,KACxB,QAAQ,CAAC,eAAe,CAA+C,CAAA;AAE1E,eAAO,MAAM,UAAU,UAAW,OAAO,KAAG,KAAK,IAAI,QAC4C,CAAA"}
@@ -0,0 +1,26 @@
1
+ const asRelatedModel = (target) => {
2
+ if (typeof target !== 'function') {
3
+ throw new TypeError('A relation target must be a function returning the related model');
4
+ }
5
+ const resolved = target();
6
+ if (typeof resolved !== 'object' ||
7
+ resolved === null ||
8
+ typeof resolved.table !== 'string') {
9
+ throw new TypeError('A relation target must be a function returning the related model');
10
+ }
11
+ return resolved;
12
+ };
13
+ const relation = (kind, target, options = {}) => ({ node: 'relation', kind, target: () => asRelatedModel(target), ...options });
14
+ /** This model holds the foreign key. */
15
+ export const belongsTo = (target, options) => relation('belongsTo', target, options);
16
+ /** The other model holds the foreign key, and there is at most one row. */
17
+ export const hasOne = (target, options) => relation('hasOne', target, options);
18
+ /** The other model holds the foreign key, and there may be many rows. */
19
+ export const hasMany = (target, options) => relation('hasMany', target, options);
20
+ /**
21
+ * Both sides are linked through a join table, and the instance carries the verbs that
22
+ * write to it: `user.roles.attach(id)` (SPEC.md §24).
23
+ */
24
+ export const belongsToMany = (target, options) => relation('belongsToMany', target, options);
25
+ export const isRelation = (value) => typeof value === 'object' && value !== null && value.node === 'relation';
26
+ //# sourceMappingURL=relations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"relations.js","sourceRoot":"","sources":["../src/relations.ts"],"names":[],"mappings":"AAwDA,MAAM,cAAc,GAAG,CAAC,MAAsB,EAAgB,EAAE;IAC9D,IAAI,OAAO,MAAM,KAAK,UAAU,EAAE,CAAC;QACjC,MAAM,IAAI,SAAS,CAAC,kEAAkE,CAAC,CAAA;IACzF,CAAC;IAED,MAAM,QAAQ,GAAI,MAAwB,EAAE,CAAA;IAE5C,IACE,OAAO,QAAQ,KAAK,QAAQ;QAC5B,QAAQ,KAAK,IAAI;QACjB,OAAQ,QAAyB,CAAC,KAAK,KAAK,QAAQ,EACpD,CAAC;QACD,MAAM,IAAI,SAAS,CAAC,kEAAkE,CAAC,CAAA;IACzF,CAAC;IAED,OAAO,QAAwB,CAAA;AACjC,CAAC,CAAA;AAED,MAAM,QAAQ,GAAG,CACf,IAAO,EACP,MAAsB,EACtB,OAAO,GAAoB,EAAE,EAChB,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,GAAG,OAAO,EAAE,CAAC,CAAA;AAEhG,wCAAwC;AACxC,MAAM,CAAC,MAAM,SAAS,GAAG,CACvB,MAAsB,EACtB,OAAyB,EACF,EAAE,CAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AAElE,2EAA2E;AAC3E,MAAM,CAAC,MAAM,MAAM,GAAG,CAAC,MAAsB,EAAE,OAAyB,EAAsB,EAAE,CAC9F,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AAErC,yEAAyE;AACzE,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,MAAsB,EAAE,OAAyB,EAAuB,EAAE,CAChG,QAAQ,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AAEtC;;;GAGG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAC3B,MAAsB,EACtB,OAAyB,EACE,EAAE,CAAC,QAAQ,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,CAAA;AAE1E,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAc,EAAqB,EAAE,CAC9D,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAK,KAA2B,CAAC,IAAI,KAAK,UAAU,CAAA"}
@@ -0,0 +1,28 @@
1
+ import { type TransactionPort } from '@assemora/core';
2
+ import type { DatabaseAdapter, DatabaseContext, QueryAst } from '@assemora/database';
3
+ /** Binds the adapter every model will use. Called once, when the application boots. */
4
+ export declare const useAdapter: (adapter: DatabaseAdapter) => void;
5
+ export declare const clearAdapter: () => void;
6
+ export declare const currentAdapter: () => DatabaseAdapter;
7
+ /**
8
+ * Every statement the data layer runs (SPEC.md §88).
9
+ *
10
+ * The query builder, a model's `find()`, an instance's writes and the pivot verbs all
11
+ * reach the adapter through here rather than through `currentAdapter().execute`, so
12
+ * there is one place that knows how long a query took — and one place that would have
13
+ * to be edited again for anything else the data layer ever wants to measure.
14
+ *
15
+ * A function, and deliberately not a wrapped adapter. `currentAdapter()` hands the
16
+ * real adapter to the CLI and to `applySchema()`, which reach for methods only the
17
+ * PostgreSQL one has; a wrapper would either lose them, taking `db:migrate` with them,
18
+ * or forward them, and be a second definition of a contract that already has one
19
+ * (SPEC.md §31, ADR-0013).
20
+ */
21
+ export declare const execute: <T>(query: QueryAst, context: DatabaseContext) => Promise<T>;
22
+ /**
23
+ * Runs an operation inside a transaction. Everything the operation awaits sees the
24
+ * transactional adapter without being told about it.
25
+ */
26
+ export declare const transaction: <T>(operation: () => Promise<T>) => Promise<T>;
27
+ export declare const dataTransactions: () => TransactionPort;
28
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"AAUA,OAAO,EAAsB,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAA;AACzE,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAQpF,uFAAuF;AACvF,eAAO,MAAM,UAAU,YAAa,eAAe,KAAG,IAErD,CAAA;AAED,eAAO,MAAM,YAAY,QAAO,IAE/B,CAAA;AAED,eAAO,MAAM,cAAc,QAAO,eAUjC,CAAA;AAED;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,OAAO,GAAU,CAAC,SAAS,QAAQ,WAAW,eAAe,KAAG,OAAO,CAAC,CAAC,CAiBrF,CAAA;AAyBD;;;GAGG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,aAAa,MAAM,OAAO,CAAC,CAAC,CAAC,KAAG,OAAO,CAAC,CAAC,CAkBrE,CAAA;AAwDD,eAAO,MAAM,gBAAgB,QAAO,eAIlC,CAAA"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Where the data layer finds its adapter, and how it reaches it (SPEC.md §33, §88).
3
+ *
4
+ * The current adapter and the current transaction travel through AsyncLocalStorage,
5
+ * so a developer never passes `tx` by hand. `execute()` below is the one seam every
6
+ * query in this package runs through, which is what makes a query measurable without
7
+ * anything having to wrap the adapter itself.
8
+ */
9
+ import { AsyncLocalStorage } from 'node:async_hooks';
10
+ import { ConfigurationError } from '@assemora/core';
11
+ import { recordQuery } from './slow-queries.js';
12
+ let ambient;
13
+ const scoped = new AsyncLocalStorage();
14
+ /** Binds the adapter every model will use. Called once, when the application boots. */
15
+ export const useAdapter = (adapter) => {
16
+ ambient = adapter;
17
+ };
18
+ export const clearAdapter = () => {
19
+ ambient = undefined;
20
+ };
21
+ export const currentAdapter = () => {
22
+ const adapter = scoped.getStore() ?? ambient;
23
+ if (adapter === undefined) {
24
+ throw new ConfigurationError('No database adapter is registered. Call useAdapter() before querying.');
25
+ }
26
+ return adapter;
27
+ };
28
+ /**
29
+ * Every statement the data layer runs (SPEC.md §88).
30
+ *
31
+ * The query builder, a model's `find()`, an instance's writes and the pivot verbs all
32
+ * reach the adapter through here rather than through `currentAdapter().execute`, so
33
+ * there is one place that knows how long a query took — and one place that would have
34
+ * to be edited again for anything else the data layer ever wants to measure.
35
+ *
36
+ * A function, and deliberately not a wrapped adapter. `currentAdapter()` hands the
37
+ * real adapter to the CLI and to `applySchema()`, which reach for methods only the
38
+ * PostgreSQL one has; a wrapper would either lose them, taking `db:migrate` with them,
39
+ * or forward them, and be a second definition of a contract that already has one
40
+ * (SPEC.md §31, ADR-0013).
41
+ */
42
+ export const execute = async (query, context) => {
43
+ const startedAt = performance.now();
44
+ try {
45
+ const answer = await currentAdapter().execute(query, context);
46
+ recordQuery(query, performance.now() - startedAt, answer);
47
+ return answer;
48
+ }
49
+ catch (error) {
50
+ // Timed either way. A query that ran for four seconds and then failed is the one
51
+ // most worth knowing about, and a log that goes quiet exactly when the database
52
+ // starts refusing is the wrong log.
53
+ recordQuery(query, performance.now() - startedAt);
54
+ throw error;
55
+ }
56
+ };
57
+ /**
58
+ * Work waiting for the OUTERMOST transaction to commit (ADR-0023).
59
+ *
60
+ * The store is the list itself, and a nested `transaction()` never replaces it — so
61
+ * every layer inside registers against the one commit that actually makes rows
62
+ * durable. A savepoint is not that commit: everything written under it is still the
63
+ * caller's to undo.
64
+ */
65
+ const waiting = new AsyncLocalStorage();
66
+ const drain = async (work) => {
67
+ for (const item of work) {
68
+ try {
69
+ await item();
70
+ }
71
+ catch {
72
+ // Swallowed on purpose, and documented on `TransactionPort.afterCommit`: this
73
+ // runs after the commit, so there is no caller left to reject to, and one
74
+ // registration's failure must not cancel the next one's. Whoever registers
75
+ // reports its own failure — the command bus logs the job it could not queue.
76
+ }
77
+ }
78
+ };
79
+ /**
80
+ * Runs an operation inside a transaction. Everything the operation awaits sees the
81
+ * transactional adapter without being told about it.
82
+ */
83
+ export const transaction = (operation) => {
84
+ const adapter = currentAdapter();
85
+ const run = () => adapter.transaction(() => scoped.run(adapter, operation));
86
+ // Nested: the caller owns the commit, so anything registered inside keeps waiting
87
+ // for theirs.
88
+ if (waiting.getStore() !== undefined)
89
+ return run();
90
+ const work = [];
91
+ // `.then` is attached outside `waiting.run`, so the work drains with no transaction
92
+ // in scope — a job dispatched by after-commit work has nothing left to wait for,
93
+ // and would otherwise be appended to a list that is already being read.
94
+ return waiting.run(work, run).then(async (value) => {
95
+ await drain(work);
96
+ return value;
97
+ });
98
+ };
99
+ /**
100
+ * Holds `work` until the outermost transaction commits, and drops it if that
101
+ * transaction is undone. With none open, "after commit" is "now".
102
+ */
103
+ const afterCommit = (work) => {
104
+ const pending = waiting.getStore();
105
+ if (pending === undefined)
106
+ return work();
107
+ pending.push(work);
108
+ return Promise.resolve();
109
+ };
110
+ /**
111
+ * The transaction stage of the command pipeline (SPEC.md §14, ADR-0008).
112
+ *
113
+ * `core` declares the port and cannot reach a database; this is the implementation
114
+ * the data layer registers, so a handler that writes several rows either commits all
115
+ * of them or none.
116
+ */
117
+ /** Nothing else can be thrown by accident, so nothing else can be mistaken for it. */
118
+ const ROLLBACK = Symbol('assemora.rollback');
119
+ /**
120
+ * Runs the operation inside a transaction that is always undone, and still answers
121
+ * with what it returned (SPEC.md §73).
122
+ *
123
+ * Rejecting is the only way to make an adapter roll back — that is how both of them
124
+ * implement it — so the value is carried out past the rejection rather than through
125
+ * it. A real failure inside the operation is rethrown untouched.
126
+ */
127
+ const rollingBack = async (operation) => {
128
+ const carried = [];
129
+ try {
130
+ await transaction(async () => {
131
+ carried.push(await operation());
132
+ throw ROLLBACK;
133
+ });
134
+ }
135
+ catch (error) {
136
+ if (error !== ROLLBACK)
137
+ throw error;
138
+ }
139
+ const [value] = carried;
140
+ if (carried.length === 0) {
141
+ throw new ConfigurationError('The transaction was undone before the operation answered');
142
+ }
143
+ return value;
144
+ };
145
+ export const dataTransactions = () => ({
146
+ run: (operation, options) => options?.rollback === true ? rollingBack(operation) : transaction(operation),
147
+ afterCommit,
148
+ });
149
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}