@everystack/model 0.3.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 +681 -0
- package/README.md +146 -0
- package/package.json +40 -0
- package/src/can.ts +36 -0
- package/src/constraints.ts +90 -0
- package/src/derive.ts +130 -0
- package/src/field.ts +193 -0
- package/src/index.ts +28 -0
- package/src/model.ts +68 -0
- package/src/raw-sql.ts +16 -0
- package/src/relations.ts +48 -0
package/src/model.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `defineModel` — Layer 2. One call colocates everything about a table: its
|
|
3
|
+
* fields, its authorization (`abilities`), its mutation hooks, and its relations.
|
|
4
|
+
*
|
|
5
|
+
* Functional, not `class extends Model`: best inference, composes with the
|
|
6
|
+
* functional `field`/`can` builders, and structurally resists the fat-model trap
|
|
7
|
+
* (a model *declares*; behavior lives in the functions it references).
|
|
8
|
+
*
|
|
9
|
+
* Phase 0 returns a plain descriptor. Later phases read it from two directions —
|
|
10
|
+
* the handler-config derivation (now) and the abilities→contract compiler (Phase 1).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { FieldBuilder } from './field';
|
|
14
|
+
import type { Ability } from './can';
|
|
15
|
+
import type { Relation } from './relations';
|
|
16
|
+
import type { TableConstraint } from './constraints';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Per-table mutation hooks. Imperative function bodies the framework cannot
|
|
20
|
+
* absorb (purge an S3 object on delete, stamp a column on create) — held
|
|
21
|
+
* verbatim and passed through to the handler's `hooks`.
|
|
22
|
+
*/
|
|
23
|
+
export interface ModelHooks {
|
|
24
|
+
beforeCreate?: (...args: any[]) => any;
|
|
25
|
+
afterCreate?: (...args: any[]) => any;
|
|
26
|
+
beforeUpdate?: (...args: any[]) => any;
|
|
27
|
+
afterUpdate?: (...args: any[]) => any;
|
|
28
|
+
beforeDelete?: (...args: any[]) => any;
|
|
29
|
+
afterDelete?: (...args: any[]) => any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ModelDefinition {
|
|
33
|
+
fields: Record<string, FieldBuilder>;
|
|
34
|
+
abilities?: Ability[];
|
|
35
|
+
hooks?: ModelHooks;
|
|
36
|
+
relations?: Record<string, Relation>;
|
|
37
|
+
/** Table-level constraints the column-scoped `field()` can't express — composite `unique`, multi-column `check`. */
|
|
38
|
+
constraints?: TableConstraint[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface ModelDescriptor {
|
|
42
|
+
/** The Postgres table name (the unit of thought, and of file). */
|
|
43
|
+
table: string;
|
|
44
|
+
fields: Record<string, FieldBuilder>;
|
|
45
|
+
abilities: Ability[];
|
|
46
|
+
hooks?: ModelHooks;
|
|
47
|
+
relations: Record<string, Relation>;
|
|
48
|
+
/** Field names marked `.primaryKey()`, in declaration order (composite-PK aware). */
|
|
49
|
+
primaryKey: string[];
|
|
50
|
+
/** Table-level constraints (composite unique, multi-column check), in declaration order. */
|
|
51
|
+
constraints: TableConstraint[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defineModel(table: string, def: ModelDefinition): ModelDescriptor {
|
|
55
|
+
const primaryKey = Object.entries(def.fields)
|
|
56
|
+
.filter(([, f]) => f.spec.isPrimaryKey)
|
|
57
|
+
.map(([name]) => name);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
table,
|
|
61
|
+
fields: def.fields,
|
|
62
|
+
abilities: def.abilities ?? [],
|
|
63
|
+
hooks: def.hooks,
|
|
64
|
+
relations: def.relations ?? {},
|
|
65
|
+
primaryKey,
|
|
66
|
+
constraints: def.constraints ?? [],
|
|
67
|
+
};
|
|
68
|
+
}
|
package/src/raw-sql.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sql\`…\`` — a raw SQL fragment, branded so the constraint builders accept only an
|
|
3
|
+
* explicit SQL predicate (never a bare string by accident). Interpolated values are
|
|
4
|
+
* spliced in verbatim: this is author-time model SQL (the developer's own predicate),
|
|
5
|
+
* not a runtime query over untrusted input, so there is no parameterization here.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface RawSql {
|
|
9
|
+
readonly sql: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function sql(strings: TemplateStringsArray, ...values: unknown[]): RawSql {
|
|
13
|
+
let out = strings[0];
|
|
14
|
+
for (let i = 0; i < values.length; i++) out += String(values[i]) + strings[i + 1];
|
|
15
|
+
return { sql: out.trim() };
|
|
16
|
+
}
|
package/src/relations.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Relation declarations. `belongsTo` / `hasMany` mirror the handler's
|
|
3
|
+
* RelationConfig ({ table, from, to, many }), resolved by the derivation.
|
|
4
|
+
*
|
|
5
|
+
* belongsTo(() => Author, 'authorId') // this.authorId → Author.<pk>
|
|
6
|
+
* hasMany(() => Comment, 'postId') // this.<pk> → Comment.postId
|
|
7
|
+
*
|
|
8
|
+
* The target is a thunk so models can reference each other regardless of file or
|
|
9
|
+
* declaration order.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ModelDescriptor } from './model';
|
|
13
|
+
|
|
14
|
+
export interface Relation {
|
|
15
|
+
kind: 'belongsTo' | 'hasMany';
|
|
16
|
+
/** Thunk to the related model (forward-reference safe). */
|
|
17
|
+
target: () => ModelDescriptor;
|
|
18
|
+
/**
|
|
19
|
+
* The foreign-key column.
|
|
20
|
+
* - belongsTo: the FK on THIS table (becomes RelationConfig.from)
|
|
21
|
+
* - hasMany: the FK on the TARGET table (becomes RelationConfig.to)
|
|
22
|
+
*/
|
|
23
|
+
column: string;
|
|
24
|
+
/**
|
|
25
|
+
* The key on the other side, when it is not the other table's primary key.
|
|
26
|
+
* - belongsTo: overrides RelationConfig.to (default: target pk)
|
|
27
|
+
* - hasMany: overrides RelationConfig.from (default: this table's pk)
|
|
28
|
+
*/
|
|
29
|
+
references?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Many-to-one: this row points at one target row via `column`. */
|
|
33
|
+
export function belongsTo(
|
|
34
|
+
target: () => ModelDescriptor,
|
|
35
|
+
column: string,
|
|
36
|
+
references?: string,
|
|
37
|
+
): Relation {
|
|
38
|
+
return { kind: 'belongsTo', target, column, references };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** One-to-many: target rows point back at this row via `column`. */
|
|
42
|
+
export function hasMany(
|
|
43
|
+
target: () => ModelDescriptor,
|
|
44
|
+
column: string,
|
|
45
|
+
references?: string,
|
|
46
|
+
): Relation {
|
|
47
|
+
return { kind: 'hasMany', target, column, references };
|
|
48
|
+
}
|