@relations-dil/relations 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/LICENSE +21 -0
- package/README.md +824 -0
- package/dist/dynamic.d.ts +122 -0
- package/dist/dynamic.js +164 -0
- package/dist/errors.d.ts +31 -0
- package/dist/errors.js +50 -0
- package/dist/field.d.ts +157 -0
- package/dist/field.js +660 -0
- package/dist/index.d.ts +59 -0
- package/dist/index.js +75 -0
- package/dist/kinds.d.ts +99 -0
- package/dist/kinds.js +169 -0
- package/dist/migrations.d.ts +62 -0
- package/dist/migrations.js +293 -0
- package/dist/mock.d.ts +88 -0
- package/dist/mock.js +511 -0
- package/dist/model.d.ts +277 -0
- package/dist/model.js +1159 -0
- package/dist/overscore.d.ts +66 -0
- package/dist/overscore.js +226 -0
- package/dist/record.d.ts +58 -0
- package/dist/record.js +246 -0
- package/dist/registry.d.ts +24 -0
- package/dist/registry.js +27 -0
- package/dist/relation.d.ts +114 -0
- package/dist/relation.js +145 -0
- package/dist/source.d.ts +147 -0
- package/dist/source.js +409 -0
- package/dist/titles.d.ts +43 -0
- package/dist/titles.js +115 -0
- package/dist/typed.d.ts +99 -0
- package/dist/typed.js +44 -0
- package/dist/util.d.ts +16 -0
- package/dist/util.js +117 -0
- package/package.json +68 -0
package/dist/source.js
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sources: the abstract backend.
|
|
3
|
+
*
|
|
4
|
+
* Everything that actually stores data implements this. The methods that touch storage
|
|
5
|
+
* are async, because in JavaScript anything that talks to a database, a socket, or a
|
|
6
|
+
* file has to be. Everything else - definitions, migrations, query building - is plain
|
|
7
|
+
* synchronous work on structures already in memory.
|
|
8
|
+
*/
|
|
9
|
+
import { ModelError } from './errors.js';
|
|
10
|
+
import { register } from './registry.js';
|
|
11
|
+
/**
|
|
12
|
+
* Base source. Subclass it, give it a name, and register happens for you.
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* class MySource extends Source {
|
|
16
|
+
* async create(model) { ... }
|
|
17
|
+
* }
|
|
18
|
+
*
|
|
19
|
+
* new MySource('example') // models with `static source = 'example'` now use it
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
export class Source {
|
|
23
|
+
/** What kind of source this is, used to namespace migration files. */
|
|
24
|
+
static KIND = null;
|
|
25
|
+
/** The name models refer to this source by. */
|
|
26
|
+
name;
|
|
27
|
+
constructor(name, options = {}) {
|
|
28
|
+
this.name = name;
|
|
29
|
+
for (const [option, value] of Object.entries(options)) {
|
|
30
|
+
this[option] = value;
|
|
31
|
+
}
|
|
32
|
+
register(this);
|
|
33
|
+
}
|
|
34
|
+
/** What kind of source this is. */
|
|
35
|
+
get KIND() {
|
|
36
|
+
return this.constructor.KIND;
|
|
37
|
+
}
|
|
38
|
+
/** Give an object an attribute if it hasn't got one. */
|
|
39
|
+
ensureAttribute(item, attribute, value = null) {
|
|
40
|
+
if (attribute in item) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
item[attribute] = value;
|
|
44
|
+
}
|
|
45
|
+
// ------------------------------------------------------------------ init
|
|
46
|
+
/** Prepare a field for this source. */
|
|
47
|
+
fieldInit(field) { }
|
|
48
|
+
/** Prepare every field in a record. */
|
|
49
|
+
recordInit(record) {
|
|
50
|
+
for (const field of record._order) {
|
|
51
|
+
this.fieldInit(field);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** Prepare a model. Called once per model identity. */
|
|
55
|
+
init(model) {
|
|
56
|
+
this.recordInit(model._fields);
|
|
57
|
+
}
|
|
58
|
+
// ------------------------------------------------------------------ definitions
|
|
59
|
+
/** Turn a field definition into whatever this source needs. */
|
|
60
|
+
fieldDefine(field, ...args) { }
|
|
61
|
+
/** Turn every field definition in a record. */
|
|
62
|
+
recordDefine(record, ...args) {
|
|
63
|
+
for (const field of record) {
|
|
64
|
+
this.fieldDefine(field, ...args);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Turn a model definition into whatever this source needs. */
|
|
68
|
+
define(model, ...args) { }
|
|
69
|
+
// ------------------------------------------------------------------ migrations
|
|
70
|
+
/** Add a field. */
|
|
71
|
+
fieldAdd(migration, ...args) { }
|
|
72
|
+
/** Remove a field. */
|
|
73
|
+
fieldRemove(definition, ...args) { }
|
|
74
|
+
/** Change a field. */
|
|
75
|
+
fieldChange(definition, migration, ...args) { }
|
|
76
|
+
/** Apply a record's worth of field changes. */
|
|
77
|
+
recordChange(definition, migration, ...args) {
|
|
78
|
+
for (const add of migration.add ?? []) {
|
|
79
|
+
this.fieldAdd(add, ...args);
|
|
80
|
+
}
|
|
81
|
+
for (const remove of migration.remove ?? []) {
|
|
82
|
+
this.fieldRemove(lookup(remove, definition), ...args);
|
|
83
|
+
}
|
|
84
|
+
for (const field of definition) {
|
|
85
|
+
if (field.name in (migration.change ?? {})) {
|
|
86
|
+
this.fieldChange(field, migration.change[field.name], ...args);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** Add a model. */
|
|
91
|
+
modelAdd(migration) { }
|
|
92
|
+
/** Remove a model. */
|
|
93
|
+
modelRemove(definition) { }
|
|
94
|
+
/** Change a model. */
|
|
95
|
+
modelChange(definition, migration) { }
|
|
96
|
+
// ------------------------------------------------------------------ create
|
|
97
|
+
/** Prepare a field for a create. */
|
|
98
|
+
createField(field, ...args) { }
|
|
99
|
+
/** Prepare every field in a record for a create. */
|
|
100
|
+
createRecord(record, ...args) {
|
|
101
|
+
for (const field of record._order) {
|
|
102
|
+
this.createField(field, ...args);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/** The query that would create these records. */
|
|
106
|
+
createQuery(model, ...args) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Whether any tie records need writing. */
|
|
110
|
+
static hasTies(model, data) {
|
|
111
|
+
const values = data ?? model;
|
|
112
|
+
for (const relation of Object.values(model.SISTERS)) {
|
|
113
|
+
if (relation.brotherSisterRef in values && filled(values[relation.brotherSisterRef])) {
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
for (const relation of Object.values(model.BROTHERS)) {
|
|
118
|
+
if (relation.sisterBrotherRef in values && filled(values[relation.sisterBrotherRef])) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
/** Write the tie records for a many-to-many. */
|
|
125
|
+
static async createTies(model, data, ids) {
|
|
126
|
+
const values = data ?? model;
|
|
127
|
+
let keys = ids ?? values[model._id];
|
|
128
|
+
if (!Array.isArray(keys)) {
|
|
129
|
+
keys = [keys];
|
|
130
|
+
}
|
|
131
|
+
const write = async (relation, ref, keyRef, valueRef) => {
|
|
132
|
+
if (!(ref in values)) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const holder = values;
|
|
136
|
+
const records = [];
|
|
137
|
+
for (const key of keys) {
|
|
138
|
+
for (const value of holder[ref] ?? []) {
|
|
139
|
+
records.push({ [keyRef]: key, [valueRef]: value });
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (records.length) {
|
|
143
|
+
await new relation.Tie(records).create();
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
for (const relation of Object.values(model.SISTERS)) {
|
|
147
|
+
await write(relation, relation.brotherSisterRef, relation.tieBrotherRef, relation.tieSisterRef);
|
|
148
|
+
}
|
|
149
|
+
for (const relation of Object.values(model.BROTHERS)) {
|
|
150
|
+
await write(relation, relation.sisterBrotherRef, relation.tieSisterRef, relation.tieBrotherRef);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
/** Write records. Subclasses do the actual storing and call this first. */
|
|
154
|
+
async create(model, ...args) {
|
|
155
|
+
if (model._bulk && Source.hasTies(model)) {
|
|
156
|
+
throw new ModelError(model, 'cannot create ties in bulk mode');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// ------------------------------------------------------------------ retrieve
|
|
160
|
+
/** Prepare a field for a retrieve. */
|
|
161
|
+
retrieveField(field, ...args) { }
|
|
162
|
+
/** Prepare every field in a record for a retrieve. */
|
|
163
|
+
retrieveRecord(record, ...args) {
|
|
164
|
+
for (const field of record._order) {
|
|
165
|
+
this.retrieveField(field, ...args);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/** The ids on one side of a tie that match a set operator on the other. */
|
|
169
|
+
static async tieIds(Tie, queryRef, resultRef, operator, values) {
|
|
170
|
+
const wanted = new Set(values instanceof Set ? values : Array.isArray(values) ? values : [values]);
|
|
171
|
+
const ties = await Tie.many({ [`${queryRef}__in`]: [...wanted] }).retrieve();
|
|
172
|
+
const results = ties[resultRef];
|
|
173
|
+
const queries = ties[queryRef];
|
|
174
|
+
// "all" wants every requested value present, so group by the model side and
|
|
175
|
+
// require the distinct match count to equal the de-duplicated request size.
|
|
176
|
+
if (operator === 'all') {
|
|
177
|
+
const matched = new Map();
|
|
178
|
+
results.forEach((result, index) => {
|
|
179
|
+
if (!matched.has(result)) {
|
|
180
|
+
matched.set(result, new Set());
|
|
181
|
+
}
|
|
182
|
+
matched.get(result)?.add(queries[index]);
|
|
183
|
+
});
|
|
184
|
+
const found = new Set();
|
|
185
|
+
for (const [result, seen] of matched) {
|
|
186
|
+
if (seen.size === wanted.size) {
|
|
187
|
+
found.add(result);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return found;
|
|
191
|
+
}
|
|
192
|
+
// has, any, eq: tied to at least one of the requested values.
|
|
193
|
+
return new Set(results);
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* The ids tied to a sibling matching field criteria, for filters like
|
|
197
|
+
* `{brother__name: 'Tom'}`. A model matches when it's tied to any such sibling.
|
|
198
|
+
*/
|
|
199
|
+
static async attrIds(relation, side, criteria) {
|
|
200
|
+
const Sibling = side === 'sister' ? relation.Sister : relation.Brother;
|
|
201
|
+
const siblingId = side === 'sister' ? relation.sisterId : relation.brotherId;
|
|
202
|
+
const queryRef = side === 'sister' ? relation.tieSisterRef : relation.tieBrotherRef;
|
|
203
|
+
const resultRef = side === 'sister' ? relation.tieBrotherRef : relation.tieSisterRef;
|
|
204
|
+
const siblings = await Sibling.many(criteria).retrieve();
|
|
205
|
+
const siblingIds = siblings[siblingId];
|
|
206
|
+
if (!siblingIds.length) {
|
|
207
|
+
return new Set();
|
|
208
|
+
}
|
|
209
|
+
return Source.tieIds(relation.Tie, queryRef, resultRef, 'any', siblingIds);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Resolve tie criteria into plain id filters by walking the tie model. Handles both
|
|
213
|
+
* tie-id criteria (`{brother_id__has: [...]}`) and sibling-attribute criteria
|
|
214
|
+
* (`{brother__name: 'Tom'}`).
|
|
215
|
+
*/
|
|
216
|
+
static async collateTies(model) {
|
|
217
|
+
if (!Object.keys(model.SISTERS).length && !Object.keys(model.BROTHERS).length) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
let include = null;
|
|
221
|
+
const exclude = new Set();
|
|
222
|
+
let any = false;
|
|
223
|
+
const sides = [
|
|
224
|
+
...Object.values(model.SISTERS).map((relation) => [relation, relation.brotherSisterRef, relation.tieSisterRef, relation.tieBrotherRef]),
|
|
225
|
+
...Object.values(model.BROTHERS).map((relation) => [relation, relation.sisterBrotherRef, relation.tieBrotherRef, relation.tieSisterRef])
|
|
226
|
+
];
|
|
227
|
+
for (const [relation, fieldRef, queryRef, resultRef] of sides) {
|
|
228
|
+
const field = model._record?.field(fieldRef);
|
|
229
|
+
if (!field?.criteria) {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
any = true;
|
|
233
|
+
for (const [criterion, values] of Object.entries(field.criteria)) {
|
|
234
|
+
const negate = criterion.startsWith('not_');
|
|
235
|
+
const operator = negate ? criterion.slice(4) : criterion;
|
|
236
|
+
const matched = await Source.tieIds(relation.Tie, queryRef, resultRef, operator, values);
|
|
237
|
+
if (negate) {
|
|
238
|
+
for (const id of matched) {
|
|
239
|
+
exclude.add(id);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
include = include === null ? matched : intersect(include, matched);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
field.criteria = {};
|
|
247
|
+
}
|
|
248
|
+
for (const [name, criteria] of Object.entries(model._ties)) {
|
|
249
|
+
any = true;
|
|
250
|
+
const relation = name in model.SISTERS ? model.SISTERS[name] : model.BROTHERS[name];
|
|
251
|
+
const side = name in model.SISTERS ? 'sister' : 'brother';
|
|
252
|
+
const matched = await Source.attrIds(relation, side, criteria);
|
|
253
|
+
include = include === null ? matched : intersect(include, matched);
|
|
254
|
+
}
|
|
255
|
+
model._ties = {};
|
|
256
|
+
if (!any) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
if (include !== null) {
|
|
260
|
+
model._record?.filter(`${model._id}__in`, [...include]);
|
|
261
|
+
}
|
|
262
|
+
if (exclude.size) {
|
|
263
|
+
model._record?.filter(`${model._id}__not_in`, [...exclude]);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/** Fill in the tie fields on retrieved records. */
|
|
267
|
+
static async retrieveTies(model) {
|
|
268
|
+
for (const retrieved of model._each()) {
|
|
269
|
+
const holder = retrieved;
|
|
270
|
+
for (const relation of Object.values(model.SISTERS)) {
|
|
271
|
+
const ties = await relation.Tie
|
|
272
|
+
.many({ [relation.tieBrotherRef]: holder[relation.brotherId] })
|
|
273
|
+
.retrieve();
|
|
274
|
+
holder[relation.brotherSisterRef] = ties[relation.tieSisterRef];
|
|
275
|
+
}
|
|
276
|
+
for (const relation of Object.values(model.BROTHERS)) {
|
|
277
|
+
const ties = await relation.Tie
|
|
278
|
+
.many({ [relation.tieSisterRef]: holder[relation.sisterId] })
|
|
279
|
+
.retrieve();
|
|
280
|
+
holder[relation.sisterBrotherRef] = ties[relation.tieBrotherRef];
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
/** The query that would count matching records. */
|
|
285
|
+
countQuery(model, ...args) {
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
/** Count matching records. Subclasses do the counting and call this first. */
|
|
289
|
+
async count(model, ...args) {
|
|
290
|
+
await Source.collateTies(model);
|
|
291
|
+
}
|
|
292
|
+
/** The query that would retrieve matching records. */
|
|
293
|
+
retrieveQuery(model, ...args) {
|
|
294
|
+
return undefined;
|
|
295
|
+
}
|
|
296
|
+
/** Read records. Subclasses do the reading and call this first. */
|
|
297
|
+
async retrieve(model, verify = true, ...args) {
|
|
298
|
+
await Source.collateTies(model);
|
|
299
|
+
}
|
|
300
|
+
// ------------------------------------------------------------------ titles
|
|
301
|
+
/** The query that would retrieve titles. */
|
|
302
|
+
titlesQuery(model, ...args) {
|
|
303
|
+
return undefined;
|
|
304
|
+
}
|
|
305
|
+
/** Titles for matching records. Subclasses build them and call this first. */
|
|
306
|
+
async titles(model, ...args) {
|
|
307
|
+
if (model._action === 'retrieve') {
|
|
308
|
+
await Source.collateTies(model);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// ------------------------------------------------------------------ update
|
|
312
|
+
/** Prepare a field for an update. */
|
|
313
|
+
updateField(field, ...args) { }
|
|
314
|
+
/** Prepare every field in a record for an update. */
|
|
315
|
+
updateRecord(record, ...args) {
|
|
316
|
+
for (const field of record._order) {
|
|
317
|
+
this.updateField(field, ...args);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
/** Prepare a field for a mass update. */
|
|
321
|
+
fieldMass(field, ...args) { }
|
|
322
|
+
/** Prepare every field in a record for a mass update. */
|
|
323
|
+
recordMass(record, ...args) {
|
|
324
|
+
for (const field of record._order) {
|
|
325
|
+
this.fieldMass(field, ...args);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** The query that would update records. */
|
|
329
|
+
updateQuery(model, ...args) {
|
|
330
|
+
return undefined;
|
|
331
|
+
}
|
|
332
|
+
/** Write changes. Resolves to how many records changed. */
|
|
333
|
+
async update(model, ...args) {
|
|
334
|
+
return 0;
|
|
335
|
+
}
|
|
336
|
+
// ------------------------------------------------------------------ delete
|
|
337
|
+
/** Prepare a field for a delete. */
|
|
338
|
+
deleteField(field, ...args) { }
|
|
339
|
+
/** Prepare every field in a record for a delete. */
|
|
340
|
+
deleteRecord(record, ...args) {
|
|
341
|
+
for (const field of record._order) {
|
|
342
|
+
this.deleteField(field, ...args);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/** The query that would delete records. */
|
|
346
|
+
deleteQuery(model, ...args) {
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
349
|
+
/** Remove the tie records for a many-to-many. */
|
|
350
|
+
static async deleteTies(model, ids) {
|
|
351
|
+
let keys = ids ?? model[model._id];
|
|
352
|
+
if (!Array.isArray(keys)) {
|
|
353
|
+
keys = [keys];
|
|
354
|
+
}
|
|
355
|
+
for (const relation of Object.values(model.SISTERS)) {
|
|
356
|
+
await relation.Tie.many({ [`${relation.tieBrotherRef}__in`]: keys }).delete();
|
|
357
|
+
}
|
|
358
|
+
for (const relation of Object.values(model.BROTHERS)) {
|
|
359
|
+
await relation.Tie.many({ [`${relation.tieSisterRef}__in`]: keys }).delete();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/** Remove records. Resolves to how many were removed. */
|
|
363
|
+
async delete(model, ...args) {
|
|
364
|
+
return 0;
|
|
365
|
+
}
|
|
366
|
+
// ------------------------------------------------------------------ files
|
|
367
|
+
/** Convert a general definition file into a source-specific one. */
|
|
368
|
+
async definition(filePath, sourcePath) { }
|
|
369
|
+
/** Convert a general migration file into a source-specific one. */
|
|
370
|
+
async migration(filePath, sourcePath) { }
|
|
371
|
+
/** Run a command or commands against the source. */
|
|
372
|
+
async execute(commands) { }
|
|
373
|
+
/** The migration pairs available for this source. */
|
|
374
|
+
async list(sourcePath) {
|
|
375
|
+
return {};
|
|
376
|
+
}
|
|
377
|
+
/** Load a definition or migration file into the source. */
|
|
378
|
+
async load(filePath) { }
|
|
379
|
+
/** Bring the source up to date with everything in a migration directory. */
|
|
380
|
+
async migrate(sourcePath) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/** A field definition by name. */
|
|
385
|
+
export function lookup(name, fields) {
|
|
386
|
+
for (const field of fields) {
|
|
387
|
+
if (name === field.name) {
|
|
388
|
+
return field;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
/** Truthy the way Python is: an empty Set, array or object counts as nothing. */
|
|
394
|
+
function filled(value) {
|
|
395
|
+
if (value instanceof Set || value instanceof Map) {
|
|
396
|
+
return value.size > 0;
|
|
397
|
+
}
|
|
398
|
+
if (Array.isArray(value)) {
|
|
399
|
+
return value.length > 0;
|
|
400
|
+
}
|
|
401
|
+
if (value && typeof value === 'object') {
|
|
402
|
+
return Object.keys(value).length > 0;
|
|
403
|
+
}
|
|
404
|
+
return Boolean(value);
|
|
405
|
+
}
|
|
406
|
+
function intersect(first, second) {
|
|
407
|
+
return new Set([...first].filter((item) => second.has(item)));
|
|
408
|
+
}
|
|
409
|
+
//# sourceMappingURL=source.js.map
|
package/dist/titles.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Titles: the human-readable label for a record, assembled from its title fields and,
|
|
3
|
+
* where a title field points at a parent, from that parent's titles too.
|
|
4
|
+
*/
|
|
5
|
+
import type { Model } from './model.js';
|
|
6
|
+
/** Titles for a set of records, keyed by id, with the ids kept in retrieval order. */
|
|
7
|
+
export declare class Titles {
|
|
8
|
+
/** Name of the id field the titles are keyed by. */
|
|
9
|
+
id: string;
|
|
10
|
+
/** Fields that make up a title. */
|
|
11
|
+
fields: string[];
|
|
12
|
+
/** Ids, in order. */
|
|
13
|
+
ids: any[];
|
|
14
|
+
/** Title values, keyed by id. */
|
|
15
|
+
titles: Map<any, any[]>;
|
|
16
|
+
/** Format instructions, one per title value. */
|
|
17
|
+
format: any[];
|
|
18
|
+
/** Parent titles, for title fields that point at another model. */
|
|
19
|
+
parents: globalThis.Record<string, Titles>;
|
|
20
|
+
private constructor();
|
|
21
|
+
/**
|
|
22
|
+
* Build titles for a model. Async because a title field pointing at a parent has to
|
|
23
|
+
* fetch that parent's titles first.
|
|
24
|
+
*/
|
|
25
|
+
static build(model: Model): Promise<Titles>;
|
|
26
|
+
/** How many titles there are. */
|
|
27
|
+
get size(): number;
|
|
28
|
+
/** Whether there's a title for an id. */
|
|
29
|
+
has(id: any): boolean;
|
|
30
|
+
/** The title values for an id. */
|
|
31
|
+
get(id: any): any[] | undefined;
|
|
32
|
+
/** Set the title values for an id. */
|
|
33
|
+
set(id: any, value: any[]): void;
|
|
34
|
+
/** Forget an id. */
|
|
35
|
+
delete(id: any): void;
|
|
36
|
+
/** Ids, in order. */
|
|
37
|
+
[Symbol.iterator](): IterableIterator<any>;
|
|
38
|
+
/** Add a record's title. */
|
|
39
|
+
add(model: Model): void;
|
|
40
|
+
/** Plain object form, for serialising. */
|
|
41
|
+
toJSON(): globalThis.Record<string, any>;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=titles.d.ts.map
|
package/dist/titles.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Titles: the human-readable label for a record, assembled from its title fields and,
|
|
3
|
+
* where a title field points at a parent, from that parent's titles too.
|
|
4
|
+
*/
|
|
5
|
+
import * as overscore from './overscore.js';
|
|
6
|
+
/** Titles for a set of records, keyed by id, with the ids kept in retrieval order. */
|
|
7
|
+
export class Titles {
|
|
8
|
+
/** Name of the id field the titles are keyed by. */
|
|
9
|
+
id;
|
|
10
|
+
/** Fields that make up a title. */
|
|
11
|
+
fields;
|
|
12
|
+
/** Ids, in order. */
|
|
13
|
+
ids = [];
|
|
14
|
+
/** Title values, keyed by id. */
|
|
15
|
+
titles = new Map();
|
|
16
|
+
/** Format instructions, one per title value. */
|
|
17
|
+
format = [];
|
|
18
|
+
/** Parent titles, for title fields that point at another model. */
|
|
19
|
+
parents = {};
|
|
20
|
+
constructor(model) {
|
|
21
|
+
this.id = model._id;
|
|
22
|
+
this.fields = model._titles;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Build titles for a model. Async because a title field pointing at a parent has to
|
|
26
|
+
* fetch that parent's titles first.
|
|
27
|
+
*/
|
|
28
|
+
static async build(model) {
|
|
29
|
+
const titles = new Titles(model);
|
|
30
|
+
for (const field of titles.fields) {
|
|
31
|
+
const relation = model._ancestor(field);
|
|
32
|
+
if (relation !== null) {
|
|
33
|
+
const parent = await relation.Parent
|
|
34
|
+
.many({ [`${relation.parentId}__in`]: model[field] })
|
|
35
|
+
.titles();
|
|
36
|
+
titles.parents[field] = parent;
|
|
37
|
+
titles.format.push(...parent.format);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const own = model._fields.field(field);
|
|
41
|
+
if (own && own.format !== null) {
|
|
42
|
+
titles.format.push(...own.format);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
titles.format.push(null);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return titles;
|
|
50
|
+
}
|
|
51
|
+
/** How many titles there are. */
|
|
52
|
+
get size() {
|
|
53
|
+
return this.ids.length;
|
|
54
|
+
}
|
|
55
|
+
/** Whether there's a title for an id. */
|
|
56
|
+
has(id) {
|
|
57
|
+
return this.ids.includes(id);
|
|
58
|
+
}
|
|
59
|
+
/** The title values for an id. */
|
|
60
|
+
get(id) {
|
|
61
|
+
return this.titles.get(id);
|
|
62
|
+
}
|
|
63
|
+
/** Set the title values for an id. */
|
|
64
|
+
set(id, value) {
|
|
65
|
+
if (!this.ids.includes(id)) {
|
|
66
|
+
this.ids.push(id);
|
|
67
|
+
}
|
|
68
|
+
this.titles.set(id, value);
|
|
69
|
+
}
|
|
70
|
+
/** Forget an id. */
|
|
71
|
+
delete(id) {
|
|
72
|
+
const at = this.ids.indexOf(id);
|
|
73
|
+
if (at !== -1) {
|
|
74
|
+
this.ids.splice(at, 1);
|
|
75
|
+
}
|
|
76
|
+
this.titles.delete(id);
|
|
77
|
+
}
|
|
78
|
+
/** Ids, in order. */
|
|
79
|
+
*[Symbol.iterator]() {
|
|
80
|
+
yield* this.ids;
|
|
81
|
+
}
|
|
82
|
+
/** Add a record's title. */
|
|
83
|
+
add(model) {
|
|
84
|
+
const title = [];
|
|
85
|
+
for (const name of this.fields) {
|
|
86
|
+
if (name in this.parents) {
|
|
87
|
+
const parent = this.parents[name];
|
|
88
|
+
const key = model[name];
|
|
89
|
+
if (parent.has(key)) {
|
|
90
|
+
title.push(...parent.get(key));
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
title.push(...parent.format.map(() => null));
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
const path = overscore.parse(name);
|
|
98
|
+
const field = String(path.shift());
|
|
99
|
+
title.push(...(model._record?.field(field)?.title(path) ?? []));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.set(model[this.id], title);
|
|
103
|
+
}
|
|
104
|
+
/** Plain object form, for serialising. */
|
|
105
|
+
toJSON() {
|
|
106
|
+
return {
|
|
107
|
+
id: this.id,
|
|
108
|
+
fields: this.fields,
|
|
109
|
+
ids: this.ids,
|
|
110
|
+
titles: Object.fromEntries(this.titles),
|
|
111
|
+
format: this.format
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=titles.js.map
|
package/dist/typed.d.ts
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed models.
|
|
3
|
+
*
|
|
4
|
+
* Field values are reached through a Proxy at runtime, which TypeScript can't see into
|
|
5
|
+
* on its own. `fields()` closes that gap: it declares the fields once and hands back a
|
|
6
|
+
* base class that already knows their types.
|
|
7
|
+
*
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { fields, int, str, list } from '@relations-dil/relations'
|
|
10
|
+
*
|
|
11
|
+
* class Unit extends fields({ id: int, name: str, tags: list }) {
|
|
12
|
+
* static source = 'example'
|
|
13
|
+
* }
|
|
14
|
+
*
|
|
15
|
+
* const unit = await Unit.one({ name: 'yep' }).retrieve()
|
|
16
|
+
*
|
|
17
|
+
* unit.name.toUpperCase() // string, checked
|
|
18
|
+
* unit.nmae // error: no such field
|
|
19
|
+
* unit.name = 5 // error: not a string
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Relations are declared separately from the model, so add them with `declare` when you
|
|
23
|
+
* want them typed too. `declare` is erased at compile time and costs nothing to run:
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* class Unit extends fields({ id: int, name: str }) {
|
|
27
|
+
* static source = 'example'
|
|
28
|
+
* declare test: Test
|
|
29
|
+
* }
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
import { Field } from './field.js';
|
|
33
|
+
import { Model, type FieldSpec, type ModelClass, type ModelIdentity } from './model.js';
|
|
34
|
+
import type { bool, dict, float, int, list, set, str } from './kinds.js';
|
|
35
|
+
/** The value type behind a kind sentinel. */
|
|
36
|
+
export type ValueOfKind<K> = K extends typeof bool ? boolean : K extends typeof int ? number : K extends typeof float ? number : K extends typeof str ? string : K extends typeof set ? Set<any> : K extends typeof list ? any[] : K extends typeof dict ? globalThis.Record<string, any> : K extends abstract new (...args: any[]) => infer T ? T : any;
|
|
37
|
+
/** The value type behind a field declaration. */
|
|
38
|
+
export type ValueOfSpec<S> = S extends Field ? any : S extends readonly (infer T)[] ? T : S extends Set<infer T> ? Set<T> : S extends (...args: any[]) => infer T ? T : S extends {
|
|
39
|
+
kind: infer K;
|
|
40
|
+
} ? ValueOfKind<K> : ValueOfKind<S>;
|
|
41
|
+
/** Every field of a declaration, as the values they hold. */
|
|
42
|
+
export type Values<F> = {
|
|
43
|
+
[Name in keyof F]: ValueOfSpec<F[Name]>;
|
|
44
|
+
};
|
|
45
|
+
/** A model instance with its fields typed. */
|
|
46
|
+
export type Typed<F> = Model & Values<F>;
|
|
47
|
+
/**
|
|
48
|
+
* A model in many mode: every field reads back as an array of its values, and indexing
|
|
49
|
+
* gives you one model.
|
|
50
|
+
*/
|
|
51
|
+
export type Listed<I> = Model & {
|
|
52
|
+
[Name in keyof Omit<I, keyof Model>]: I[Name][];
|
|
53
|
+
} & {
|
|
54
|
+
[index: number]: I;
|
|
55
|
+
};
|
|
56
|
+
/** The class of a model with its fields typed. */
|
|
57
|
+
export interface TypedClass<F> {
|
|
58
|
+
[member: string]: any;
|
|
59
|
+
new (...args: any[]): Typed<F>;
|
|
60
|
+
/** The field declaration this class was built from. */
|
|
61
|
+
fields: F;
|
|
62
|
+
/** Name of the source this model lives in. */
|
|
63
|
+
source?: string;
|
|
64
|
+
/** Name of the model in the source. Defaults to the underscored class name. */
|
|
65
|
+
store?: string;
|
|
66
|
+
/** Human-facing title. Defaults to the class name. */
|
|
67
|
+
title?: string;
|
|
68
|
+
/** Which field is the id, by name or position. `null` for no id. */
|
|
69
|
+
id?: string | number | null;
|
|
70
|
+
/** Which fields make up a title. */
|
|
71
|
+
titles?: string | string[];
|
|
72
|
+
/** Which fields to show in a listing. */
|
|
73
|
+
list?: string | string[];
|
|
74
|
+
/** Unique indexes. */
|
|
75
|
+
unique?: string | string[] | globalThis.Record<string, string[]> | false;
|
|
76
|
+
/** Regular indexes. */
|
|
77
|
+
index?: string | string[] | globalThis.Record<string, string[]>;
|
|
78
|
+
/** Default sort order. */
|
|
79
|
+
order?: string | string[];
|
|
80
|
+
/** Default chunk size. */
|
|
81
|
+
chunk?: number;
|
|
82
|
+
/** Everything knowable about this model without an instance. */
|
|
83
|
+
thy(target?: ModelIdentity): ModelIdentity;
|
|
84
|
+
/** The definition of this model, as the source wants it. */
|
|
85
|
+
define(...args: any[]): any;
|
|
86
|
+
/** A model that will retrieve exactly one record. */
|
|
87
|
+
one<T extends abstract new (...args: any[]) => any>(this: T, ...args: any[]): InstanceType<T>;
|
|
88
|
+
/** A model that will retrieve any number of records. */
|
|
89
|
+
many<T extends abstract new (...args: any[]) => any>(this: T, ...args: any[]): Listed<InstanceType<T>>;
|
|
90
|
+
/** A model set up to insert many records without reading their ids back. */
|
|
91
|
+
bulk<T extends abstract new (...args: any[]) => any>(this: T, size?: number): Listed<InstanceType<T>>;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A base class with these fields, typed. Extend it instead of `Model` and every field
|
|
95
|
+
* is checked at the point you touch it. Pass a base to share configuration, the way a
|
|
96
|
+
* `Base` class shares a source across models.
|
|
97
|
+
*/
|
|
98
|
+
export declare function fields<F extends globalThis.Record<string, FieldSpec>>(spec: F, base?: ModelClass | TypedClass<any>): TypedClass<F>;
|
|
99
|
+
//# sourceMappingURL=typed.d.ts.map
|