@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/mock.js
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An in-memory source.
|
|
3
|
+
*
|
|
4
|
+
* Everything relations can do, done against plain objects in this process. Use it in
|
|
5
|
+
* tests, in examples, and to feel out a model's shape before you pick a backend.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { MockSource } from '@relations-dil/relations/mock'
|
|
9
|
+
*
|
|
10
|
+
* new MockSource('example')
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
14
|
+
import { join } from 'node:path';
|
|
15
|
+
import * as overscore from './overscore.js';
|
|
16
|
+
import { ModelError, UniqueError } from './errors.js';
|
|
17
|
+
import { Source } from './source.js';
|
|
18
|
+
import { Titles } from './titles.js';
|
|
19
|
+
import { clone, stable } from './util.js';
|
|
20
|
+
import { construct } from './model.js';
|
|
21
|
+
/** A stand-in for a real source's query object. */
|
|
22
|
+
export class MockQuery {
|
|
23
|
+
/** What the query would do. */
|
|
24
|
+
action;
|
|
25
|
+
/** The model it was built for. */
|
|
26
|
+
model = null;
|
|
27
|
+
constructor(action) {
|
|
28
|
+
this.action = action;
|
|
29
|
+
}
|
|
30
|
+
/** Attach the model, the way a real query builder would. */
|
|
31
|
+
bind(model) {
|
|
32
|
+
this.model = model;
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** A complete source that keeps everything in memory. */
|
|
37
|
+
export class MockSource extends Source {
|
|
38
|
+
static KIND = 'mock';
|
|
39
|
+
/** Query classes, so tests can assert on what would run. */
|
|
40
|
+
SELECT = MockQuery;
|
|
41
|
+
INSERT = MockQuery;
|
|
42
|
+
UPDATE = MockQuery;
|
|
43
|
+
DELETE = MockQuery;
|
|
44
|
+
/** Last id handed out, keyed by model name. */
|
|
45
|
+
ids = {};
|
|
46
|
+
/** Records keyed by id, keyed by model name. */
|
|
47
|
+
data = {};
|
|
48
|
+
/** Unique index values keyed by id, keyed by index, keyed by model name. */
|
|
49
|
+
unique = {};
|
|
50
|
+
/** Migrations applied so far, or null when nothing has been loaded. */
|
|
51
|
+
migrations = null;
|
|
52
|
+
/** Snapshot taken while a write is in flight, so a failure can roll back. */
|
|
53
|
+
transaction = null;
|
|
54
|
+
constructor(name, options = {}) {
|
|
55
|
+
super(name, options);
|
|
56
|
+
this.ids = {};
|
|
57
|
+
this.data = {};
|
|
58
|
+
this.unique = {};
|
|
59
|
+
this.migrations = null;
|
|
60
|
+
}
|
|
61
|
+
/** Make room for a model, and give it an auto id if it has an id field. */
|
|
62
|
+
init(model) {
|
|
63
|
+
this.recordInit(model._fields);
|
|
64
|
+
// Even models without an id field get one internally. It just isn't visible.
|
|
65
|
+
const name = model.NAME;
|
|
66
|
+
this.ids[name] = this.ids[name] ?? 0;
|
|
67
|
+
this.data[name] = this.data[name] ?? new Map();
|
|
68
|
+
this.unique[name] = this.unique[name] ?? {};
|
|
69
|
+
for (const unique of Object.keys(model._unique)) {
|
|
70
|
+
this.unique[name][unique] = this.unique[name][unique] ?? new Map();
|
|
71
|
+
}
|
|
72
|
+
if (model._id !== null) {
|
|
73
|
+
const field = model._fields.field(model._id);
|
|
74
|
+
if (field.auto === null) {
|
|
75
|
+
field.auto = true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// ------------------------------------------------------------------ definitions
|
|
80
|
+
fieldDefine(field, definitions) {
|
|
81
|
+
definitions.push(field);
|
|
82
|
+
}
|
|
83
|
+
define(model) {
|
|
84
|
+
const definitions = [];
|
|
85
|
+
this.recordDefine(model.fields, definitions);
|
|
86
|
+
return [{ ACTION: 'add', ...model, fields: definitions }];
|
|
87
|
+
}
|
|
88
|
+
fieldAdd(migration, migrations) {
|
|
89
|
+
const definitions = [];
|
|
90
|
+
this.fieldDefine(migration, definitions);
|
|
91
|
+
migrations.push(...definitions.map((definition) => ({ ...definition, ACTION: 'add' })));
|
|
92
|
+
}
|
|
93
|
+
fieldRemove(definition, migrations) {
|
|
94
|
+
migrations.push({ ACTION: 'remove', ...definition });
|
|
95
|
+
}
|
|
96
|
+
fieldChange(definition, migration, migrations) {
|
|
97
|
+
migrations.push({ ACTION: 'change', DEFINITION: definition, MIGRATION: migration });
|
|
98
|
+
}
|
|
99
|
+
modelAdd(definition) {
|
|
100
|
+
return this.define(definition);
|
|
101
|
+
}
|
|
102
|
+
modelRemove(definition) {
|
|
103
|
+
return [{ ACTION: 'remove', ...definition }];
|
|
104
|
+
}
|
|
105
|
+
modelChange(definition, migration) {
|
|
106
|
+
const migrations = [];
|
|
107
|
+
this.recordChange(definition.fields, migration.fields ?? {}, migrations);
|
|
108
|
+
return [{ ACTION: 'change', DEFINITION: definition, MIGRATION: { ...migration, fields: migrations } }];
|
|
109
|
+
}
|
|
110
|
+
// ------------------------------------------------------------------ storage helpers
|
|
111
|
+
/** Copy extracted paths out of container fields into their own stored keys. */
|
|
112
|
+
static extract(model, values) {
|
|
113
|
+
for (const field of model._fields._order) {
|
|
114
|
+
if (!field.extract) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
for (const path of Object.keys(field.extract)) {
|
|
118
|
+
values[`${field.store}__${path}`] = overscore.get(values[field.store], path);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return values;
|
|
122
|
+
}
|
|
123
|
+
/** Check every unique index, recording the new value or throwing. */
|
|
124
|
+
uniques(model, values, id) {
|
|
125
|
+
const name = model.NAME;
|
|
126
|
+
for (const [unique, fields] of Object.entries(model._unique)) {
|
|
127
|
+
const value = stable(Object.fromEntries(fields.map((field) => [field, overscore.get(values, field)])));
|
|
128
|
+
for (const [key, exists] of this.unique[name][unique]) {
|
|
129
|
+
if (value === exists && id !== key) {
|
|
130
|
+
throw new UniqueError(model, `value ${value} violates unique ${unique}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.unique[name][unique].set(id, value);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Run a write with a snapshot in place, restoring it if the write fails. */
|
|
137
|
+
async rolling(work) {
|
|
138
|
+
if (this.transaction !== null) {
|
|
139
|
+
return work();
|
|
140
|
+
}
|
|
141
|
+
this.transaction = clone([this.ids, this.data, this.unique]);
|
|
142
|
+
try {
|
|
143
|
+
const result = await work();
|
|
144
|
+
this.transaction = null;
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const snapshot = this.transaction;
|
|
149
|
+
this.ids = snapshot[0];
|
|
150
|
+
this.data = snapshot[1];
|
|
151
|
+
this.unique = snapshot[2];
|
|
152
|
+
this.transaction = null;
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// ------------------------------------------------------------------ create
|
|
157
|
+
createQuery(model) {
|
|
158
|
+
return new this.INSERT('CREATE');
|
|
159
|
+
}
|
|
160
|
+
async create(model) {
|
|
161
|
+
await super.create(model);
|
|
162
|
+
return this.rolling(async () => {
|
|
163
|
+
const name = model.NAME;
|
|
164
|
+
for (const creating of model._each('create')) {
|
|
165
|
+
const values = creating._record.create({});
|
|
166
|
+
this.ids[name] += 1;
|
|
167
|
+
this.uniques(model, values, this.ids[name]);
|
|
168
|
+
if (model._id !== null && (values[model._id] ?? null) === null) {
|
|
169
|
+
values[model._fields.field(model._id).store] = this.ids[name];
|
|
170
|
+
creating[model._id] = this.ids[name];
|
|
171
|
+
}
|
|
172
|
+
this.data[name].set(this.ids[name], MockSource.extract(creating, values));
|
|
173
|
+
if (!model._bulk) {
|
|
174
|
+
if (model._id) {
|
|
175
|
+
await Source.createTies(creating);
|
|
176
|
+
}
|
|
177
|
+
for (const attr of Object.keys(creating.CHILDREN)) {
|
|
178
|
+
const child = creating._children[attr];
|
|
179
|
+
if (child) {
|
|
180
|
+
await child.create();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
creating._action = 'update';
|
|
184
|
+
creating._record._action = 'update';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (model._bulk) {
|
|
188
|
+
model._models = [];
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
model._action = 'update';
|
|
192
|
+
}
|
|
193
|
+
return model;
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
// ------------------------------------------------------------------ retrieve
|
|
197
|
+
/** The records matching a fuzzy `like`, including matches through parent titles. */
|
|
198
|
+
async modelLike(model) {
|
|
199
|
+
const parents = {};
|
|
200
|
+
for (const field of model._titles) {
|
|
201
|
+
const relation = model._ancestor(field);
|
|
202
|
+
if (relation) {
|
|
203
|
+
const parent = await relation.Parent.many({ like: model._like }).limit(model._chunk).retrieve();
|
|
204
|
+
parents[model._fields.field(field).store] = parent[relation.parentId];
|
|
205
|
+
model.overflow = model.overflow || parent.overflow;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const likes = [];
|
|
209
|
+
for (const record of this.data[model.NAME].values()) {
|
|
210
|
+
if (model._record.like(record, model._titles, model._like, parents)) {
|
|
211
|
+
likes.push(record);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return likes;
|
|
215
|
+
}
|
|
216
|
+
/** Apply the model's sort, or its default order. */
|
|
217
|
+
static modelSort(model) {
|
|
218
|
+
const sort = model._sort ?? model._order;
|
|
219
|
+
if (sort.length) {
|
|
220
|
+
model.sort(...sort);
|
|
221
|
+
model._sort = null;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
/** Apply the model's limit, flagging overflow when it was reached. */
|
|
225
|
+
static modelLimit(model) {
|
|
226
|
+
if (model._limit === null) {
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
model._models = (model._models ?? []).slice(model._offset, model._offset + model._limit);
|
|
230
|
+
model.overflow = model.overflow || model._models.length >= model._limit;
|
|
231
|
+
}
|
|
232
|
+
countQuery(model) {
|
|
233
|
+
return new this.SELECT('COUNT');
|
|
234
|
+
}
|
|
235
|
+
async count(model) {
|
|
236
|
+
await super.count(model);
|
|
237
|
+
await model._collate();
|
|
238
|
+
const values = model._like !== null ? await this.modelLike(model) : [...this.data[model.NAME].values()];
|
|
239
|
+
let matches = 0;
|
|
240
|
+
for (const record of values) {
|
|
241
|
+
if (model._record.retrieve(record)) {
|
|
242
|
+
matches += 1;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return matches;
|
|
246
|
+
}
|
|
247
|
+
retrieveQuery(model) {
|
|
248
|
+
return new this.SELECT('RETRIEVE');
|
|
249
|
+
}
|
|
250
|
+
async retrieve(model, verify = true) {
|
|
251
|
+
await super.retrieve(model, verify);
|
|
252
|
+
await model._collate();
|
|
253
|
+
const values = model._like !== null ? await this.modelLike(model) : [...this.data[model.NAME].values()];
|
|
254
|
+
const matches = [];
|
|
255
|
+
for (const record of values) {
|
|
256
|
+
if (model._record.retrieve(record)) {
|
|
257
|
+
matches.push(record);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (model._mode === 'one' && matches.length > 1) {
|
|
261
|
+
throw new ModelError(model, 'more than one retrieved');
|
|
262
|
+
}
|
|
263
|
+
if (model._mode === 'one' && model._role !== 'child') {
|
|
264
|
+
if (matches.length < 1) {
|
|
265
|
+
if (verify) {
|
|
266
|
+
throw new ModelError(model, 'none retrieved');
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
model._record = model._build('update', { read: matches[0] });
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
model._models = matches.map((match) => construct(model.constructor, { read: match }));
|
|
274
|
+
model._record = null;
|
|
275
|
+
}
|
|
276
|
+
model._action = 'update';
|
|
277
|
+
if (model._mode === 'many') {
|
|
278
|
+
MockSource.modelSort(model);
|
|
279
|
+
MockSource.modelLimit(model);
|
|
280
|
+
}
|
|
281
|
+
await Source.retrieveTies(model);
|
|
282
|
+
return model;
|
|
283
|
+
}
|
|
284
|
+
// ------------------------------------------------------------------ titles
|
|
285
|
+
titlesQuery(model) {
|
|
286
|
+
return new this.SELECT('TITLES');
|
|
287
|
+
}
|
|
288
|
+
async titles(model) {
|
|
289
|
+
await super.titles(model);
|
|
290
|
+
if (model._action === 'retrieve') {
|
|
291
|
+
await this.retrieve(model);
|
|
292
|
+
}
|
|
293
|
+
const titles = await Titles.build(model);
|
|
294
|
+
for (const titling of model._each()) {
|
|
295
|
+
titles.add(titling);
|
|
296
|
+
}
|
|
297
|
+
return titles;
|
|
298
|
+
}
|
|
299
|
+
// ------------------------------------------------------------------ update
|
|
300
|
+
updateQuery(model) {
|
|
301
|
+
return new this.UPDATE('UPDATE');
|
|
302
|
+
}
|
|
303
|
+
async update(model) {
|
|
304
|
+
return this.rolling(async () => {
|
|
305
|
+
const name = model.NAME;
|
|
306
|
+
let updated = 0;
|
|
307
|
+
if (model._action === 'retrieve' && model._record._action === 'update') {
|
|
308
|
+
// A mass update: every matching record gets the same values.
|
|
309
|
+
const values = model._record.mass({});
|
|
310
|
+
const ties = model._record.tie({});
|
|
311
|
+
for (const [id, data] of this.data[name]) {
|
|
312
|
+
if (model._record.retrieve(data)) {
|
|
313
|
+
updated += 1;
|
|
314
|
+
const updating = { ...data, ...values };
|
|
315
|
+
this.uniques(model, updating, id);
|
|
316
|
+
Object.assign(data, MockSource.extract(model, clone(values)));
|
|
317
|
+
await Source.deleteTies(model, id);
|
|
318
|
+
await Source.createTies(model, { ...updating, ...ties });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
else if (model._id) {
|
|
323
|
+
for (const updating of model._each('update')) {
|
|
324
|
+
const data = MockSource.extract(updating, updating._record.update({}));
|
|
325
|
+
this.uniques(model, data, updating[model._id]);
|
|
326
|
+
Object.assign(this.data[name].get(updating[model._id]), data);
|
|
327
|
+
await Source.deleteTies(updating);
|
|
328
|
+
await Source.createTies(updating);
|
|
329
|
+
updated += 1;
|
|
330
|
+
for (const attr of Object.keys(updating.CHILDREN)) {
|
|
331
|
+
const child = updating._children[attr];
|
|
332
|
+
if (child) {
|
|
333
|
+
await child.create();
|
|
334
|
+
await child.update();
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
throw new ModelError(model, 'nothing to update from');
|
|
341
|
+
}
|
|
342
|
+
return updated;
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
// ------------------------------------------------------------------ delete
|
|
346
|
+
deleteQuery(model) {
|
|
347
|
+
return new this.DELETE('DELETE');
|
|
348
|
+
}
|
|
349
|
+
async delete(model) {
|
|
350
|
+
return this.rolling(async () => {
|
|
351
|
+
const name = model.NAME;
|
|
352
|
+
const ids = [];
|
|
353
|
+
if (model._action === 'retrieve') {
|
|
354
|
+
for (const [id, record] of this.data[name]) {
|
|
355
|
+
if (model._record.retrieve(record)) {
|
|
356
|
+
ids.push(id);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
else if (model._id) {
|
|
361
|
+
for (const deleting of model._each()) {
|
|
362
|
+
ids.push(deleting[model._id]);
|
|
363
|
+
deleting._action = 'create';
|
|
364
|
+
}
|
|
365
|
+
model._action = 'create';
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
throw new ModelError(model, 'nothing to delete from');
|
|
369
|
+
}
|
|
370
|
+
for (const id of ids) {
|
|
371
|
+
this.data[name].delete(id);
|
|
372
|
+
for (const unique of Object.keys(model._unique)) {
|
|
373
|
+
this.unique[name][unique].delete(id);
|
|
374
|
+
}
|
|
375
|
+
await Source.deleteTies(model, id);
|
|
376
|
+
}
|
|
377
|
+
return ids.length;
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
// ------------------------------------------------------------------ files
|
|
381
|
+
async definition(filePath, sourcePath) {
|
|
382
|
+
const definitions = [];
|
|
383
|
+
const definition = JSON.parse(await readFile(filePath, 'utf8'));
|
|
384
|
+
for (const name of Object.keys(definition).sort()) {
|
|
385
|
+
if (definition[name].source === this.name) {
|
|
386
|
+
definitions.push(...this.define(definition[name]));
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (definitions.length) {
|
|
390
|
+
await write(sourcePath, filePath, definitions);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
async migration(filePath, sourcePath) {
|
|
394
|
+
const migrations = [];
|
|
395
|
+
const migration = JSON.parse(await readFile(filePath, 'utf8'));
|
|
396
|
+
for (const add of Object.keys(migration.add ?? {}).sort()) {
|
|
397
|
+
if (migration.add[add].source === this.name) {
|
|
398
|
+
migrations.push(...this.modelAdd(migration.add[add]));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
for (const remove of Object.keys(migration.remove ?? {}).sort()) {
|
|
402
|
+
if (migration.remove[remove].source === this.name) {
|
|
403
|
+
migrations.push(...this.modelRemove(migration.remove[remove]));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
for (const change of Object.keys(migration.change ?? {}).sort()) {
|
|
407
|
+
if (migration.change[change].definition.source === this.name) {
|
|
408
|
+
migrations.push(...this.modelChange(migration.change[change].definition, migration.change[change].migration));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (migrations.length) {
|
|
412
|
+
await write(sourcePath, filePath, migrations);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async execute(models) {
|
|
416
|
+
const each = Array.isArray(models) ? models : [models];
|
|
417
|
+
for (const model of each) {
|
|
418
|
+
if (model.ACTION === 'add') {
|
|
419
|
+
this.data[model.name] = this.data[model.name] ?? new Map();
|
|
420
|
+
this.ids[model.name] = this.ids[model.name] ?? 0;
|
|
421
|
+
}
|
|
422
|
+
else if (model.ACTION === 'remove') {
|
|
423
|
+
delete this.data[model.name];
|
|
424
|
+
delete this.ids[model.name];
|
|
425
|
+
}
|
|
426
|
+
else if (model.ACTION === 'change') {
|
|
427
|
+
const name = model.MIGRATION.name ?? model.DEFINITION.name;
|
|
428
|
+
if (model.DEFINITION.name !== name) {
|
|
429
|
+
this.data[name] = this.data[model.DEFINITION.name];
|
|
430
|
+
this.ids[name] = this.ids[model.DEFINITION.name];
|
|
431
|
+
delete this.data[model.DEFINITION.name];
|
|
432
|
+
delete this.ids[model.DEFINITION.name];
|
|
433
|
+
}
|
|
434
|
+
for (const field of model.MIGRATION.fields ?? []) {
|
|
435
|
+
if (field.ACTION === 'add') {
|
|
436
|
+
for (const record of this.data[name].values()) {
|
|
437
|
+
record[field.store] = field.default ?? null;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
else if (field.ACTION === 'remove') {
|
|
441
|
+
for (const record of this.data[name].values()) {
|
|
442
|
+
delete record[field.store];
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
else if (field.ACTION === 'change') {
|
|
446
|
+
const store = field.MIGRATION.store ?? field.DEFINITION.store;
|
|
447
|
+
if (field.DEFINITION.store !== store) {
|
|
448
|
+
for (const record of this.data[name].values()) {
|
|
449
|
+
record[store] = record[field.DEFINITION.store];
|
|
450
|
+
delete record[field.DEFINITION.store];
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async load(loadPath) {
|
|
459
|
+
await this.execute(JSON.parse(await readFile(loadPath, 'utf8')));
|
|
460
|
+
}
|
|
461
|
+
async list(sourcePath) {
|
|
462
|
+
const migrations = {};
|
|
463
|
+
for (const fileName of await readdir(sourcePath)) {
|
|
464
|
+
if (!fileName.endsWith('.json') || !fileName.includes('-')) {
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
const base = fileName.split('.')[0];
|
|
468
|
+
const at = base.indexOf('-');
|
|
469
|
+
const kind = base.slice(0, at);
|
|
470
|
+
const stamp = base.slice(at + 1);
|
|
471
|
+
migrations[stamp] = migrations[stamp] ?? {};
|
|
472
|
+
migrations[stamp][kind] = fileName;
|
|
473
|
+
}
|
|
474
|
+
return migrations;
|
|
475
|
+
}
|
|
476
|
+
async migrate(sourcePath) {
|
|
477
|
+
let migrated = false;
|
|
478
|
+
const paths = (await readdir(sourcePath))
|
|
479
|
+
.filter((file) => file.startsWith('migration-') && file.endsWith('.json'))
|
|
480
|
+
.sort();
|
|
481
|
+
if (this.migrations === null) {
|
|
482
|
+
this.migrations = [];
|
|
483
|
+
await this.load(join(sourcePath, 'definition.json'));
|
|
484
|
+
migrated = true;
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
for (const path of paths) {
|
|
488
|
+
if (!this.migrations.includes(stampOf(path))) {
|
|
489
|
+
await this.load(join(sourcePath, path));
|
|
490
|
+
migrated = true;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
for (const path of paths) {
|
|
495
|
+
const stamp = stampOf(path);
|
|
496
|
+
if (!this.migrations.includes(stamp)) {
|
|
497
|
+
this.migrations.push(stamp);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return migrated;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function stampOf(path) {
|
|
504
|
+
return path.split('/').pop()?.replace(/^migration-/, '').split('.')[0];
|
|
505
|
+
}
|
|
506
|
+
async function write(sourcePath, filePath, contents) {
|
|
507
|
+
const { writeFile } = await import('node:fs/promises');
|
|
508
|
+
const fileName = filePath.split('/').pop().split('.')[0];
|
|
509
|
+
await writeFile(join(sourcePath, `${fileName}.json`), `${JSON.stringify(contents)}\n`);
|
|
510
|
+
}
|
|
511
|
+
//# sourceMappingURL=mock.js.map
|