@relations-dil/relations 0.2.1 → 0.2.2

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/README.md CHANGED
@@ -335,6 +335,17 @@ static fields = {
335
335
  }
336
336
  ```
337
337
 
338
+ A relation can do the injecting for you, so a shared model can take on an optional parent
339
+ without being edited or getting a column of its own. Name the dict field to store it in:
340
+
341
+ ```ts
342
+ new OneToMany(Unum, Entity, { childInject: 'what' })
343
+ // adds entity.unum_id, stored at what.relations.unum.id; entity.unum is null when there isn't one
344
+ ```
345
+
346
+ The key is named `<model>_<id>`, or `<parent source>_<model>_<id>` when the parent lives in a
347
+ different source, which has to be a dns label (letters, digits and hyphens).
348
+
338
349
  ---
339
350
 
340
351
  ## Querying
package/dist/errors.d.ts CHANGED
@@ -24,6 +24,10 @@ export declare class ModelError extends Error {
24
24
  export declare class UniqueError extends ModelError {
25
25
  constructor(model: any, message: string);
26
26
  }
27
+ /** A source can't be registered, usually because its name isn't a dns label. */
28
+ export declare class SourceError extends Error {
29
+ constructor(message: string);
30
+ }
27
31
  /** Something went wrong diffing or applying migrations. */
28
32
  export declare class MigrationsError extends Error {
29
33
  constructor(message: string);
package/dist/errors.js CHANGED
@@ -40,6 +40,13 @@ export class UniqueError extends ModelError {
40
40
  this.name = 'UniqueError';
41
41
  }
42
42
  }
43
+ /** A source can't be registered, usually because its name isn't a dns label. */
44
+ export class SourceError extends Error {
45
+ constructor(message) {
46
+ super(message);
47
+ this.name = 'SourceError';
48
+ }
49
+ }
43
50
  /** Something went wrong diffing or applying migrations. */
44
51
  export class MigrationsError extends Error {
45
52
  constructor(message) {
package/dist/field.js CHANGED
@@ -491,7 +491,23 @@ export class Field {
491
491
  write(values) {
492
492
  const value = this.export();
493
493
  if (this.inject) {
494
- overscore.set(values, this.inject.split('__').slice(1).join('__'), value);
494
+ const path = this.inject.split('__').slice(1).join('__');
495
+ if (value === null || value === undefined) {
496
+ // A null isn't stored when it's only dict keys: a missing key reads back as null,
497
+ // and storage has no nulls to trip over.
498
+ const keys = overscore.parse(path);
499
+ let container = values;
500
+ for (const key of keys.slice(0, -1)) {
501
+ container =
502
+ overscore.isDict(container) && typeof key === 'string' ? (key in container ? container[key] : {}) : null;
503
+ }
504
+ const last = keys[keys.length - 1];
505
+ if (overscore.isDict(container) && typeof last === 'string') {
506
+ delete container[last];
507
+ return;
508
+ }
509
+ }
510
+ overscore.set(values, path, value);
495
511
  }
496
512
  else if (this.store) {
497
513
  values[this.store] = value;
package/dist/index.d.ts CHANGED
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import * as overscore from './overscore.js';
33
33
  export { overscore };
34
- export { FieldError, MigrationsError, ModelError, RecordError, UniqueError } from './errors.js';
34
+ export { FieldError, MigrationsError, ModelError, RecordError, SourceError, UniqueError } from './errors.js';
35
35
  export { OverscoreError } from './overscore.js';
36
36
  export { bool, builtin, container, dict, float, int, kindName, kindOf, list, scalar, set, str, CONTAINERS, KINDS, SCALARS, type FieldKind, type Kind, type KindClass } from './kinds.js';
37
37
  export { Field, OPERATORS, RESERVED, type Attr, type FieldOptions, type Init, type Validation } from './field.js';
@@ -46,7 +46,7 @@ export { LocalSource, type StorageLike } from './local.js';
46
46
  export { RestSource, type FetchInit, type FetchLike, type FetchResponse } from './rest.js';
47
47
  export { fields, type Listed, type Typed, type TypedClass, type ValueOfKind, type ValueOfSpec, type Values } from './typed.js';
48
48
  export { definitionsOf, modelFrom, modelsFrom, type Built, type Definition, type RelationDefinition } from './dynamic.js';
49
- export { SOURCES, clear, register, source, unregister, type Registered } from './registry.js';
49
+ export { DNS, SOURCES, clear, register, source, unregister, type Registered } from './registry.js';
50
50
  import type { ModelClass } from './model.js';
51
51
  /**
52
52
  * Every model class exported by a module, for handing to migrations.
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@
31
31
  */
32
32
  import * as overscore from './overscore.js';
33
33
  export { overscore };
34
- export { FieldError, MigrationsError, ModelError, RecordError, UniqueError } from './errors.js';
34
+ export { FieldError, MigrationsError, ModelError, RecordError, SourceError, UniqueError } from './errors.js';
35
35
  export { OverscoreError } from './overscore.js';
36
36
  export { bool, builtin, container, dict, float, int, kindName, kindOf, list, scalar, set, str, CONTAINERS, KINDS, SCALARS } from './kinds.js';
37
37
  export { Field, OPERATORS, RESERVED } from './field.js';
@@ -46,7 +46,7 @@ export { LocalSource } from './local.js';
46
46
  export { RestSource } from './rest.js';
47
47
  export { fields } from './typed.js';
48
48
  export { definitionsOf, modelFrom, modelsFrom } from './dynamic.js';
49
- export { SOURCES, clear, register, source, unregister } from './registry.js';
49
+ export { DNS, SOURCES, clear, register, source, unregister } from './registry.js';
50
50
  import { Model as ModelBase } from './model.js';
51
51
  /**
52
52
  * Every model class exported by a module, for handing to migrations.
package/dist/model.js CHANGED
@@ -545,12 +545,17 @@ export class Model extends ModelIdentity {
545
545
  if (name in this.PARENTS) {
546
546
  const relation = this.PARENTS[name];
547
547
  if (!this._parents[name]) {
548
- this._parents[name] =
549
- this._action === 'retrieve'
550
- ? relation.Parent.many().limit(this._chunk)
551
- : construct(relation.Parent, {
552
- child: { [relation.parentId]: this._item(relation.childParentRef) }
553
- });
548
+ if (this._action === 'retrieve') {
549
+ this._parents[name] = relation.Parent.many().limit(this._chunk);
550
+ }
551
+ else if (this._item(relation.childParentRef) === null) {
552
+ return null; // No key means no parent, nothing to cache
553
+ }
554
+ else {
555
+ this._parents[name] = construct(relation.Parent, {
556
+ child: { [relation.parentId]: this._item(relation.childParentRef) }
557
+ });
558
+ }
554
559
  }
555
560
  return this._parents[name];
556
561
  }
package/dist/record.js CHANGED
@@ -161,7 +161,14 @@ export class Record {
161
161
  /** Whether a stored record satisfies every field's criteria. */
162
162
  retrieve(values) {
163
163
  for (const field of this._order) {
164
- if (!field.retrieve(values)) {
164
+ if (field.inject) {
165
+ // An injected field is checked where it's stored, in the field it lives inside.
166
+ const injected = overscore.get(values[this.injected(field)] ?? {}, field.inject.split('__').slice(1).join('__'));
167
+ if (!field.retrieve({ [field.store]: injected })) {
168
+ return false;
169
+ }
170
+ }
171
+ else if (!field.retrieve(values)) {
165
172
  return false;
166
173
  }
167
174
  }
@@ -6,6 +6,11 @@
6
6
  * the whole point of relations: the same model runs against a database in one service
7
7
  * and against a REST API in another.
8
8
  */
9
+ /**
10
+ * A dns label, which is what a source's name has to be so it's safe to use in other names:
11
+ * letters, digits and hyphens, 1 to 63 characters, no leading or trailing hyphen.
12
+ */
13
+ export declare const DNS: RegExp;
9
14
  /** Anything registered as a source. Kept loose to avoid a circular import. */
10
15
  export interface Registered {
11
16
  name: string;
@@ -13,7 +18,7 @@ export interface Registered {
13
18
  }
14
19
  /** Every registered source, keyed by name. */
15
20
  export declare const SOURCES: Map<string, Registered>;
16
- /** Register a source under its name, replacing any source already there. */
21
+ /** Register a source under its name, replacing any source already there. The name has to be a dns label. */
17
22
  export declare function register(source: Registered): void;
18
23
  /** The source registered under a name, or `undefined`. */
19
24
  export declare function source(name: string | null | undefined): Registered | undefined;
package/dist/registry.js CHANGED
@@ -6,10 +6,19 @@
6
6
  * the whole point of relations: the same model runs against a database in one service
7
7
  * and against a REST API in another.
8
8
  */
9
+ import { SourceError } from './errors.js';
10
+ /**
11
+ * A dns label, which is what a source's name has to be so it's safe to use in other names:
12
+ * letters, digits and hyphens, 1 to 63 characters, no leading or trailing hyphen.
13
+ */
14
+ export const DNS = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/i;
9
15
  /** Every registered source, keyed by name. */
10
16
  export const SOURCES = new Map();
11
- /** Register a source under its name, replacing any source already there. */
17
+ /** Register a source under its name, replacing any source already there. The name has to be a dns label. */
12
18
  export function register(source) {
19
+ if (typeof source.name !== 'string' || !DNS.test(source.name)) {
20
+ throw new SourceError(`source ${source.name} is not dns compliant`);
21
+ }
13
22
  SOURCES.set(source.name, source);
14
23
  }
15
24
  /** The source registered under a name, or `undefined`. */
@@ -32,6 +32,11 @@ export interface OneToOptions {
32
32
  parentId?: string | number;
33
33
  /** Field on the child that points at the parent. Inferred by convention. */
34
34
  childParentRef?: string | number;
35
+ /**
36
+ * A dict field on the child to store the parent id in, so the child needs no field of its own
37
+ * for it. The relation adds that field, at `<childInject>__relations__<parent>__<id>`.
38
+ */
39
+ childInject?: string;
35
40
  }
36
41
  /** One parent record to one or many child records. */
37
42
  export declare class OneTo extends Relation {
@@ -49,6 +54,8 @@ export declare class OneTo extends Relation {
49
54
  childParentRef: string;
50
55
  /** Attribute on the child that reaches the parent. */
51
56
  childParentAttr: string;
57
+ /** The dict field in the child the parent id is stored in, if it's not a field of its own. */
58
+ childInject: string | null;
52
59
  /** Whether the child side holds one record or many. */
53
60
  MODE: 'one' | 'many';
54
61
  constructor(Parent: ModelClass, Child: ModelClass, options?: OneToOptions);
package/dist/relation.js CHANGED
@@ -12,6 +12,9 @@
12
12
  * ```
