@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/field.js
ADDED
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fields: the typed, validating, filterable units a record is made of.
|
|
3
|
+
*/
|
|
4
|
+
import * as overscore from './overscore.js';
|
|
5
|
+
import { FieldError } from './errors.js';
|
|
6
|
+
import { clone, compare, equal, sorted } from './util.js';
|
|
7
|
+
import { builtin, container, kindName, scalar, set as setKind, str as strKind } from './kinds.js';
|
|
8
|
+
/** Which comparison operators exist, and whether each takes multiple values. */
|
|
9
|
+
export const OPERATORS = Object.freeze({
|
|
10
|
+
null: false,
|
|
11
|
+
eq: false,
|
|
12
|
+
gt: false,
|
|
13
|
+
gte: false,
|
|
14
|
+
lt: false,
|
|
15
|
+
lte: false,
|
|
16
|
+
like: false,
|
|
17
|
+
start: false,
|
|
18
|
+
end: false,
|
|
19
|
+
in: true,
|
|
20
|
+
has: true,
|
|
21
|
+
any: true,
|
|
22
|
+
all: true
|
|
23
|
+
});
|
|
24
|
+
/**
|
|
25
|
+
* Names a field can't have, because the model already uses them. Use `store` when the
|
|
26
|
+
* source column really is called one of these.
|
|
27
|
+
*/
|
|
28
|
+
export const RESERVED = [
|
|
29
|
+
'action',
|
|
30
|
+
'add',
|
|
31
|
+
'append',
|
|
32
|
+
'bulk',
|
|
33
|
+
'catch',
|
|
34
|
+
'constructor',
|
|
35
|
+
'count',
|
|
36
|
+
'create',
|
|
37
|
+
'define',
|
|
38
|
+
'delete',
|
|
39
|
+
'export',
|
|
40
|
+
'filter',
|
|
41
|
+
'finally',
|
|
42
|
+
'insert',
|
|
43
|
+
'keys',
|
|
44
|
+
'like',
|
|
45
|
+
'limit',
|
|
46
|
+
'many',
|
|
47
|
+
'match',
|
|
48
|
+
'models',
|
|
49
|
+
'one',
|
|
50
|
+
'overflow',
|
|
51
|
+
'prepare',
|
|
52
|
+
'query',
|
|
53
|
+
'queue',
|
|
54
|
+
'read',
|
|
55
|
+
'retrieve',
|
|
56
|
+
'satisfy',
|
|
57
|
+
'set',
|
|
58
|
+
'size',
|
|
59
|
+
'sort',
|
|
60
|
+
'then',
|
|
61
|
+
'thy',
|
|
62
|
+
'titles',
|
|
63
|
+
'toJSON',
|
|
64
|
+
'update',
|
|
65
|
+
'write'
|
|
66
|
+
];
|
|
67
|
+
/** Attributes that can appear in a definition, in the order they're emitted. */
|
|
68
|
+
const DEFINABLE = [
|
|
69
|
+
'name',
|
|
70
|
+
'store',
|
|
71
|
+
'none',
|
|
72
|
+
'default',
|
|
73
|
+
'options',
|
|
74
|
+
'validation',
|
|
75
|
+
'length',
|
|
76
|
+
'format',
|
|
77
|
+
'readonly',
|
|
78
|
+
'auto',
|
|
79
|
+
'refresh',
|
|
80
|
+
'inject',
|
|
81
|
+
'tied',
|
|
82
|
+
'extract'
|
|
83
|
+
];
|
|
84
|
+
function membership(collection, item) {
|
|
85
|
+
if (collection instanceof Set) {
|
|
86
|
+
return collection.has(item);
|
|
87
|
+
}
|
|
88
|
+
if (Array.isArray(collection)) {
|
|
89
|
+
return collection.some((each) => equal(each, item));
|
|
90
|
+
}
|
|
91
|
+
if (typeof collection === 'string') {
|
|
92
|
+
return collection.includes(String(item));
|
|
93
|
+
}
|
|
94
|
+
if (collection && typeof collection === 'object') {
|
|
95
|
+
return Object.prototype.hasOwnProperty.call(collection, String(item));
|
|
96
|
+
}
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
function count(collection) {
|
|
100
|
+
if (collection instanceof Set) {
|
|
101
|
+
return collection.size;
|
|
102
|
+
}
|
|
103
|
+
if (Array.isArray(collection) || typeof collection === 'string') {
|
|
104
|
+
return collection.length;
|
|
105
|
+
}
|
|
106
|
+
return Object.keys(collection).length;
|
|
107
|
+
}
|
|
108
|
+
function mapping(value) {
|
|
109
|
+
if (value === null || value === undefined) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
if (typeof value === 'string') {
|
|
113
|
+
return { [value]: value };
|
|
114
|
+
}
|
|
115
|
+
if (Array.isArray(value)) {
|
|
116
|
+
return Object.fromEntries(value.map((each) => [each, each]));
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* A single typed value on a model, with everything needed to validate it, filter on it,
|
|
122
|
+
* and move it in and out of a source.
|
|
123
|
+
*/
|
|
124
|
+
export class Field {
|
|
125
|
+
/** Data kind to cast values as, or validate against. */
|
|
126
|
+
kind;
|
|
127
|
+
/** Name used in models. */
|
|
128
|
+
_name = null;
|
|
129
|
+
/** Name used when reading and writing. `false` means never stored. */
|
|
130
|
+
store = null;
|
|
131
|
+
/** Whether this field is used for a many-to-many tie. */
|
|
132
|
+
tied = null;
|
|
133
|
+
/** Attributes to store, for class kinds. */
|
|
134
|
+
attr = null;
|
|
135
|
+
/** Attributes to construct with, for class kinds. */
|
|
136
|
+
init = null;
|
|
137
|
+
/** Attributes that make up the titles. */
|
|
138
|
+
titles = null;
|
|
139
|
+
/** Interior values to pull out into their own stored columns. */
|
|
140
|
+
extract = null;
|
|
141
|
+
/** Path in another field to store this value at. */
|
|
142
|
+
inject = null;
|
|
143
|
+
/** Default value, or a factory for one. */
|
|
144
|
+
default = null;
|
|
145
|
+
/** Whether null is allowed. */
|
|
146
|
+
none = true;
|
|
147
|
+
/** What `none` was before it got inferred. */
|
|
148
|
+
_none = null;
|
|
149
|
+
/** The complete set of allowed values. */
|
|
150
|
+
options = null;
|
|
151
|
+
/** A regular expression or predicate. */
|
|
152
|
+
validation = null;
|
|
153
|
+
/** Length of the value. */
|
|
154
|
+
length = null;
|
|
155
|
+
/** Formatting instructions, one per title. */
|
|
156
|
+
format = null;
|
|
157
|
+
/** Whether the caller may write this field. */
|
|
158
|
+
readonly = null;
|
|
159
|
+
/** Whether the source generates the value. */
|
|
160
|
+
auto = null;
|
|
161
|
+
/** Whether to reset to the default on an untouched update. */
|
|
162
|
+
refresh = null;
|
|
163
|
+
/** Current value. */
|
|
164
|
+
_value = null;
|
|
165
|
+
/** Value as last read from or written to the source, in exported form. */
|
|
166
|
+
original = null;
|
|
167
|
+
/** Criteria to search by, keyed by criterion. */
|
|
168
|
+
criteria = null;
|
|
169
|
+
/** Whether the value has been set since the record was built. */
|
|
170
|
+
changed = null;
|
|
171
|
+
constructor(kind, options) {
|
|
172
|
+
this.kind = kind;
|
|
173
|
+
// A lone boolean means `none`, unless the field itself is boolean.
|
|
174
|
+
if (typeof options === 'boolean' && kind !== undefined) {
|
|
175
|
+
options = { none: options };
|
|
176
|
+
}
|
|
177
|
+
const settings = options ?? {};
|
|
178
|
+
for (const [option, value] of Object.entries(settings)) {
|
|
179
|
+
if (value !== undefined) {
|
|
180
|
+
;
|
|
181
|
+
this[option] = value;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
// Remember what was asked for, before inference - relations uses it for unique indexes.
|
|
185
|
+
this._none = settings.none === undefined ? null : settings.none;
|
|
186
|
+
// Containers are never null and default to empty, so they're always usable.
|
|
187
|
+
if (container(this.kind)) {
|
|
188
|
+
this.none = false;
|
|
189
|
+
if (this.default === null) {
|
|
190
|
+
this.default = () => this.kind.empty();
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else if (settings.none === undefined) {
|
|
194
|
+
// With no default, options, or validation there's nothing to be wrong about.
|
|
195
|
+
this.none = this.default === null && this.options === null && this.validation === null;
|
|
196
|
+
}
|
|
197
|
+
if (this.default !== null && typeof this.default !== 'function') {
|
|
198
|
+
if (!this.check(this.default)) {
|
|
199
|
+
throw new FieldError(this, `${JSON.stringify(this.default)} default not ${kindName(this.kind)} for ${this.name}`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (this.options !== null && this.kind !== setKind) {
|
|
203
|
+
for (const option of this.options) {
|
|
204
|
+
if (!this.check(option)) {
|
|
205
|
+
throw new FieldError(this, `${JSON.stringify(option)} option not ${kindName(this.kind)} for ${this.name}`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (this.validation !== null &&
|
|
210
|
+
typeof this.validation !== 'string' &&
|
|
211
|
+
!(this.validation instanceof RegExp) &&
|
|
212
|
+
typeof this.validation !== 'function') {
|
|
213
|
+
throw new FieldError(this, `${String(this.validation)} validation not regex or function for ${this.name}`);
|
|
214
|
+
}
|
|
215
|
+
if (!builtin(this.kind) && settings.attr === undefined) {
|
|
216
|
+
throw new FieldError(this, `${kindName(this.kind)} requires at least attr`);
|
|
217
|
+
}
|
|
218
|
+
// attr stands in for titles and init when those aren't given.
|
|
219
|
+
if (settings.attr !== undefined && settings.titles === undefined) {
|
|
220
|
+
this.titles = settings.attr;
|
|
221
|
+
}
|
|
222
|
+
if (settings.attr !== undefined && settings.init === undefined) {
|
|
223
|
+
this.init = settings.attr;
|
|
224
|
+
}
|
|
225
|
+
if (typeof this.attr !== 'function') {
|
|
226
|
+
this.attr = mapping(this.attr);
|
|
227
|
+
}
|
|
228
|
+
if (typeof this.init !== 'function') {
|
|
229
|
+
this.init = mapping(this.init);
|
|
230
|
+
}
|
|
231
|
+
if (typeof this.titles === 'string') {
|
|
232
|
+
this.titles = [this.titles];
|
|
233
|
+
}
|
|
234
|
+
else if (this.titles !== null && !Array.isArray(this.titles)) {
|
|
235
|
+
this.titles = Object.keys(this.titles);
|
|
236
|
+
}
|
|
237
|
+
if (this.format === null && this.titles !== null) {
|
|
238
|
+
this.format = this.titles.map(() => null);
|
|
239
|
+
}
|
|
240
|
+
if (this.format !== null && !Array.isArray(this.format)) {
|
|
241
|
+
this.format = [this.format];
|
|
242
|
+
}
|
|
243
|
+
// extract accepts a path, a list of paths, or paths mapped to kinds; str is the default kind.
|
|
244
|
+
if (typeof this.extract === 'string') {
|
|
245
|
+
this.extract = { [this.extract]: strKind };
|
|
246
|
+
}
|
|
247
|
+
else if (Array.isArray(this.extract)) {
|
|
248
|
+
this.extract = Object.fromEntries(this.extract.map((each) => [each, strKind]));
|
|
249
|
+
}
|
|
250
|
+
else if (this.extract !== null && typeof this.extract === 'object') {
|
|
251
|
+
this.extract = Object.fromEntries(Object.entries(this.extract).map(([path, kind]) => [path, kind ?? strKind]));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Name used in models. Setting it validates the name and fills in `store`. */
|
|
255
|
+
get name() {
|
|
256
|
+
return this._name;
|
|
257
|
+
}
|
|
258
|
+
set name(value) {
|
|
259
|
+
if (value !== null) {
|
|
260
|
+
let error = null;
|
|
261
|
+
if (RESERVED.includes(value)) {
|
|
262
|
+
error = 'is reserved';
|
|
263
|
+
}
|
|
264
|
+
else if (value.includes('__')) {
|
|
265
|
+
error = "cannot contain '__'";
|
|
266
|
+
}
|
|
267
|
+
else if (value[0] === '_') {
|
|
268
|
+
error = "cannot start with '_'";
|
|
269
|
+
}
|
|
270
|
+
if (error) {
|
|
271
|
+
throw new FieldError(this, `field name '${value}' ${error} - use the store option for this name`);
|
|
272
|
+
}
|
|
273
|
+
if (this.store === null) {
|
|
274
|
+
this.store = value;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
this._name = value;
|
|
278
|
+
}
|
|
279
|
+
/** Current value. Setting it validates and casts, and marks the field changed. */
|
|
280
|
+
get value() {
|
|
281
|
+
return this._value;
|
|
282
|
+
}
|
|
283
|
+
set value(value) {
|
|
284
|
+
this._value = this.valid(value);
|
|
285
|
+
this.changed = true;
|
|
286
|
+
}
|
|
287
|
+
/** Set the value without marking the field changed. */
|
|
288
|
+
load(value) {
|
|
289
|
+
this._value = this.valid(value);
|
|
290
|
+
}
|
|
291
|
+
/** Whether a value is already of this field's kind. */
|
|
292
|
+
check(value) {
|
|
293
|
+
return builtin(this.kind) ? this.kind.check(value) : value instanceof this.kind;
|
|
294
|
+
}
|
|
295
|
+
/** An independent copy, used when a model builds a record from its fields. */
|
|
296
|
+
copy() {
|
|
297
|
+
const field = Object.create(Field.prototype);
|
|
298
|
+
for (const [key, value] of Object.entries(this)) {
|
|
299
|
+
;
|
|
300
|
+
field[key] = ['kind', 'attr', 'init', 'validation', 'default'].includes(key.replace(/^_/, ''))
|
|
301
|
+
? value
|
|
302
|
+
: clone(value);
|
|
303
|
+
}
|
|
304
|
+
field.kind = this.kind;
|
|
305
|
+
field.attr = this.attr;
|
|
306
|
+
field.init = this.init;
|
|
307
|
+
field.validation = this.validation;
|
|
308
|
+
field.default = this.default;
|
|
309
|
+
field.extract = this.extract;
|
|
310
|
+
return field;
|
|
311
|
+
}
|
|
312
|
+
/** The definition of this field, for schemas and migrations. */
|
|
313
|
+
define() {
|
|
314
|
+
const definition = { kind: kindName(this.kind) };
|
|
315
|
+
for (const attr of DEFINABLE) {
|
|
316
|
+
const value = this[attr];
|
|
317
|
+
if (value === null || value === undefined || typeof value === 'function') {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (attr === 'extract') {
|
|
321
|
+
definition[attr] = Object.fromEntries(Object.entries(this.extract).map(([path, kind]) => [path, kindName(kind)]));
|
|
322
|
+
}
|
|
323
|
+
else if (attr === 'validation' && value instanceof RegExp) {
|
|
324
|
+
definition[attr] = value.source;
|
|
325
|
+
}
|
|
326
|
+
else {
|
|
327
|
+
definition[attr] = clone(value);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return definition;
|
|
331
|
+
}
|
|
332
|
+
/** The valid form of a value, throwing when it can't be made valid. */
|
|
333
|
+
valid(value) {
|
|
334
|
+
// none rules all
|
|
335
|
+
if (value === null || value === undefined) {
|
|
336
|
+
if (!this.none) {
|
|
337
|
+
throw new FieldError(this, `null not allowed for ${this.name}`);
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (!this.check(value)) {
|
|
342
|
+
try {
|
|
343
|
+
if (typeof this.init === 'function') {
|
|
344
|
+
value = this.init(value);
|
|
345
|
+
}
|
|
346
|
+
else if (this.init !== null && overscore.isDict(value)) {
|
|
347
|
+
const built = {};
|
|
348
|
+
for (const [init, store] of Object.entries(this.init)) {
|
|
349
|
+
built[init] = overscore.get(value, store);
|
|
350
|
+
}
|
|
351
|
+
value = new this.kind(built);
|
|
352
|
+
}
|
|
353
|
+
else if (builtin(this.kind)) {
|
|
354
|
+
value = this.kind.cast(value);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
value = new this.kind(value);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
catch (error) {
|
|
361
|
+
throw new FieldError(this, `${JSON.stringify(value) ?? String(value)} invalid for ${this.name}: ${error.message}`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (this.options !== null) {
|
|
365
|
+
if (this.kind === setKind) {
|
|
366
|
+
for (const each of value) {
|
|
367
|
+
if (!membership(this.options, each)) {
|
|
368
|
+
throw new FieldError(this, `${JSON.stringify(each)} not in ${JSON.stringify(this.options)} for ${this.name}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
else if (!membership(this.options, value)) {
|
|
373
|
+
throw new FieldError(this, `${JSON.stringify(value)} not in ${JSON.stringify(this.options)} for ${this.name}`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (this.validation !== null) {
|
|
377
|
+
if (typeof this.validation === 'string' || this.validation instanceof RegExp) {
|
|
378
|
+
const expression = this.validation instanceof RegExp ? this.validation : new RegExp(this.validation);
|
|
379
|
+
if (!expression.test(String(value))) {
|
|
380
|
+
throw new FieldError(this, `${JSON.stringify(value)} doesn't match ${String(this.validation)} for ${this.name}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
else if (typeof this.validation === 'function') {
|
|
384
|
+
if (!this.validation(value)) {
|
|
385
|
+
throw new FieldError(this, `${JSON.stringify(value)} invalid for ${this.name}`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return value;
|
|
390
|
+
}
|
|
391
|
+
/** Add a criterion to this field's criteria. */
|
|
392
|
+
filter(value, criterion = 'eq') {
|
|
393
|
+
const path = overscore.parse(criterion);
|
|
394
|
+
const last = path[path.length - 1];
|
|
395
|
+
let operator;
|
|
396
|
+
if (typeof last !== 'string' || !(last.replace(/^not_/, '') in OPERATORS)) {
|
|
397
|
+
operator = 'eq';
|
|
398
|
+
criterion = `${criterion}__eq`;
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
operator = String(path.pop());
|
|
402
|
+
if (operator.startsWith('not_')) {
|
|
403
|
+
operator = operator.slice(4);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (path.length && scalar(this.kind)) {
|
|
407
|
+
throw new FieldError(this, `no path ${JSON.stringify(path)} with kind ${kindName(this.kind)}`);
|
|
408
|
+
}
|
|
409
|
+
if (this.criteria === null) {
|
|
410
|
+
this.criteria = {};
|
|
411
|
+
}
|
|
412
|
+
if ((value === null || value === undefined) && operator === 'eq') {
|
|
413
|
+
value = true;
|
|
414
|
+
operator = 'null';
|
|
415
|
+
criterion = `${criterion.slice(0, -2)}null`;
|
|
416
|
+
}
|
|
417
|
+
if (OPERATORS[operator]) {
|
|
418
|
+
if (!(criterion in this.criteria)) {
|
|
419
|
+
this.criteria[criterion] = [];
|
|
420
|
+
}
|
|
421
|
+
const values = value instanceof Set ? [...value] : Array.isArray(value) ? value : [value];
|
|
422
|
+
if (path.length || this.kind === setKind || kindName(this.kind) === 'list') {
|
|
423
|
+
this.criteria[criterion].push(...values);
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
this.criteria[criterion].push(...values.map((item) => this.valid(item)));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
else if (operator === 'null') {
|
|
430
|
+
this.criteria[criterion] = !(value === 'false' ||
|
|
431
|
+
value === 'no' ||
|
|
432
|
+
value === '0' ||
|
|
433
|
+
value === 0 ||
|
|
434
|
+
!value);
|
|
435
|
+
}
|
|
436
|
+
else if (path.length) {
|
|
437
|
+
this.criteria[criterion] = value;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
this.criteria[criterion] = this.valid(value);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** The storable form of this field's value. */
|
|
444
|
+
export() {
|
|
445
|
+
if (this.kind === setKind) {
|
|
446
|
+
const value = this._value ?? new Set();
|
|
447
|
+
if (this.options !== null) {
|
|
448
|
+
return this.options.filter((option) => value.has(option));
|
|
449
|
+
}
|
|
450
|
+
return sorted(value);
|
|
451
|
+
}
|
|
452
|
+
if (this._value === null || this._value === undefined || this.attr === null) {
|
|
453
|
+
return clone(this._value ?? null);
|
|
454
|
+
}
|
|
455
|
+
const values = {};
|
|
456
|
+
if (typeof this.attr === 'function') {
|
|
457
|
+
this.attr(values, this._value);
|
|
458
|
+
}
|
|
459
|
+
else {
|
|
460
|
+
for (const [attr, store] of Object.entries(this.attr)) {
|
|
461
|
+
const found = this._value[attr];
|
|
462
|
+
overscore.set(values, store, typeof found === 'function' ? found.call(this._value) : found);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return values;
|
|
466
|
+
}
|
|
467
|
+
/** Store a value at a path inside this field. */
|
|
468
|
+
apply(path, value) {
|
|
469
|
+
if (scalar(this.kind)) {
|
|
470
|
+
throw new FieldError(this, `no apply for ${kindName(this.kind)}`);
|
|
471
|
+
}
|
|
472
|
+
let values = this.export();
|
|
473
|
+
if (values === null || values === undefined) {
|
|
474
|
+
values = container(this.kind) && kindName(this.kind) !== 'dict' ? [] : {};
|
|
475
|
+
}
|
|
476
|
+
overscore.set(values, path, value);
|
|
477
|
+
this.value = values;
|
|
478
|
+
}
|
|
479
|
+
/** Read the value at a path inside this field. */
|
|
480
|
+
access(path) {
|
|
481
|
+
if (scalar(this.kind)) {
|
|
482
|
+
throw new FieldError(this, `no access for ${kindName(this.kind)}`);
|
|
483
|
+
}
|
|
484
|
+
return overscore.get(this.export(), path);
|
|
485
|
+
}
|
|
486
|
+
/** Whether the value differs from what was last read or written. */
|
|
487
|
+
delta() {
|
|
488
|
+
return !equal(this.export(), this.original);
|
|
489
|
+
}
|
|
490
|
+
/** Write this field's value into a values object for storage. */
|
|
491
|
+
write(values) {
|
|
492
|
+
const value = this.export();
|
|
493
|
+
if (this.inject) {
|
|
494
|
+
overscore.set(values, this.inject.split('__').slice(1).join('__'), value);
|
|
495
|
+
}
|
|
496
|
+
else if (this.store) {
|
|
497
|
+
values[this.store] = value;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** Write this field's value for a create. */
|
|
501
|
+
create(values) {
|
|
502
|
+
if (!this.auto) {
|
|
503
|
+
this.write(values);
|
|
504
|
+
this.original = this.export();
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
/** Whether a stored record satisfies this field's criteria. */
|
|
508
|
+
retrieve(values) {
|
|
509
|
+
for (const [criterion, satisfy] of Object.entries(this.criteria ?? {})) {
|
|
510
|
+
let value = this.store === false ? null : values[this.store] ?? null;
|
|
511
|
+
if (builtin(this.kind)) {
|
|
512
|
+
value = this.valid(value);
|
|
513
|
+
}
|
|
514
|
+
else if (!value) {
|
|
515
|
+
value = value || {};
|
|
516
|
+
}
|
|
517
|
+
const path = overscore.parse(criterion);
|
|
518
|
+
const pieces = String(path.pop()).split('_');
|
|
519
|
+
const operator = pieces[pieces.length - 1];
|
|
520
|
+
const invert = pieces.length > 1;
|
|
521
|
+
value = path.length ? overscore.get(this.kind === setKind ? sorted(value) : value, path) : value;
|
|
522
|
+
let condition = false;
|
|
523
|
+
if (operator === 'null') {
|
|
524
|
+
condition = satisfy === (value === null || value === undefined);
|
|
525
|
+
}
|
|
526
|
+
else if (value === null || value === undefined) {
|
|
527
|
+
condition = false;
|
|
528
|
+
}
|
|
529
|
+
else if (operator === 'in') {
|
|
530
|
+
condition = membership(satisfy, value);
|
|
531
|
+
}
|
|
532
|
+
else if (operator === 'eq') {
|
|
533
|
+
condition = equal(value, satisfy);
|
|
534
|
+
}
|
|
535
|
+
else if (operator === 'gt') {
|
|
536
|
+
condition = compare(value, satisfy) > 0;
|
|
537
|
+
}
|
|
538
|
+
else if (operator === 'gte') {
|
|
539
|
+
condition = compare(value, satisfy) >= 0;
|
|
540
|
+
}
|
|
541
|
+
else if (operator === 'lt') {
|
|
542
|
+
condition = compare(value, satisfy) < 0;
|
|
543
|
+
}
|
|
544
|
+
else if (operator === 'lte') {
|
|
545
|
+
condition = compare(value, satisfy) <= 0;
|
|
546
|
+
}
|
|
547
|
+
else if (operator === 'like') {
|
|
548
|
+
condition = String(value).toLowerCase().includes(String(satisfy).toLowerCase());
|
|
549
|
+
}
|
|
550
|
+
else if (operator === 'start') {
|
|
551
|
+
condition = String(value).toLowerCase().startsWith(String(satisfy).toLowerCase());
|
|
552
|
+
}
|
|
553
|
+
else if (operator === 'end') {
|
|
554
|
+
condition = String(value).toLowerCase().endsWith(String(satisfy).toLowerCase());
|
|
555
|
+
}
|
|
556
|
+
else if (operator === 'has') {
|
|
557
|
+
condition = satisfy.every((item) => membership(value, item));
|
|
558
|
+
}
|
|
559
|
+
else if (operator === 'any') {
|
|
560
|
+
condition = satisfy.some((item) => membership(value, item));
|
|
561
|
+
}
|
|
562
|
+
else if (operator === 'all') {
|
|
563
|
+
condition =
|
|
564
|
+
satisfy.every((item) => membership(value, item)) && count(value) === new Set(satisfy).size;
|
|
565
|
+
}
|
|
566
|
+
if (invert) {
|
|
567
|
+
condition = !condition;
|
|
568
|
+
}
|
|
569
|
+
if (!condition) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return true;
|
|
574
|
+
}
|
|
575
|
+
/** Whether a stored record matches a fuzzy `like`, or is one of a parent's matches. */
|
|
576
|
+
like(values, like, parents, path) {
|
|
577
|
+
const at = path === undefined ? undefined : typeof path === 'string' ? overscore.parse(path) : path;
|
|
578
|
+
let value = this.store === false ? null : values[this.store] ?? null;
|
|
579
|
+
if (this.titles !== null && at === undefined) {
|
|
580
|
+
if (!value) {
|
|
581
|
+
return false;
|
|
582
|
+
}
|
|
583
|
+
for (const store of this.titles) {
|
|
584
|
+
const needle = String(like).toLowerCase();
|
|
585
|
+
if (needle.length && String(overscore.get(value, store) ?? value[store]).toLowerCase().includes(needle)) {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
if (at && at.length) {
|
|
592
|
+
value = overscore.get(value, at);
|
|
593
|
+
}
|
|
594
|
+
else {
|
|
595
|
+
value = this.valid(value);
|
|
596
|
+
}
|
|
597
|
+
if (this.store !== false && this.store !== null && this.store in parents) {
|
|
598
|
+
return membership(parents[this.store], value);
|
|
599
|
+
}
|
|
600
|
+
return String(value).toLowerCase().includes(String(like).toLowerCase());
|
|
601
|
+
}
|
|
602
|
+
/** Load this field's value from a stored record. */
|
|
603
|
+
read(values) {
|
|
604
|
+
if (this.inject) {
|
|
605
|
+
this.load(overscore.get(values, this.inject.split('__').slice(1).join('__')));
|
|
606
|
+
}
|
|
607
|
+
else if (this.store) {
|
|
608
|
+
this.load(values[this.store] ?? null);
|
|
609
|
+
}
|
|
610
|
+
this.original = this.export();
|
|
611
|
+
this.changed = null;
|
|
612
|
+
}
|
|
613
|
+
/** The title values for this field, optionally at a path. */
|
|
614
|
+
title(path) {
|
|
615
|
+
if (scalar(this.kind)) {
|
|
616
|
+
return [this._value];
|
|
617
|
+
}
|
|
618
|
+
const at = path === undefined ? [] : typeof path === 'string' ? overscore.parse(path) : path;
|
|
619
|
+
if (container(this.kind)) {
|
|
620
|
+
return [at.length ? overscore.get(this.export(), at) : this.export()];
|
|
621
|
+
}
|
|
622
|
+
const values = this.export();
|
|
623
|
+
if (at.length) {
|
|
624
|
+
return [overscore.get(values, at)];
|
|
625
|
+
}
|
|
626
|
+
return (this.titles ?? []).map((title) => overscore.get(values, title));
|
|
627
|
+
}
|
|
628
|
+
/** Write this field's value for an update, if it changed. */
|
|
629
|
+
update(values) {
|
|
630
|
+
if (this.refresh && !this.delta()) {
|
|
631
|
+
this.value = typeof this.default === 'function' ? this.default() : this.default;
|
|
632
|
+
}
|
|
633
|
+
if (this.delta()) {
|
|
634
|
+
this.write(values);
|
|
635
|
+
this.original = this.export();
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
/** Write this field's value for a mass update, if it was set. */
|
|
639
|
+
mass(values) {
|
|
640
|
+
if (this.refresh && !this.changed) {
|
|
641
|
+
this.value = typeof this.default === 'function' ? this.default() : this.default;
|
|
642
|
+
}
|
|
643
|
+
if (this.changed) {
|
|
644
|
+
if (this.inject) {
|
|
645
|
+
throw new FieldError(this, 'no mass update with inject');
|
|
646
|
+
}
|
|
647
|
+
this.write(values);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** Write this field's value for a many-to-many tie update, if it was set. */
|
|
651
|
+
tie(values) {
|
|
652
|
+
if (!this.tied) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (this.changed) {
|
|
656
|
+
values[this.name] = this.export();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
//# sourceMappingURL=field.js.map
|