@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.
@@ -0,0 +1,59 @@
1
+ /**
2
+ * relations - a simple, flexible DIL (data interface layer).
3
+ *
4
+ * Define a model once, independent of any backend. Point it at a source at runtime. The
5
+ * same model description drives a database in one service and a REST API in the next.
6
+ *
7
+ * ```ts
8
+ * import { Model, OneToMany, int, str } from '@relations-dil/relations'
9
+ * import { MockSource } from '@relations-dil/relations/mock'
10
+ *
11
+ * class Base extends Model {
12
+ * static source = 'example'
13
+ * }
14
+ *
15
+ * class Unit extends Base {
16
+ * static fields = { id: int, name: str }
17
+ * }
18
+ *
19
+ * class Test extends Base {
20
+ * static fields = { id: int, unit_id: int, name: str }
21
+ * }
22
+ *
23
+ * new OneToMany(Unit, Test)
24
+ * new MockSource('example')
25
+ *
26
+ * await new Unit('yep').create()
27
+ *
28
+ * const unit = await Unit.one({ name: 'yep' }).retrieve()
29
+ * unit.id // 1
30
+ * ```
31
+ */
32
+ import * as overscore from './overscore.js';
33
+ export { overscore };
34
+ export { FieldError, MigrationsError, ModelError, RecordError, UniqueError } from './errors.js';
35
+ export { OverscoreError } from './overscore.js';
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
+ export { Field, OPERATORS, RESERVED, type Attr, type FieldOptions, type Init, type Validation } from './field.js';
38
+ export { Record, type Action } from './record.js';
39
+ export { Model, ModelIdentity, construct, toField, type FieldSpec, type ModelAction, type ModelClass, type ModelMode, type ModelRole } from './model.js';
40
+ export { ManyToMany, OneTo, OneToMany, OneToOne, Relation, type ManyToManyOptions, type OneToOptions } from './relation.js';
41
+ export { Source, lookup, type Query } from './source.js';
42
+ export { Titles } from './titles.js';
43
+ export { Migrations, pretty, type Renamer } from './migrations.js';
44
+ export { MockQuery, MockSource } from './mock.js';
45
+ export { fields, type Listed, type Typed, type TypedClass, type ValueOfKind, type ValueOfSpec, type Values } from './typed.js';
46
+ export { definitionsOf, modelFrom, modelsFrom, type Built, type Definition, type RelationDefinition } from './dynamic.js';
47
+ export { SOURCES, clear, register, source, unregister, type Registered } from './registry.js';
48
+ import type { ModelClass } from './model.js';
49
+ /**
50
+ * Every model class exported by a module, for handing to migrations.
51
+ *
52
+ * ```ts
53
+ * import * as schema from './models.js'
54
+ *
55
+ * await new Migrations('ddl').generate(models(schema))
56
+ * ```
57
+ */
58
+ export declare function models(module: globalThis.Record<string, any>, fromBase?: any): ModelClass[];
59
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,75 @@
1
+ /**
2
+ * relations - a simple, flexible DIL (data interface layer).
3
+ *
4
+ * Define a model once, independent of any backend. Point it at a source at runtime. The
5
+ * same model description drives a database in one service and a REST API in the next.
6
+ *
7
+ * ```ts
8
+ * import { Model, OneToMany, int, str } from '@relations-dil/relations'
9
+ * import { MockSource } from '@relations-dil/relations/mock'
10
+ *
11
+ * class Base extends Model {
12
+ * static source = 'example'
13
+ * }
14
+ *
15
+ * class Unit extends Base {
16
+ * static fields = { id: int, name: str }
17
+ * }
18
+ *
19
+ * class Test extends Base {
20
+ * static fields = { id: int, unit_id: int, name: str }
21
+ * }
22
+ *
23
+ * new OneToMany(Unit, Test)
24
+ * new MockSource('example')
25
+ *
26
+ * await new Unit('yep').create()
27
+ *
28
+ * const unit = await Unit.one({ name: 'yep' }).retrieve()
29
+ * unit.id // 1
30
+ * ```
31
+ */
32
+ import * as overscore from './overscore.js';
33
+ export { overscore };
34
+ export { FieldError, MigrationsError, ModelError, RecordError, UniqueError } from './errors.js';
35
+ export { OverscoreError } from './overscore.js';
36
+ export { bool, builtin, container, dict, float, int, kindName, kindOf, list, scalar, set, str, CONTAINERS, KINDS, SCALARS } from './kinds.js';
37
+ export { Field, OPERATORS, RESERVED } from './field.js';
38
+ export { Record } from './record.js';
39
+ export { Model, ModelIdentity, construct, toField } from './model.js';
40
+ export { ManyToMany, OneTo, OneToMany, OneToOne, Relation } from './relation.js';
41
+ export { Source, lookup } from './source.js';
42
+ export { Titles } from './titles.js';
43
+ export { Migrations, pretty } from './migrations.js';
44
+ export { MockQuery, MockSource } from './mock.js';
45
+ export { fields } from './typed.js';
46
+ export { definitionsOf, modelFrom, modelsFrom } from './dynamic.js';
47
+ export { SOURCES, clear, register, source, unregister } from './registry.js';
48
+ import { Model as ModelBase } from './model.js';
49
+ /**
50
+ * Every model class exported by a module, for handing to migrations.
51
+ *
52
+ * ```ts
53
+ * import * as schema from './models.js'
54
+ *
55
+ * await new Migrations('ddl').generate(models(schema))
56
+ * ```
57
+ */
58
+ export function models(module, fromBase = ModelBase) {
59
+ const found = [];
60
+ for (const value of Object.values(module)) {
61
+ if (typeof value !== 'function' || value === fromBase) {
62
+ continue;
63
+ }
64
+ let prototype = Object.getPrototypeOf(value);
65
+ while (prototype) {
66
+ if (prototype === fromBase) {
67
+ found.push(value);
68
+ break;
69
+ }
70
+ prototype = Object.getPrototypeOf(prototype);
71
+ }
72
+ }
73
+ return found;
74
+ }
75
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Field kinds.
3
+ *
4
+ * Python has `int`, `str`, `dict` and friends as first class values you can hand to a
5
+ * field. JavaScript doesn't, so relations exports the same seven names as sentinels:
6
+ *
7
+ * ```ts
8
+ * import { Model, int, str, list } from '@relations-dil/relations'
9
+ *
10
+ * class Unit extends Model {
11
+ * static source = 'example'
12
+ * static fields = { id: int, name: str, tags: list }
13
+ * }
14
+ * ```
15
+ *
16
+ * A field can also take a class constructor as its kind, in which case the field needs
17
+ * `attr` to say how to flatten instances into storable values.
18
+ */
19
+ /** One of the seven built-in kinds. */
20
+ export interface Kind {
21
+ /** Name used in definitions and migrations. */
22
+ readonly name: 'bool' | 'int' | 'float' | 'str' | 'set' | 'list' | 'dict';
23
+ /** Whether a value is already of this kind. */
24
+ check(value: unknown): boolean;
25
+ /** Coerce a value to this kind, throwing when it can't be done. */
26
+ cast(value: unknown): any;
27
+ /** A fresh empty value, for the container kinds. */
28
+ empty(): any;
29
+ }
30
+ /** A class usable as a field kind. Such a field must also declare `attr`. */
31
+ export type KindClass = abstract new (...args: any[]) => any;
32
+ /** Anything acceptable as a field's kind. */
33
+ export type FieldKind = Kind | KindClass;
34
+ /** Boolean. Casts the strings `false`, `no` and `0` to `false`, everything else by truthiness. */
35
+ export declare const bool: Readonly<{
36
+ name: "bool";
37
+ check: (value: unknown) => value is boolean;
38
+ cast(value: unknown): boolean;
39
+ empty: () => false;
40
+ }>;
41
+ /** Whole number. Truncates toward zero, the way Python's `int()` does. */
42
+ export declare const int: Readonly<{
43
+ name: "int";
44
+ check: (value: unknown) => boolean;
45
+ cast(value: unknown): number;
46
+ empty: () => 0;
47
+ }>;
48
+ /** Number, whole or not. */
49
+ export declare const float: Readonly<{
50
+ name: "float";
51
+ check: (value: unknown) => boolean;
52
+ cast(value: unknown): number;
53
+ empty: () => 0;
54
+ }>;
55
+ /** String. */
56
+ export declare const str: Readonly<{
57
+ name: "str";
58
+ check: (value: unknown) => value is string;
59
+ cast(value: unknown): string;
60
+ empty: () => "";
61
+ }>;
62
+ /** A `Set`. Exports as a sorted array (or in `options` order when options are set). */
63
+ export declare const set: Readonly<{
64
+ name: "set";
65
+ check: (value: unknown) => value is Set<any>;
66
+ cast(value: unknown): Set<any>;
67
+ empty: () => Set<unknown>;
68
+ }>;
69
+ /** An array. */
70
+ export declare const list: Readonly<{
71
+ name: "list";
72
+ check: (value: unknown) => value is any[];
73
+ cast(value: unknown): any[];
74
+ empty: () => never[];
75
+ }>;
76
+ /** A plain object. */
77
+ export declare const dict: Readonly<{
78
+ name: "dict";
79
+ check: (value: unknown) => value is Record<string, unknown>;
80
+ cast(value: unknown): any;
81
+ empty: () => {};
82
+ }>;
83
+ /** All built-in kinds, keyed by name. */
84
+ export declare const KINDS: Record<string, Kind>;
85
+ /** The kinds that hold a single scalar value, with no interior path to reach into. */
86
+ export declare const SCALARS: Kind[];
87
+ /** The kinds that hold other values. */
88
+ export declare const CONTAINERS: Kind[];
89
+ /** Whether `kind` is one of the seven built-ins rather than a class. */
90
+ export declare function builtin(kind: FieldKind): kind is Kind;
91
+ /** The name of a kind, whether built-in or a class. */
92
+ export declare function kindName(kind: FieldKind): string;
93
+ /** Whether `kind` is a scalar built-in. */
94
+ export declare function scalar(kind: FieldKind): boolean;
95
+ /** Whether `kind` is a container built-in. */
96
+ export declare function container(kind: FieldKind): boolean;
97
+ /** Look a built-in kind up by name, for reading back definitions. */
98
+ export declare function kindOf(name: string): Kind | undefined;
99
+ //# sourceMappingURL=kinds.d.ts.map
package/dist/kinds.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Field kinds.
3
+ *
4
+ * Python has `int`, `str`, `dict` and friends as first class values you can hand to a
5
+ * field. JavaScript doesn't, so relations exports the same seven names as sentinels:
6
+ *
7
+ * ```ts
8
+ * import { Model, int, str, list } from '@relations-dil/relations'
9
+ *
10
+ * class Unit extends Model {
11
+ * static source = 'example'
12
+ * static fields = { id: int, name: str, tags: list }
13
+ * }
14
+ * ```
15
+ *
16
+ * A field can also take a class constructor as its kind, in which case the field needs
17
+ * `attr` to say how to flatten instances into storable values.
18
+ */
19
+ import { sorted } from './util.js';
20
+ import { isDict } from './overscore.js';
21
+ class CastError extends Error {
22
+ }
23
+ function fail(value, kind) {
24
+ throw new CastError(`cannot cast ${JSON.stringify(value) ?? String(value)} to ${kind}`);
25
+ }
26
+ /** Boolean. Casts the strings `false`, `no` and `0` to `false`, everything else by truthiness. */
27
+ export const bool = Object.freeze({
28
+ name: 'bool',
29
+ check: (value) => typeof value === 'boolean',
30
+ cast(value) {
31
+ if (typeof value === 'string') {
32
+ const lowered = value.toLowerCase();
33
+ if (['false', 'no', '0', ''].includes(lowered)) {
34
+ return false;
35
+ }
36
+ return true;
37
+ }
38
+ return Boolean(value);
39
+ },
40
+ empty: () => false
41
+ });
42
+ /** Whole number. Truncates toward zero, the way Python's `int()` does. */
43
+ export const int = Object.freeze({
44
+ name: 'int',
45
+ check: (value) => typeof value === 'number' && Number.isInteger(value),
46
+ cast(value) {
47
+ if (typeof value === 'boolean') {
48
+ return Number(value);
49
+ }
50
+ const number = Number(value);
51
+ if (typeof value === 'object' || value === '' || !Number.isFinite(number)) {
52
+ fail(value, 'int');
53
+ }
54
+ return Math.trunc(number);
55
+ },
56
+ empty: () => 0
57
+ });
58
+ /** Number, whole or not. */
59
+ export const float = Object.freeze({
60
+ name: 'float',
61
+ check: (value) => typeof value === 'number' && Number.isFinite(value),
62
+ cast(value) {
63
+ if (typeof value === 'boolean') {
64
+ return Number(value);
65
+ }
66
+ const number = Number(value);
67
+ if (typeof value === 'object' || value === '' || !Number.isFinite(number)) {
68
+ fail(value, 'float');
69
+ }
70
+ return number;
71
+ },
72
+ empty: () => 0
73
+ });
74
+ /** String. */
75
+ export const str = Object.freeze({
76
+ name: 'str',
77
+ check: (value) => typeof value === 'string',
78
+ cast(value) {
79
+ if (value === null || value === undefined) {
80
+ fail(value, 'str');
81
+ }
82
+ if (typeof value === 'object') {
83
+ return JSON.stringify(value);
84
+ }
85
+ return String(value);
86
+ },
87
+ empty: () => ''
88
+ });
89
+ /** A `Set`. Exports as a sorted array (or in `options` order when options are set). */
90
+ export const set = Object.freeze({
91
+ name: 'set',
92
+ check: (value) => value instanceof Set,
93
+ cast(value) {
94
+ if (value instanceof Set) {
95
+ return value;
96
+ }
97
+ if (Array.isArray(value)) {
98
+ return new Set(value);
99
+ }
100
+ if (typeof value === 'string') {
101
+ return new Set([value]);
102
+ }
103
+ if (value && typeof value[Symbol.iterator] === 'function') {
104
+ return new Set(value);
105
+ }
106
+ return fail(value, 'set');
107
+ },
108
+ empty: () => new Set()
109
+ });
110
+ /** An array. */
111
+ export const list = Object.freeze({
112
+ name: 'list',
113
+ check: (value) => Array.isArray(value),
114
+ cast(value) {
115
+ if (Array.isArray(value)) {
116
+ return value;
117
+ }
118
+ if (value instanceof Set) {
119
+ return sorted(value);
120
+ }
121
+ if (value && typeof value === 'object' && typeof value[Symbol.iterator] === 'function') {
122
+ return [...value];
123
+ }
124
+ return fail(value, 'list');
125
+ },
126
+ empty: () => []
127
+ });
128
+ /** A plain object. */
129
+ export const dict = Object.freeze({
130
+ name: 'dict',
131
+ check: (value) => isDict(value),
132
+ cast(value) {
133
+ if (isDict(value)) {
134
+ return value;
135
+ }
136
+ if (value instanceof Map) {
137
+ return Object.fromEntries(value);
138
+ }
139
+ return fail(value, 'dict');
140
+ },
141
+ empty: () => ({})
142
+ });
143
+ /** All built-in kinds, keyed by name. */
144
+ export const KINDS = Object.freeze({ bool, int, float, str, set, list, dict });
145
+ /** The kinds that hold a single scalar value, with no interior path to reach into. */
146
+ export const SCALARS = [bool, int, float, str];
147
+ /** The kinds that hold other values. */
148
+ export const CONTAINERS = [set, list, dict];
149
+ /** Whether `kind` is one of the seven built-ins rather than a class. */
150
+ export function builtin(kind) {
151
+ return typeof kind === 'object' && kind !== null && 'check' in kind && 'cast' in kind;
152
+ }
153
+ /** The name of a kind, whether built-in or a class. */
154
+ export function kindName(kind) {
155
+ return builtin(kind) ? kind.name : kind.name || 'object';
156
+ }
157
+ /** Whether `kind` is a scalar built-in. */
158
+ export function scalar(kind) {
159
+ return builtin(kind) && SCALARS.includes(kind);
160
+ }
161
+ /** Whether `kind` is a container built-in. */
162
+ export function container(kind) {
163
+ return builtin(kind) && CONTAINERS.includes(kind);
164
+ }
165
+ /** Look a built-in kind up by name, for reading back definitions. */
166
+ export function kindOf(name) {
167
+ return KINDS[name];
168
+ }
169
+ //# sourceMappingURL=kinds.js.map
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Migrations: diffing model definitions over time, and handing the diffs to a source.
3
+ *
4
+ * Definitions are backend-neutral JSON. A source converts them into whatever it needs -
5
+ * DDL for a database, nothing at all for an API - so the same migration history drives
6
+ * every backend a set of models is used with.
7
+ */
8
+ import { type ModelClass } from './model.js';
9
+ /**
10
+ * Decides which removals are actually renames. Called with the kind of thing being
11
+ * renamed and the names added and removed; returns removed-name to added-name pairs.
12
+ *
13
+ * Python's relations prompts on stdin here. This takes a function instead, so the same
14
+ * code works in a script, a test, or a CLI you write yourself.
15
+ */
16
+ export type Renamer = (name: string, adds: string[], removes: string[]) => globalThis.Record<string, string>;
17
+ /** Diffs model definitions and drives a source through the resulting changes. */
18
+ export declare class Migrations {
19
+ /** Directory the definition and migration files live in. */
20
+ directory: string;
21
+ /** How renames are decided. Defaults to treating nothing as a rename. */
22
+ renamer: Renamer;
23
+ constructor(directory?: string, options?: {
24
+ renamer?: Renamer;
25
+ });
26
+ /** The definition as of the last generate, or an empty one. */
27
+ current(): Promise<globalThis.Record<string, any>>;
28
+ /** Definitions for a set of model classes, keyed by model name. */
29
+ static define(models: ModelClass[]): globalThis.Record<string, any>;
30
+ /** Work out which removals are renames, and take those out of adds and removes. */
31
+ static rename(renamer: Renamer, name: string, adds: string[], removes: string[]): globalThis.Record<string, string>;
32
+ /** A field definition by name. */
33
+ static lookup(name: string, fields: globalThis.Record<string, any>[]): globalThis.Record<string, any> | null;
34
+ /** What changed between two field definitions. */
35
+ static field(current: globalThis.Record<string, any>, define: globalThis.Record<string, any>): globalThis.Record<string, any>;
36
+ /** What changed between two sets of field definitions. */
37
+ static fields(renamer: Renamer, model: string, current: globalThis.Record<string, any>[], define: globalThis.Record<string, any>[]): globalThis.Record<string, any>;
38
+ /** What changed between two sets of indexes. */
39
+ static indexes(renamer: Renamer, model: string, kind: string, current: globalThis.Record<string, string[]>, define: globalThis.Record<string, string[]>): globalThis.Record<string, any>;
40
+ /** What changed between two model definitions. */
41
+ static model(current: globalThis.Record<string, any>, define: globalThis.Record<string, any>, renamer?: Renamer): globalThis.Record<string, any>;
42
+ /** What changed across a whole set of models. */
43
+ static models(current: globalThis.Record<string, any>, define: globalThis.Record<string, any>, renamer?: Renamer): globalThis.Record<string, any>;
44
+ /**
45
+ * Snapshot the current models, writing a migration alongside the previous snapshot.
46
+ * Resolves to whether anything was written.
47
+ */
48
+ generate(models: ModelClass[], stamp?: string): Promise<boolean>;
49
+ /** Where a source's converted files live. */
50
+ sourcePath(name: string): string;
51
+ /** Convert every definition and migration into a source's own form. */
52
+ convert(name: string): Promise<void>;
53
+ /** The migration pairs available for a source. */
54
+ list(name: string): Promise<globalThis.Record<string, any>>;
55
+ /** Load one converted file into a source. */
56
+ load(name: string, fileName: string): Promise<void>;
57
+ /** Bring a source up to date. Resolves to whether anything was applied. */
58
+ apply(name: string): Promise<boolean>;
59
+ }
60
+ /** Sorted-key JSON, so definitions diff cleanly in version control. */
61
+ export declare function pretty(value: any): string;
62
+ //# sourceMappingURL=migrations.d.ts.map