13
13
  */
14
14
  import { ModelError } from './errors.js';
15
+ import { Field } from './field.js';
16
+ import { dict } from './kinds.js';
17
+ import { DNS } from './registry.js';
15
18
  /** Base for every relation, holding the convention used to find the joining field. */
16
19
  export class Relation {
17
20
  /** Whether the two sides can share an id field name. */
@@ -55,6 +58,8 @@ export class OneTo extends Relation {
55
58
  childParentRef;
56
59
  /** Attribute on the child that reaches the parent. */
57
60
  childParentAttr;
61
+ /** The dict field in the child the parent id is stored in, if it's not a field of its own. */
62
+ childInject;
58
63
  /** Whether the child side holds one record or many. */
59
64
  MODE;
60
65
  constructor(Parent, Child, options = {}) {
@@ -63,14 +68,48 @@ export class OneTo extends Relation {
63
68
  this.Parent = Parent;
64
69
  this.Child = Child;
65
70
  this.MODE = kind.MODE;
71
+ this.childInject = options.childInject ?? null;
66
72
  const parent = Parent.thy();
67
- const child = Child.thy();
73
+ let child = Child.thy();
68
74
  this.parentId = parent._fieldName(options.parentId ?? parent._id);
69
75
  this.parentChildAttr = options.parentChildAttr ?? child.NAME;
76
+ let childParentRef = options.childParentRef;
77
+ // If asked, add the child field for the parent id, stored in a dict field of the child.
78
+ if (this.childInject !== null) {
79
+ const stored = child._fields._names.get(this.childInject);
80
+ if (stored === undefined) {
81
+ throw new ModelError(child, `cannot find field ${this.childInject} in ${child.NAME}`);
82
+ }
83
+ if (stored.kind !== dict) {
84
+ throw new ModelError(child, `field ${this.childInject} not a dict in ${child.NAME}`);
85
+ }
86
+ // Same source is just the model name, else prefix the source of the parent (a dns label).
87
+ let named = parent.NAME;
88
+ if (parent.SOURCE !== child.SOURCE) {
89
+ if (typeof parent.SOURCE !== 'string' || !DNS.test(parent.SOURCE)) {
90
+ throw new ModelError(parent, `source ${parent.SOURCE} is not dns compliant`);
91
+ }
92
+ named = `${parent.SOURCE.toLowerCase().replace(/-/g, '_')}_${parent.NAME}`;
93
+ }
94
+ const ref = childParentRef !== undefined ? String(childParentRef) : `${named}_${this.parentId}`;
95
+ if (child._fields.has(ref)) {
96
+ throw new ModelError(child, `field ${ref} already exists in ${child.NAME}`);
97
+ }
98
+ const kindOfParent = parent._fields._names.get(this.parentId).kind;
99
+ Child.fields = {
100
+ ...Child.fields,
101
+ [ref]: new Field(kindOfParent, {
102
+ inject: `${this.childInject}__relations__${named}__${this.parentId}`,
103
+ none: true
104
+ })
105
+ };
106
+ childParentRef = ref;
107
+ child = Child.thy();
108
+ }
70
109
  this.childParentAttr = options.childParentAttr ?? parent.NAME;
71
110
  this.childParentRef =
72
- options.childParentRef !== undefined
73
- ? child._fieldName(options.childParentRef)
111
+ childParentRef !== undefined
112
+ ? child._fieldName(childParentRef)
74
113
  : Relation.relativeField(parent, child, kind.SAME);
75
114
  this.Parent._child(this);
76
115
  this.Child._parent(this);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@relations-dil/relations",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A simple, flexible DIL (data interface layer). Define models once, independent of backend; set the source at runtime.",
5
5
  "license": "MIT",
6
6
  "repository": {