@assemora/database 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.
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `@assemora/database` — the neutral contract between the data layer and a database.
3
+ *
4
+ * It owns the Query AST (SPEC.md §30), the adapter interface (SPEC.md §31) and the
5
+ * schema diff a migration generator works from (SPEC.md §34), and nothing in it is
6
+ * specific to any engine. `@assemora/database-postgres` implements the contract with
7
+ * Drizzle inside; the in-memory adapter here implements the same contract for tests
8
+ * and for the phases where PostgreSQL does not exist yet.
9
+ */
10
+ export type { ColumnDescriptor, ColumnType, DatabaseAdapter, DatabaseContext, DatabaseSchema, RelationDescriptor, RelationKind, TableDescriptor, } from './adapter.js';
11
+ export { LOCALE_COLUMN, TRANSLATION_OF_COLUMN, UNSPECIFIED_LOCALE } from './adapter.js';
12
+ export { isSchemaNotApplied, SCHEMA_NOT_APPLIED, schemaNotApplied, } from './errors.js';
13
+ export { joinTableDescriptor, type PivotAddress, pivotAddress, withJoinTables, } from './join-table.js';
14
+ export { createMemoryAdapter, type MemoryAdapter, type Row } from './memory.js';
15
+ export { type Combinator, type Comparison, type ComparisonOperator, type Condition, type ConditionGroup, comparison, emptyQuery, group, type JsonComparison, jsonContains, jsonEquals, jsonLike, type Order, orComparison, type QueryAst, type QueryOperation, type RelationLoad, type SortDirection, } from './query-ast.js';
16
+ export { type ChangeRisk, type ColumnAdded, type ColumnEnumChanged, type ColumnNullabilityChanged, type ColumnRemoved, type ColumnTypeChanged, type ColumnUniquenessChanged, describeChange, diffSchema, type ForeignKeyAdded, type ForeignKeyRemoved, type IndexAdded, type IndexRemoved, isDestructive, mayFailOnExistingRows, type PrimaryKeyMoved, type SchemaChange, type SchemaDiff, type TableAdded, type TableRemoved, } from './schema-diff.js';
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,YAAY,EACV,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,eAAe,EACf,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,eAAe,GAChB,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACvF,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,mBAAmB,EACnB,KAAK,YAAY,EACjB,YAAY,EACZ,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,KAAK,GAAG,EAAE,MAAM,aAAa,CAAA;AAC/E,OAAO,EACL,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,SAAS,EACd,KAAK,cAAc,EACnB,UAAU,EACV,UAAU,EACV,KAAK,EACL,KAAK,cAAc,EACnB,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,KAAK,KAAK,EACV,YAAY,EACZ,KAAK,QAAQ,EACb,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,aAAa,GACnB,MAAM,gBAAgB,CAAA;AACvB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,WAAW,EAChB,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,EAC5B,cAAc,EACd,UAAU,EACV,KAAK,eAAe,EACpB,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,aAAa,EACb,qBAAqB,EACrB,KAAK,eAAe,EACpB,KAAK,YAAY,EACjB,KAAK,UAAU,EACf,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,kBAAkB,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * `@assemora/database` — the neutral contract between the data layer and a database.
3
+ *
4
+ * It owns the Query AST (SPEC.md §30), the adapter interface (SPEC.md §31) and the
5
+ * schema diff a migration generator works from (SPEC.md §34), and nothing in it is
6
+ * specific to any engine. `@assemora/database-postgres` implements the contract with
7
+ * Drizzle inside; the in-memory adapter here implements the same contract for tests
8
+ * and for the phases where PostgreSQL does not exist yet.
9
+ */
10
+ export { LOCALE_COLUMN, TRANSLATION_OF_COLUMN, UNSPECIFIED_LOCALE } from './adapter.js';
11
+ export { isSchemaNotApplied, SCHEMA_NOT_APPLIED, schemaNotApplied, } from './errors.js';
12
+ export { joinTableDescriptor, pivotAddress, withJoinTables, } from './join-table.js';
13
+ export { createMemoryAdapter } from './memory.js';
14
+ export { comparison, emptyQuery, group, jsonContains, jsonEquals, jsonLike, orComparison, } from './query-ast.js';
15
+ export { describeChange, diffSchema, isDestructive, mayFailOnExistingRows, } from './schema-diff.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAYH,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AACvF,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,aAAa,CAAA;AACpB,OAAO,EACL,mBAAmB,EAEnB,YAAY,EACZ,cAAc,GACf,MAAM,iBAAiB,CAAA;AACxB,OAAO,EAAE,mBAAmB,EAAgC,MAAM,aAAa,CAAA;AAC/E,OAAO,EAML,UAAU,EACV,UAAU,EACV,KAAK,EAEL,YAAY,EACZ,UAAU,EACV,QAAQ,EAER,YAAY,GAKb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAQL,cAAc,EACd,UAAU,EAKV,aAAa,EACb,qBAAqB,GAMtB,MAAM,kBAAkB,CAAA"}
@@ -0,0 +1,66 @@
1
+ import type { RelationDescriptor, TableDescriptor } from './adapter.js';
2
+ /**
3
+ * The join table a `belongsToMany` is stored in (SPEC.md §23).
4
+ *
5
+ * Named by `through` when the relation declares one, and by the two table names
6
+ * otherwise — sorted, so both sides of a mutual relation name the same table.
7
+ *
8
+ * ```ts
9
+ * joinTableDescriptor(User.descriptor, rolesRelation, Role.descriptor)
10
+ * // { name: 'roles_users', columns: [roleId, userId], uniqueTogether: [['roleId', 'userId']] }
11
+ * ```
12
+ */
13
+ export declare const joinTableDescriptor: (owner: TableDescriptor, relation: RelationDescriptor, target?: TableDescriptor) => TableDescriptor;
14
+ /**
15
+ * Where one row's links live, and what identifies them.
16
+ *
17
+ * Everything the pivot verbs of SPEC.md §24 need, and nothing an adapter has to learn:
18
+ * `attach` is an insert of the two columns, `detach` a delete of the two conditions,
19
+ * `sync` both inside one transaction — all of it ordinary Query AST.
20
+ */
21
+ export type PivotAddress = {
22
+ /** The join table, ready to be handed to `DatabaseContext.table`. */
23
+ readonly table: TableDescriptor;
24
+ /** The column holding the owner's key. */
25
+ readonly ownerColumn: string;
26
+ /** The column holding the related row's key. */
27
+ readonly relatedColumn: string;
28
+ /** What the owner's row stores in the key the link points at. */
29
+ readonly ownerValue: unknown;
30
+ };
31
+ /**
32
+ * Addresses the join table for one row (SPEC.md §24).
33
+ *
34
+ * ```ts
35
+ * const pivot = pivotAddress(User.descriptor, roles, user.toJSON(), Role.descriptor)
36
+ *
37
+ * await adapter.execute(
38
+ * {
39
+ * ...emptyQuery(pivot.table.name, 'insert'),
40
+ * data: { [pivot.ownerColumn]: pivot.ownerValue, [pivot.relatedColumn]: roleId },
41
+ * },
42
+ * { table: pivot.table },
43
+ * )
44
+ * ```
45
+ */
46
+ export declare const pivotAddress: (owner: TableDescriptor, relation: RelationDescriptor, row: Readonly<Record<string, unknown>>, target?: TableDescriptor) => PivotAddress;
47
+ /**
48
+ * The tables a schema really has: the declared ones, plus a join table per
49
+ * `belongsToMany` (SPEC.md §23, §34).
50
+ *
51
+ * Idempotent, and that is what makes it safe to call anywhere. A join table declares
52
+ * no `belongsToMany` of its own, so expanding an expanded schema adds nothing — a
53
+ * snapshot that already holds the join table compares clean against a registry that
54
+ * derives it.
55
+ *
56
+ * A model declared for a table a relation derives is refused, rather than allowed to
57
+ * win. Only the DDL would ever read it: the pivot verbs of SPEC.md §24 write the two
58
+ * derived columns and nothing else, so a declared pivot carrying a surrogate key or a
59
+ * `joinedAt` is a table `attach` cannot fill, and one carrying exactly the two keys is
60
+ * a second descriptor for a name the adapter already builds. Keeping the declaration
61
+ * for the DDL and deriving the writes is the disagreement this whole file exists to
62
+ * prevent — a pivot with columns of its own is a model like any other, and it is
63
+ * declared with two `belongsTo` relations rather than with `through`.
64
+ */
65
+ export declare const withJoinTables: (tables: readonly TableDescriptor[]) => readonly TableDescriptor[];
66
+ //# sourceMappingURL=join-table.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join-table.d.ts","sourceRoot":"","sources":["../src/join-table.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAGV,kBAAkB,EAClB,eAAe,EAChB,MAAM,cAAc,CAAA;AAmKrB;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,UACvB,eAAe,YACZ,kBAAkB,WACnB,eAAe,KACvB,eAAwF,CAAA;AAE3F;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,qEAAqE;IACrE,QAAQ,CAAC,KAAK,EAAE,eAAe,CAAA;IAC/B,0CAA0C;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;IAC5B,gDAAgD;IAChD,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAA;IAC9B,iEAAiE;IACjE,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAA;CAC7B,CAAA;AAED;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,UAChB,eAAe,YACZ,kBAAkB,OACvB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,WAC7B,eAAe,KACvB,YAoBF,CAAA;AAoCD;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,cAAc,WAAY,SAAS,eAAe,EAAE,KAAG,SAAS,eAAe,EAyD3F,CAAA"}
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Join tables (SPEC.md §23, §24).
3
+ *
4
+ * `belongsToMany` is the one relation that stores nothing on either table it links:
5
+ * the pairs live in a third table no model declares. Three subsystems need that
6
+ * table — the data layer writes the pivot verbs to it, the DDL creates it, the diff
7
+ * notices it arriving — and deriving it three times is how they come to disagree. So
8
+ * it is derived once, here, as an ordinary `TableDescriptor`. Everything that already
9
+ * knows what to do with a table knows what to do with this one, and a pivot write is
10
+ * an ordinary insert through the Query AST rather than an operation of its own
11
+ * (ADR-0001, ADR-0013).
12
+ *
13
+ * The derivation is symmetric on purpose. Both sides of a mutual `belongsToMany`
14
+ * derive the same name, the same two columns in the same order and the same foreign
15
+ * keys, so a schema holding `users.roles` and `roles.users` holds one join table
16
+ * rather than two that disagree.
17
+ */
18
+ import { AssemoraError } from '@assemora/core';
19
+ /**
20
+ * `users` → `userId`. Crude on purpose, and the same rule a `hasMany` foreign key
21
+ * follows, so a join column is named the way every other reference to that table is.
22
+ * `foreignPivotKey` and `relatedPivotKey` are there for where it does not fit.
23
+ */
24
+ const pivotColumnFor = (table) => `${table.endsWith('s') ? table.slice(0, -1) : table}Id`;
25
+ /**
26
+ * What a key column holds when the descriptor holding it is not to hand.
27
+ *
28
+ * `model()` names `id` as the primary key of a table that marks no column primary,
29
+ * and a primary key nobody typed is a `uuid` in every starter (SPEC.md §17).
30
+ */
31
+ const DEFAULT_KEY = 'id';
32
+ const DEFAULT_KEY_TYPE = 'uuid';
33
+ const typeOfKey = (table, key) => table?.columns.find((column) => column.name === key)?.type ?? DEFAULT_KEY_TYPE;
34
+ const assertBelongsToMany = (owner, relation) => {
35
+ if (relation.kind === 'belongsToMany')
36
+ return;
37
+ throw new AssemoraError('NOT_A_JOIN_TABLE', `"${owner.name}.${relation.name}" is a ${relation.kind} relation, and only belongsToMany is stored in a join table`, { status: 500 });
38
+ };
39
+ /**
40
+ * The two ends of the link, in the order they were declared in.
41
+ *
42
+ * `ownerKey` means here what it means for every other kind — the column on the owner
43
+ * that the reference points at — so the only thing `belongsToMany` needs of its own is
44
+ * a name for each of the two join columns.
45
+ *
46
+ * `target` is optional because a relation names the other table but does not carry it.
47
+ * Passing it is what makes the related column take the type of the key it holds;
48
+ * `withJoinTables` always does, and a caller with one table in its hand gets the
49
+ * default rather than an error.
50
+ */
51
+ const sidesOf = (owner, relation, target) => {
52
+ assertBelongsToMany(owner, relation);
53
+ const relatedKey = target?.primaryKey ?? DEFAULT_KEY;
54
+ const sides = {
55
+ owner: {
56
+ table: owner.name,
57
+ column: relation.foreignPivotKey ?? pivotColumnFor(owner.name),
58
+ key: relation.ownerKey,
59
+ type: typeOfKey(owner, relation.ownerKey),
60
+ },
61
+ related: {
62
+ table: relation.target,
63
+ column: relation.relatedPivotKey ?? pivotColumnFor(relation.target),
64
+ key: relatedKey,
65
+ type: typeOfKey(target, relatedKey),
66
+ },
67
+ };
68
+ if (sides.owner.column === sides.related.column) {
69
+ throw new AssemoraError('INVALID_JOIN_TABLE', `The join table for "${owner.name}.${relation.name}" would hold two columns named "${sides.owner.column}", ` +
70
+ 'so the two sides of the link cannot be told apart. Name them with `foreignPivotKey` and ' +
71
+ '`relatedPivotKey` — a relation whose target is its own table always has to.', { status: 500 });
72
+ }
73
+ return sides;
74
+ };
75
+ /**
76
+ * The two ends in a canonical order, which is what makes the derivation symmetric.
77
+ *
78
+ * Sorted by table name, and by column name where one table links to itself: both
79
+ * declarations of a mutual relation then produce the identical descriptor, down to the
80
+ * order of the columns, so `withJoinTables` can recognise them as one table instead of
81
+ * reporting two.
82
+ *
83
+ * Compared by code point rather than by `localeCompare`, because the name this decides
84
+ * is written into a migration and read back on another machine. A locale is not a
85
+ * shared fact, and one that orders two tables the other way round would rename a table
86
+ * that nobody touched.
87
+ */
88
+ const ordered = (first, second) => {
89
+ const inOrder = first.table === second.table ? first.column <= second.column : first.table < second.table;
90
+ return inOrder ? [first, second] : [second, first];
91
+ };
92
+ const pivotColumn = (side) => ({
93
+ name: side.column,
94
+ type: side.type,
95
+ isPrimary: false,
96
+ isNullable: false,
97
+ // Neither key is unique on its own — a user holds many roles and a role many users.
98
+ // Only the pair is, and `uniqueTogether` on the table is what says so.
99
+ isUnique: false,
100
+ // The foreign key below brings the index with it: a generator indexes the
101
+ // referencing side of every relation it creates, and a second index on the same
102
+ // column would only cost writes.
103
+ isIndexed: false,
104
+ hasDefault: false,
105
+ });
106
+ /**
107
+ * A `belongsTo` per side, so the join table is created with real foreign keys.
108
+ *
109
+ * Without them a deleted role leaves its pivot rows behind, and the next load of
110
+ * `user.roles` returns ids pointing at nothing. It also means every generator that
111
+ * already turns a `belongsTo` into a constraint and an index needs nothing new.
112
+ *
113
+ * The relation is named after its column rather than after the table it points at: a
114
+ * table linked to itself has two sides with the same target, and a descriptor holding
115
+ * two relations of one name is one that nothing can address.
116
+ */
117
+ const pivotRelation = (side) => ({
118
+ name: side.column,
119
+ kind: 'belongsTo',
120
+ target: side.table,
121
+ foreignKey: side.column,
122
+ ownerKey: side.key,
123
+ });
124
+ const describeJoinTable = (sides, through) => {
125
+ const [first, second] = ordered(sides.owner, sides.related);
126
+ return {
127
+ name: through ?? `${first.table}_${second.table}`,
128
+ // A join table has no key of its own: the pair is its identity, and `primaryKey`
129
+ // names a single column. Naming half the pair would tell a generator to make one
130
+ // side unique, which is the opposite of what a link table is for.
131
+ primaryKey: '',
132
+ columns: [pivotColumn(first), pivotColumn(second)],
133
+ relations: [pivotRelation(first), pivotRelation(second)],
134
+ uniqueTogether: [[first.column, second.column]],
135
+ };
136
+ };
137
+ /**
138
+ * The join table a `belongsToMany` is stored in (SPEC.md §23).
139
+ *
140
+ * Named by `through` when the relation declares one, and by the two table names
141
+ * otherwise — sorted, so both sides of a mutual relation name the same table.
142
+ *
143
+ * ```ts
144
+ * joinTableDescriptor(User.descriptor, rolesRelation, Role.descriptor)
145
+ * // { name: 'roles_users', columns: [roleId, userId], uniqueTogether: [['roleId', 'userId']] }
146
+ * ```
147
+ */
148
+ export const joinTableDescriptor = (owner, relation, target) => describeJoinTable(sidesOf(owner, relation, target), relation.through);
149
+ /**
150
+ * Addresses the join table for one row (SPEC.md §24).
151
+ *
152
+ * ```ts
153
+ * const pivot = pivotAddress(User.descriptor, roles, user.toJSON(), Role.descriptor)
154
+ *
155
+ * await adapter.execute(
156
+ * {
157
+ * ...emptyQuery(pivot.table.name, 'insert'),
158
+ * data: { [pivot.ownerColumn]: pivot.ownerValue, [pivot.relatedColumn]: roleId },
159
+ * },
160
+ * { table: pivot.table },
161
+ * )
162
+ * ```
163
+ */
164
+ export const pivotAddress = (owner, relation, row, target) => {
165
+ const sides = sidesOf(owner, relation, target);
166
+ const ownerValue = row[sides.owner.key];
167
+ // A link to a row that was never stored points at nothing, and inserting it writes a
168
+ // pivot row no read can ever join. Refusing here catches it once for every caller.
169
+ if (ownerValue === undefined || ownerValue === null) {
170
+ throw new AssemoraError('UNSAVED_RECORD', `Cannot address "${owner.name}.${relation.name}": the row has no ${sides.owner.key}. Save it first.`, { status: 409 });
171
+ }
172
+ return {
173
+ table: describeJoinTable(sides, relation.through),
174
+ ownerColumn: sides.owner.column,
175
+ relatedColumn: sides.related.column,
176
+ ownerValue,
177
+ };
178
+ };
179
+ /** What two derivations of one join table have to agree on, as one sentence. */
180
+ const signatureOf = (table) => table.relations
181
+ .map((relation) => `${relation.foreignKey} -> ${relation.target}.${relation.ownerKey}`)
182
+ .join(', ');
183
+ /**
184
+ * Everything about a table that is not its name, written down in a fixed order.
185
+ *
186
+ * Two descriptors that agree on this describe the identical table. Used to recognise
187
+ * a derived join table handed back to `withJoinTables` — the expansion is idempotent,
188
+ * so the join table it added last time arrives among the declared ones and must not be
189
+ * mistaken for a model of it.
190
+ */
191
+ const shapeOf = (table) => [
192
+ table.primaryKey,
193
+ table.softDeleteColumn ?? '',
194
+ table.columns
195
+ .map((column) => `${column.name}:${column.type}:${column.isPrimary}:${column.isNullable}:` +
196
+ `${column.isUnique}:${column.isIndexed}:${column.hasDefault}`)
197
+ .join(','),
198
+ table.relations
199
+ .map((relation) => `${relation.name}:${relation.kind}:${relation.target}:${relation.foreignKey}:${relation.ownerKey}`)
200
+ .join(','),
201
+ (table.uniqueTogether ?? []).map((group) => group.join('+')).join(','),
202
+ ].join(' | ');
203
+ /**
204
+ * The tables a schema really has: the declared ones, plus a join table per
205
+ * `belongsToMany` (SPEC.md §23, §34).
206
+ *
207
+ * Idempotent, and that is what makes it safe to call anywhere. A join table declares
208
+ * no `belongsToMany` of its own, so expanding an expanded schema adds nothing — a
209
+ * snapshot that already holds the join table compares clean against a registry that
210
+ * derives it.
211
+ *
212
+ * A model declared for a table a relation derives is refused, rather than allowed to
213
+ * win. Only the DDL would ever read it: the pivot verbs of SPEC.md §24 write the two
214
+ * derived columns and nothing else, so a declared pivot carrying a surrogate key or a
215
+ * `joinedAt` is a table `attach` cannot fill, and one carrying exactly the two keys is
216
+ * a second descriptor for a name the adapter already builds. Keeping the declaration
217
+ * for the DDL and deriving the writes is the disagreement this whole file exists to
218
+ * prevent — a pivot with columns of its own is a model like any other, and it is
219
+ * declared with two `belongsTo` relations rather than with `through`.
220
+ */
221
+ export const withJoinTables = (tables) => {
222
+ const declared = new Map(tables.map((table) => [table.name, table]));
223
+ const derived = new Map();
224
+ for (const owner of tables) {
225
+ for (const relation of owner.relations) {
226
+ if (relation.kind !== 'belongsToMany')
227
+ continue;
228
+ const from = `${owner.name}.${relation.name}`;
229
+ const table = joinTableDescriptor(owner, relation, declared.get(relation.target));
230
+ const model = declared.get(table.name);
231
+ if (model !== undefined) {
232
+ // The expansion is idempotent, so the join table added by an earlier call
233
+ // arrives here among the declared tables. It is this derivation, not a model
234
+ // of it, and it is skipped exactly as it was before there was a rule.
235
+ if (shapeOf(model) === shapeOf(table))
236
+ continue;
237
+ throw new AssemoraError('DECLARED_JOIN_TABLE', `"${from}" derives the join table "${table.name}", and a model declares it too. ` +
238
+ 'A derived pivot holds the two key columns and nothing else, which is all `attach`, ' +
239
+ '`detach` and `sync` write — so a model of it is a table those verbs cannot fill. ' +
240
+ 'Either drop the model and let the relation derive the table, or keep the model, ' +
241
+ 'give it two `belongsTo` relations and address it as an ordinary table instead of ' +
242
+ 'through `belongsToMany`.', { status: 500 });
243
+ }
244
+ const existing = derived.get(table.name);
245
+ if (existing === undefined) {
246
+ derived.set(table.name, { table, from });
247
+ continue;
248
+ }
249
+ // Both sides of a mutual relation derive the same table from the same two model
250
+ // names, so a disagreement is always an override stated on one side only. Left
251
+ // alone it reaches the schema diff as two tables with one name, and is reported
252
+ // there as a duplicate descriptor rather than as the declaration that caused it.
253
+ if (signatureOf(existing.table) !== signatureOf(table)) {
254
+ throw new AssemoraError('CONFLICTING_JOIN_TABLE', `"${existing.from}" and "${from}" describe the join table "${table.name}" differently: ` +
255
+ `(${signatureOf(existing.table)}) against (${signatureOf(table)}). ` +
256
+ 'Mirror `through`, `foreignPivotKey`, `relatedPivotKey` and `ownerKey` on both sides, ' +
257
+ 'or declare a model for the join table.', { status: 500 });
258
+ }
259
+ }
260
+ }
261
+ if (derived.size === 0)
262
+ return tables;
263
+ return [...tables, ...[...derived.values()].map((entry) => entry.table)];
264
+ };
265
+ //# sourceMappingURL=join-table.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"join-table.js","sourceRoot":"","sources":["../src/join-table.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { DatabaseAdapter } from './adapter.js';
2
+ export type Row = Record<string, unknown>;
3
+ export type MemoryAdapter = DatabaseAdapter & {
4
+ /** The rows a table currently holds, as stored. */
5
+ rows(table: string): readonly Row[];
6
+ seed(table: string, rows: readonly Row[]): void;
7
+ reset(): void;
8
+ /**
9
+ * Counters the tests read; not part of ordinary use.
10
+ *
11
+ * `scanCount()` is what `statementCount()` is on the PostgreSQL adapter: one pass
12
+ * over one table. A relation loaded in a batch costs a fixed number of passes
13
+ * however many rows it is loaded for, and one loaded row by row costs a pass per
14
+ * row — which is how SPEC.md §89 asks for an N+1 to be caught.
15
+ */
16
+ readonly diagnostics: {
17
+ scanCount(): number;
18
+ reset(): void;
19
+ };
20
+ };
21
+ export declare const createMemoryAdapter: (seed?: Readonly<Record<string, readonly Row[]>>) => MemoryAdapter;
22
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../src/memory.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,eAAe,EAKhB,MAAM,cAAc,CAAA;AAWrB,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;AAEzC,MAAM,MAAM,aAAa,GAAG,eAAe,GAAG;IAC5C,mDAAmD;IACnD,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,EAAE,CAAA;IACnC,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,EAAE,GAAG,IAAI,CAAA;IAC/C,KAAK,IAAI,IAAI,CAAA;IACb;;;;;;;OAOG;IACH,QAAQ,CAAC,WAAW,EAAE;QACpB,SAAS,IAAI,MAAM,CAAA;QACnB,KAAK,IAAI,IAAI,CAAA;KACd,CAAA;CACF,CAAA;AA8ND,eAAO,MAAM,mBAAmB,UACxB,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,GAAG,EAAE,CAAC,CAAC,KAC7C,aAyRF,CAAA"}