letopis 0.20.3 → 1.0.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/AGENT-CHEATSHEET.en.md +368 -0
- package/AGENT-CHEATSHEET.md +354 -0
- package/CHANGELOG.md +348 -0
- package/MIGRATION.md +190 -0
- package/README.en.md +1937 -0
- package/README.md +1493 -3466
- package/dist/acl.d.ts +26 -50
- package/dist/acl.js +22 -267
- package/dist/admin.d.ts +138 -0
- package/dist/admin.js +170 -0
- package/dist/auth.d.ts +120 -73
- package/dist/auth.js +121 -306
- package/dist/cache.d.ts +73 -0
- package/dist/cache.js +148 -0
- package/dist/chain.d.ts +124 -191
- package/dist/chain.js +369 -551
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +164 -0
- package/dist/demo/booking.d.ts +289 -0
- package/dist/demo/booking.js +159 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.js +70 -0
- package/dist/import.d.ts +179 -0
- package/dist/import.js +792 -0
- package/dist/index.d.ts +172 -26
- package/dist/index.js +304 -178
- package/dist/jsonschema.d.ts +22 -0
- package/dist/jsonschema.js +167 -0
- package/dist/load.d.ts +76 -0
- package/dist/load.js +884 -0
- package/dist/model.d.ts +166 -0
- package/dist/model.js +224 -0
- package/dist/ops.d.ts +7 -6
- package/dist/ops.js +7 -51
- package/dist/pglite.d.ts +22 -0
- package/dist/pglite.js +45 -0
- package/dist/registry.d.ts +57 -0
- package/dist/registry.js +82 -0
- package/dist/sql.d.ts +59 -142
- package/dist/sql.js +568 -654
- package/dist/sync.d.ts +31 -0
- package/dist/sync.js +108 -0
- package/dist/tx.d.ts +129 -8
- package/dist/tx.js +300 -73
- package/dist/typed.d.ts +97 -0
- package/dist/typed.js +1 -0
- package/dist/types.d.ts +71 -250
- package/dist/types.js +27 -108
- package/dist/up.d.ts +140 -47
- package/dist/up.js +339 -267
- package/dist/uuid.d.ts +21 -6
- package/dist/uuid.js +48 -64
- package/dist/validate.d.ts +24 -0
- package/dist/validate.js +251 -0
- package/dist/watch.d.ts +62 -0
- package/dist/watch.js +168 -0
- package/dist/write.d.ts +117 -74
- package/dist/write.js +658 -720
- package/llms.txt +26 -0
- package/package.json +49 -19
- package/sql/10-core.sql +136 -0
- package/sql/15-errors.sql +60 -0
- package/sql/20-context.sql +153 -0
- package/sql/30-validate.sql +423 -0
- package/sql/40-class.sql +259 -0
- package/sql/50-acl.sql +539 -0
- package/sql/60-write.sql +1369 -0
- package/sql/70-read.sql +245 -0
- package/sql/80-auth.sql +827 -0
- package/sql/90-time.sql +957 -0
- package/sql/95-seed.system.sql +178 -0
- package/sql/99-revision.sql +3 -0
- package/sql/README.md +56 -0
- package/sql/seed.booking.sql +39 -112
- package/dist/schema.d.ts +0 -15
- package/dist/schema.js +0 -351
- package/dist/sessions.d.ts +0 -32
- package/dist/sessions.js +0 -114
- package/dist/tables.d.ts +0 -105
- package/dist/tables.js +0 -248
- package/docker/Dockerfile +0 -40
- package/docker/start.sh +0 -18
- package/scripts/check-docs.mjs +0 -375
- package/scripts/gen-api-contract.mjs +0 -226
- package/scripts/gen-types.mjs +0 -350
- package/scripts/release-notes.mjs +0 -76
- package/scripts/schema-sync.mjs +0 -185
- package/sql/ddl.sql +0 -600
- package/sql/seed.auth.sql +0 -73
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Подпуть `letopis/model` (план, §2.3, §2.10, §2.11, §2.12, §2.13, этап 2, п. 1–2): описание классов
|
|
3
|
+
* на Zod 4 и их JSON Schema для строки класса `Class`.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const Org = hub('Org', { fields: z.object({ name: z.string() }), key: ['name'] })
|
|
7
|
+
* const Master = hub('Master', { ends: { Org }, fields: z.object({ name: z.string() }), key: ['Org', 'name'] })
|
|
8
|
+
* const booking = link('booking', { ends: { Staff: one(Master), Customer: one(Customer).optional() }, onDelete: { Customer: 'unset' } })
|
|
9
|
+
* ```
|
|
10
|
+
* Короткая форма `hub('Master', { name: z.string() }, { key: ['name'] })` — поля сразу вторым аргументом.
|
|
11
|
+
* Поля, концы и ключ наследуются по правилу 0.21 «потомок заменяет поле целиком» (`extends`).
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { type JsonSchema } from './jsonschema.js';
|
|
15
|
+
export type OnDelete = 'restrict' | 'cascade' | 'unset';
|
|
16
|
+
/** Свободные метаданные класса (§2.13). Приложение типизирует их, расширяя интерфейс. */
|
|
17
|
+
export interface ClassMeta {
|
|
18
|
+
[key: string]: unknown;
|
|
19
|
+
}
|
|
20
|
+
/** Политика хранения истории (§2.11). */
|
|
21
|
+
export interface HistoryPolicy {
|
|
22
|
+
all: string;
|
|
23
|
+
daily?: string;
|
|
24
|
+
weekly?: string;
|
|
25
|
+
monthly?: string;
|
|
26
|
+
yearly?: string;
|
|
27
|
+
tz?: string;
|
|
28
|
+
}
|
|
29
|
+
type Target = AnyModel | string;
|
|
30
|
+
/** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. */
|
|
31
|
+
export declare class End<Many extends boolean = boolean, Required extends boolean = boolean> {
|
|
32
|
+
readonly targets: readonly Target[];
|
|
33
|
+
readonly many: Many;
|
|
34
|
+
readonly required: Required;
|
|
35
|
+
readonly min?: number;
|
|
36
|
+
readonly max?: number;
|
|
37
|
+
readonly onDeleteRule?: OnDelete;
|
|
38
|
+
readonly metaData?: Record<string, unknown>;
|
|
39
|
+
constructor(targets: readonly Target[], opts: {
|
|
40
|
+
many: Many;
|
|
41
|
+
required: Required;
|
|
42
|
+
min?: number;
|
|
43
|
+
max?: number;
|
|
44
|
+
onDelete?: OnDelete;
|
|
45
|
+
meta?: Record<string, unknown>;
|
|
46
|
+
});
|
|
47
|
+
private with;
|
|
48
|
+
/** Необязательный конец. */
|
|
49
|
+
optional(): End<Many, false>;
|
|
50
|
+
/** Правило при удалении цели: restrict (по умолчанию), cascade, unset. */
|
|
51
|
+
onDelete(rule: OnDelete): End<Many, Required>;
|
|
52
|
+
/** Метаданные конца (§2.13). */
|
|
53
|
+
meta(meta: Record<string, unknown>): End<Many, Required>;
|
|
54
|
+
/** Имена классов целей. */
|
|
55
|
+
classNames(): string[];
|
|
56
|
+
}
|
|
57
|
+
/** Одиночный конец; несколько классов — союз: `one(Service, Product)`. По умолчанию обязательный. */
|
|
58
|
+
export declare function one(...targets: Target[]): End<false, true>;
|
|
59
|
+
/** Множественный конец: список целей в одном конце (§4 предложения). */
|
|
60
|
+
export declare function many(target: Target | readonly Target[], opts?: {
|
|
61
|
+
min?: number;
|
|
62
|
+
max?: number;
|
|
63
|
+
onDelete?: OnDelete;
|
|
64
|
+
}): End<true, boolean>;
|
|
65
|
+
type EndsInput = Record<string, End | AnyModel>;
|
|
66
|
+
type EndOf<E> = E extends End<infer M, infer R> ? End<M, R> : End<false, true>;
|
|
67
|
+
type LinkValue<E> = EndOf<E> extends End<true, boolean> ? string[] : string;
|
|
68
|
+
type LinksOf<E extends EndsInput> = {
|
|
69
|
+
[K in keyof E as EndOf<E[K]> extends End<boolean, true> ? K : never]: LinkValue<E[K]>;
|
|
70
|
+
} & {
|
|
71
|
+
[K in keyof E as EndOf<E[K]> extends End<boolean, true> ? never : K]?: LinkValue<E[K]>;
|
|
72
|
+
};
|
|
73
|
+
/** Описание класса — вторым аргументом hub()/link(). */
|
|
74
|
+
export interface ClassDef<S extends z.ZodRawShape = z.ZodRawShape, E extends EndsInput = EndsInput, P extends AnyModel | undefined = undefined> {
|
|
75
|
+
alias?: string;
|
|
76
|
+
extends?: P;
|
|
77
|
+
abstract?: boolean;
|
|
78
|
+
ends?: E;
|
|
79
|
+
fields?: z.ZodObject<S> | S;
|
|
80
|
+
key?: readonly string[];
|
|
81
|
+
onDelete?: Partial<Record<keyof E & string, OnDelete>>;
|
|
82
|
+
history?: boolean | HistoryPolicy;
|
|
83
|
+
ownerDefault?: 'tenant' | 'actor';
|
|
84
|
+
cache?: {
|
|
85
|
+
ttl?: number;
|
|
86
|
+
};
|
|
87
|
+
meta?: ClassMeta;
|
|
88
|
+
}
|
|
89
|
+
/** Описание класса в строке `Class` (метасхема `class_metaschema()` в lib/sql/40-class.sql). */
|
|
90
|
+
export interface ClassDescription {
|
|
91
|
+
name: string;
|
|
92
|
+
kind: 'hub' | 'link';
|
|
93
|
+
parent?: string;
|
|
94
|
+
abstract?: boolean;
|
|
95
|
+
alias?: string;
|
|
96
|
+
fields: {
|
|
97
|
+
properties: Record<string, JsonSchema>;
|
|
98
|
+
required?: string[];
|
|
99
|
+
};
|
|
100
|
+
ends?: Record<string, {
|
|
101
|
+
class: string | string[];
|
|
102
|
+
many?: boolean;
|
|
103
|
+
required?: boolean;
|
|
104
|
+
min?: number;
|
|
105
|
+
max?: number;
|
|
106
|
+
onDelete?: OnDelete;
|
|
107
|
+
meta?: Record<string, unknown>;
|
|
108
|
+
}>;
|
|
109
|
+
key?: string[];
|
|
110
|
+
history?: boolean | HistoryPolicy;
|
|
111
|
+
ownerDefault?: 'tenant' | 'actor';
|
|
112
|
+
cache?: {
|
|
113
|
+
ttl?: number;
|
|
114
|
+
};
|
|
115
|
+
meta?: ClassMeta;
|
|
116
|
+
}
|
|
117
|
+
/** Правило refine, которое не попадёт в базу: проверяется только в библиотеке. */
|
|
118
|
+
export interface LostRule {
|
|
119
|
+
path: string;
|
|
120
|
+
kind: string;
|
|
121
|
+
}
|
|
122
|
+
type DataOf<P, S extends z.ZodRawShape> = P extends Model<string, infer PD, unknown> ? Omit<PD, keyof z.output<z.ZodObject<S>>> & z.output<z.ZodObject<S>> : z.output<z.ZodObject<S>>;
|
|
123
|
+
type AllLinks<P, E extends EndsInput> = P extends Model<string, unknown, infer PL> ? Omit<PL, keyof E> & LinksOf<E> : LinksOf<E>;
|
|
124
|
+
export interface Model<N extends string = string, D = unknown, L = unknown, A extends string = string> {
|
|
125
|
+
readonly name: N;
|
|
126
|
+
readonly kind: 'hub' | 'link';
|
|
127
|
+
readonly alias?: A;
|
|
128
|
+
readonly parent?: AnyModel;
|
|
129
|
+
readonly abstract: boolean;
|
|
130
|
+
/** Собственные поля класса (без предков). */
|
|
131
|
+
readonly ownShape: z.ZodRawShape;
|
|
132
|
+
/** Собственные концы класса (без предков). */
|
|
133
|
+
readonly ownEnds: Readonly<Record<string, End>>;
|
|
134
|
+
readonly def: ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>;
|
|
135
|
+
/** Поля с учётом предков: Zod-схема данных строки (для ранних ошибок в библиотеке). */
|
|
136
|
+
readonly schema: z.ZodObject<z.ZodRawShape>;
|
|
137
|
+
/** Концы с учётом предков. */
|
|
138
|
+
readonly ends: Readonly<Record<string, End>>;
|
|
139
|
+
/** Ключ с учётом предков ([] — класс без ключа). */
|
|
140
|
+
readonly key: readonly string[];
|
|
141
|
+
/** Описание для строки Class: только собственное, предков база развернёт сама. */
|
|
142
|
+
describe(): ClassDescription;
|
|
143
|
+
/** Правила refine, которых нет в JSON Schema и в базе. */
|
|
144
|
+
lostRules(): LostRule[];
|
|
145
|
+
/** Только для вывода типов. */
|
|
146
|
+
readonly __types?: {
|
|
147
|
+
data: D;
|
|
148
|
+
links: L;
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export type AnyModel = Model<string, any, any, any>;
|
|
152
|
+
/** Тип данных строки класса: `Infer<typeof Master>`. */
|
|
153
|
+
export type Infer<M> = M extends Model<string, infer D, unknown> ? D : never;
|
|
154
|
+
/** Тип концов строки класса. */
|
|
155
|
+
export type InferLinks<M> = M extends Model<string, unknown, infer L> ? L : never;
|
|
156
|
+
type ShapeOf<F> = F extends z.ZodObject<infer S> ? S : F extends z.ZodRawShape ? F : Record<never, never>;
|
|
157
|
+
type AliasOf<D> = D extends {
|
|
158
|
+
alias: infer A extends string;
|
|
159
|
+
} ? A : string;
|
|
160
|
+
/** Хаб — объект (мастер, услуга). */
|
|
161
|
+
export declare function hub<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts: Omit<ClassDef, 'fields'>): Model<N, z.output<z.ZodObject<S>>, Record<never, never>>;
|
|
162
|
+
export declare function hub<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): Model<N, DataOf<D['extends'], ShapeOf<D['fields']>>, AllLinks<D['extends'], D['ends'] extends EndsInput ? D['ends'] : Record<never, never>>, AliasOf<D>>;
|
|
163
|
+
/** Связь — отношение между объектами со своими данными (бронь, навык). */
|
|
164
|
+
export declare function link<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts: Omit<ClassDef, 'fields'>): Model<N, z.output<z.ZodObject<S>>, Record<never, never>>;
|
|
165
|
+
export declare function link<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): Model<N, DataOf<D['extends'], ShapeOf<D['fields']>>, AllLinks<D['extends'], D['ends'] extends EndsInput ? D['ends'] : Record<never, never>>, AliasOf<D>>;
|
|
166
|
+
export { normalizeSchema, translatePattern, type JsonSchema } from './jsonschema.js';
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Подпуть `letopis/model` (план, §2.3, §2.10, §2.11, §2.12, §2.13, этап 2, п. 1–2): описание классов
|
|
3
|
+
* на Zod 4 и их JSON Schema для строки класса `Class`.
|
|
4
|
+
*
|
|
5
|
+
* ```ts
|
|
6
|
+
* const Org = hub('Org', { fields: z.object({ name: z.string() }), key: ['name'] })
|
|
7
|
+
* const Master = hub('Master', { ends: { Org }, fields: z.object({ name: z.string() }), key: ['Org', 'name'] })
|
|
8
|
+
* const booking = link('booking', { ends: { Staff: one(Master), Customer: one(Customer).optional() }, onDelete: { Customer: 'unset' } })
|
|
9
|
+
* ```
|
|
10
|
+
* Короткая форма `hub('Master', { name: z.string() }, { key: ['name'] })` — поля сразу вторым аргументом.
|
|
11
|
+
* Поля, концы и ключ наследуются по правилу 0.21 «потомок заменяет поле целиком» (`extends`).
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { LetopisError } from './errors.js';
|
|
15
|
+
import { normalizeSchema } from './jsonschema.js';
|
|
16
|
+
/** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. */
|
|
17
|
+
export class End {
|
|
18
|
+
targets;
|
|
19
|
+
many;
|
|
20
|
+
required;
|
|
21
|
+
min;
|
|
22
|
+
max;
|
|
23
|
+
onDeleteRule;
|
|
24
|
+
metaData;
|
|
25
|
+
constructor(targets, opts) {
|
|
26
|
+
if (!targets.length)
|
|
27
|
+
throw new LetopisError('invalid_class', 'у конца должен быть хотя бы один класс цели');
|
|
28
|
+
this.targets = targets;
|
|
29
|
+
this.many = opts.many;
|
|
30
|
+
this.required = opts.required;
|
|
31
|
+
this.min = opts.min;
|
|
32
|
+
this.max = opts.max;
|
|
33
|
+
this.onDeleteRule = opts.onDelete;
|
|
34
|
+
this.metaData = opts.meta;
|
|
35
|
+
}
|
|
36
|
+
with(patch) {
|
|
37
|
+
return new End(this.targets, {
|
|
38
|
+
many: (patch.many ?? this.many),
|
|
39
|
+
required: (patch.required ?? this.required),
|
|
40
|
+
min: this.min,
|
|
41
|
+
max: this.max,
|
|
42
|
+
onDelete: patch.onDelete ?? this.onDeleteRule,
|
|
43
|
+
meta: patch.meta ?? this.metaData,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
/** Необязательный конец. */
|
|
47
|
+
optional() { return this.with({ required: false }); }
|
|
48
|
+
/** Правило при удалении цели: restrict (по умолчанию), cascade, unset. */
|
|
49
|
+
onDelete(rule) { return this.with({ onDelete: rule }); }
|
|
50
|
+
/** Метаданные конца (§2.13). */
|
|
51
|
+
meta(meta) { return this.with({ meta }); }
|
|
52
|
+
/** Имена классов целей. */
|
|
53
|
+
classNames() {
|
|
54
|
+
return this.targets.map((t) => (typeof t === 'string' ? t : t.name));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Одиночный конец; несколько классов — союз: `one(Service, Product)`. По умолчанию обязательный. */
|
|
58
|
+
export function one(...targets) {
|
|
59
|
+
return new End(targets, { many: false, required: true });
|
|
60
|
+
}
|
|
61
|
+
/** Множественный конец: список целей в одном конце (§4 предложения). */
|
|
62
|
+
export function many(target, opts = {}) {
|
|
63
|
+
const targets = Array.isArray(target) ? target : [target];
|
|
64
|
+
return new End(targets, { many: true, required: (opts.min ?? 0) > 0, min: opts.min, max: opts.max, onDelete: opts.onDelete });
|
|
65
|
+
}
|
|
66
|
+
const NAME = /^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$/;
|
|
67
|
+
function isZod(v) {
|
|
68
|
+
return typeof v === 'object' && v !== null && '_zod' in v;
|
|
69
|
+
}
|
|
70
|
+
/** Пути правил refine/superRefine в схеме Zod — их нет в JSON Schema. */
|
|
71
|
+
function collectLost(schema, path, out) {
|
|
72
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
|
+
const def = schema._zod.def;
|
|
74
|
+
for (const c of def.checks ?? []) {
|
|
75
|
+
if (c?._zod?.def?.check === 'custom')
|
|
76
|
+
out.push({ path, kind: 'refine' });
|
|
77
|
+
}
|
|
78
|
+
if (def.type === 'object')
|
|
79
|
+
for (const [k, v] of Object.entries(def.shape))
|
|
80
|
+
collectLost(v, `${path}.${k}`, out);
|
|
81
|
+
else if (def.type === 'array')
|
|
82
|
+
collectLost(def.element, `${path}[]`, out);
|
|
83
|
+
else if (def.type === 'record')
|
|
84
|
+
collectLost(def.valueType, `${path}.*`, out);
|
|
85
|
+
else if (def.type === 'union')
|
|
86
|
+
def.options.forEach((o, i) => collectLost(o, `${path}|${i}`, out));
|
|
87
|
+
else if (def.type === 'pipe')
|
|
88
|
+
out.push({ path, kind: 'transform' });
|
|
89
|
+
if (def.innerType)
|
|
90
|
+
collectLost(def.innerType, path, out);
|
|
91
|
+
}
|
|
92
|
+
function makeModel(kind, name, defArg, optsArg) {
|
|
93
|
+
if (!NAME.test(name))
|
|
94
|
+
throw new LetopisError('invalid_class', `имя класса ${name} недопустимо`);
|
|
95
|
+
// короткая форма: hub(name, { поле: z…, … }, { key, meta, … })
|
|
96
|
+
let def;
|
|
97
|
+
const isShape = defArg && typeof defArg === 'object' && Object.values(defArg).length > 0 && Object.values(defArg).every(isZod);
|
|
98
|
+
if (optsArg !== undefined || isShape) {
|
|
99
|
+
def = { ...optsArg, fields: (defArg ?? {}) };
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
def = (defArg ?? {});
|
|
103
|
+
}
|
|
104
|
+
const fields = def.fields;
|
|
105
|
+
const ownShape = !fields ? {} : fields instanceof z.ZodObject ? fields.shape : fields;
|
|
106
|
+
for (const k of Object.keys(ownShape)) {
|
|
107
|
+
if (k.startsWith('$'))
|
|
108
|
+
throw new LetopisError('invalid_class', `${name}: имя поля ${k} начинается с $ — так загрузчик помечает служебные ключи`);
|
|
109
|
+
}
|
|
110
|
+
const ownEnds = {};
|
|
111
|
+
for (const [role, v] of Object.entries(def.ends ?? {})) {
|
|
112
|
+
if (!NAME.test(role))
|
|
113
|
+
throw new LetopisError('invalid_class', `${name}: имя конца ${role} недопустимо`);
|
|
114
|
+
let end = v instanceof End ? v : one(v);
|
|
115
|
+
const rule = def.onDelete?.[role];
|
|
116
|
+
if (rule)
|
|
117
|
+
end = end.onDelete(rule);
|
|
118
|
+
ownEnds[role] = end;
|
|
119
|
+
}
|
|
120
|
+
const parent = def.extends;
|
|
121
|
+
const shape = { ...(parent?.schema.shape ?? {}), ...ownShape };
|
|
122
|
+
const ends = { ...(parent?.ends ?? {}), ...ownEnds };
|
|
123
|
+
const key = def.key ?? parent?.key ?? [];
|
|
124
|
+
for (const k of key) {
|
|
125
|
+
const f = shape[k];
|
|
126
|
+
if (f) {
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
128
|
+
let zd = f._zod.def;
|
|
129
|
+
// умолчание допустимо (база подставит его до проверки), необязательность — нет
|
|
130
|
+
if (zd.type === 'default')
|
|
131
|
+
zd = zd.innerType._zod.def;
|
|
132
|
+
if (zd.type !== 'string')
|
|
133
|
+
throw new LetopisError('invalid_class', `${name}: поле ключа ${k} должно быть обязательной строкой`);
|
|
134
|
+
}
|
|
135
|
+
else if (ends[k]) {
|
|
136
|
+
if (ends[k].many || !ends[k].required)
|
|
137
|
+
throw new LetopisError('invalid_class', `${name}: конец ключа ${k} должен быть обязательным и одиночным`);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
throw new LetopisError('invalid_class', `${name}: ключ ссылается на неизвестное поле или конец ${k}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
for (const [role, e] of Object.entries(ends)) {
|
|
144
|
+
if (e.onDeleteRule === 'unset' && (e.required || (e.min ?? 0) > 0)) {
|
|
145
|
+
throw new LetopisError('invalid_class', `${name}: onDelete unset недопустим для обязательного конца ${role}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// строгий верхний уровень — как в базе (resolved.schema: additionalProperties false)
|
|
149
|
+
const schema = z.strictObject(shape);
|
|
150
|
+
const model = {
|
|
151
|
+
name,
|
|
152
|
+
kind,
|
|
153
|
+
alias: def.alias,
|
|
154
|
+
parent,
|
|
155
|
+
abstract: def.abstract ?? false,
|
|
156
|
+
ownShape,
|
|
157
|
+
ownEnds,
|
|
158
|
+
def,
|
|
159
|
+
schema,
|
|
160
|
+
ends,
|
|
161
|
+
key,
|
|
162
|
+
describe() {
|
|
163
|
+
const js = normalizeSchema(z.toJSONSchema(z.object(ownShape), { io: 'input' }), { where: name, topLevel: true });
|
|
164
|
+
// поля ключа обязательны и с умолчанием: база подставляет умолчания до проверки (§2.3)
|
|
165
|
+
const required = new Set(Array.isArray(js.required) ? js.required : []);
|
|
166
|
+
for (const k of key)
|
|
167
|
+
if (k in ownShape)
|
|
168
|
+
required.add(k);
|
|
169
|
+
const desc = {
|
|
170
|
+
name,
|
|
171
|
+
kind,
|
|
172
|
+
fields: {
|
|
173
|
+
properties: (js.properties ?? {}),
|
|
174
|
+
...(required.size ? { required: [...required] } : {}),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
if (parent)
|
|
178
|
+
desc.parent = parent.name;
|
|
179
|
+
if (def.abstract)
|
|
180
|
+
desc.abstract = true;
|
|
181
|
+
if (def.alias)
|
|
182
|
+
desc.alias = def.alias;
|
|
183
|
+
if (Object.keys(ownEnds).length) {
|
|
184
|
+
desc.ends = Object.fromEntries(Object.entries(ownEnds).map(([role, e]) => {
|
|
185
|
+
const cls = e.classNames();
|
|
186
|
+
return [role, {
|
|
187
|
+
class: cls.length === 1 ? cls[0] : cls,
|
|
188
|
+
...(e.many ? { many: true } : {}),
|
|
189
|
+
...(e.required ? { required: true } : {}),
|
|
190
|
+
...(e.min !== undefined ? { min: e.min } : {}),
|
|
191
|
+
...(e.max !== undefined ? { max: e.max } : {}),
|
|
192
|
+
...(e.onDeleteRule ? { onDelete: e.onDeleteRule } : {}),
|
|
193
|
+
...(e.metaData ? { meta: e.metaData } : {}),
|
|
194
|
+
}];
|
|
195
|
+
}));
|
|
196
|
+
}
|
|
197
|
+
if (def.key)
|
|
198
|
+
desc.key = [...def.key];
|
|
199
|
+
if (def.history !== undefined)
|
|
200
|
+
desc.history = def.history;
|
|
201
|
+
if (def.ownerDefault)
|
|
202
|
+
desc.ownerDefault = def.ownerDefault;
|
|
203
|
+
if (def.cache)
|
|
204
|
+
desc.cache = def.cache;
|
|
205
|
+
if (def.meta)
|
|
206
|
+
desc.meta = def.meta;
|
|
207
|
+
return desc;
|
|
208
|
+
},
|
|
209
|
+
lostRules() {
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const [k, v] of Object.entries(ownShape))
|
|
212
|
+
collectLost(v, `${name}.${k}`, out);
|
|
213
|
+
return out;
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
return model;
|
|
217
|
+
}
|
|
218
|
+
export function hub(name, def, opts) {
|
|
219
|
+
return makeModel('hub', name, def, opts);
|
|
220
|
+
}
|
|
221
|
+
export function link(name, def, opts) {
|
|
222
|
+
return makeModel('link', name, def, opts);
|
|
223
|
+
}
|
|
224
|
+
export { normalizeSchema, translatePattern } from './jsonschema.js';
|
package/dist/ops.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Операторы
|
|
3
|
-
*
|
|
2
|
+
* Операторы фильтров (план, этап 3, п. 5) — 18 фабрик 0.21 (`lib/src/ops.ts` тега v0.21.0) без
|
|
3
|
+
* изменений смысла. Узел помечен символом Symbol.for('letopis.op'): из JSON его не получить,
|
|
4
|
+
* а вторая копия пакета в том же процессе его узнаёт. Приведение по типу листа делает sql.ts.
|
|
4
5
|
*/
|
|
5
6
|
import { type Op, type Scalar } from './types.js';
|
|
6
7
|
export declare const isOp: (v: unknown) => v is Op;
|
|
@@ -32,11 +33,11 @@ export declare const has: (v: Scalar) => Op;
|
|
|
32
33
|
export declare const hasAny: (vs: Scalar[]) => Op;
|
|
33
34
|
/** массив-поле содержит все */
|
|
34
35
|
export declare const hasAll: (vs: Scalar[]) => Op;
|
|
35
|
-
/** поле присутствует (true)
|
|
36
|
+
/** поле присутствует (true) или отсутствует (false) */
|
|
36
37
|
export declare const exists: (yes?: boolean) => Op;
|
|
37
|
-
/** поле
|
|
38
|
+
/** поле null или отсутствует */
|
|
38
39
|
export declare const isNull: () => Op;
|
|
39
|
-
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр
|
|
40
|
+
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр — «не равно» */
|
|
40
41
|
export declare const not: (v: Scalar | Op) => Op;
|
|
41
|
-
/** ИЛИ на уровне фильтра шага:
|
|
42
|
+
/** ИЛИ на уровне фильтра шага: t.booking(or({ notes: 'a' }, { notes: 'b' })) */
|
|
42
43
|
export declare const or: (...filters: Record<string, unknown>[]) => Op;
|
package/dist/ops.js
CHANGED
|
@@ -1,45 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Операторы
|
|
3
|
-
*
|
|
2
|
+
* Операторы фильтров (план, этап 3, п. 5) — 18 фабрик 0.21 (`lib/src/ops.ts` тега v0.21.0) без
|
|
3
|
+
* изменений смысла. Узел помечен символом Symbol.for('letopis.op'): из JSON его не получить,
|
|
4
|
+
* а вторая копия пакета в том же процессе его узнаёт. Приведение по типу листа делает sql.ts.
|
|
4
5
|
*/
|
|
5
|
-
//
|
|
6
|
-
// FILE: lib/src/ops.ts
|
|
7
|
-
// VERSION: 1.0.0
|
|
8
|
-
// START_MODULE_CONTRACT
|
|
9
|
-
// PURPOSE: Билдеры операторов фильтра, создающие узлы Op для DSL фильтров.
|
|
10
|
-
// SCOPE: type guard isOp + конструкторы операторов сравнения / списков / строк / массивов / наличия / логики (ne…or) поверх локальной фабрики op().
|
|
11
|
-
// DEPENDS: M-TYPES
|
|
12
|
-
// LINKS: M-OPS, V-M-OPS
|
|
13
|
-
// ROLE: RUNTIME
|
|
14
|
-
// MAP_MODE: EXPORTS
|
|
15
|
-
// END_MODULE_CONTRACT
|
|
16
|
-
//
|
|
17
|
-
// START_MODULE_MAP
|
|
18
|
-
// op - (локальный) фабрика узла Op: { [OP]: name, args }.
|
|
19
|
-
// isOp - type guard: значение является узлом Op (содержит символ OP).
|
|
20
|
-
// ne - ≠ (IS DISTINCT FROM).
|
|
21
|
-
// gt - >.
|
|
22
|
-
// gte - ≥.
|
|
23
|
-
// lt - <.
|
|
24
|
-
// lte - ≤.
|
|
25
|
-
// between - a ≤ x ≤ b.
|
|
26
|
-
// inList - значение из списка (IN).
|
|
27
|
-
// like - LIKE (шаблон с % и _).
|
|
28
|
-
// ilike - ILIKE (без регистра).
|
|
29
|
-
// starts - начинается с (LIKE s%).
|
|
30
|
-
// ends - заканчивается на (LIKE %s).
|
|
31
|
-
// has - массив-поле содержит значение.
|
|
32
|
-
// hasAny - массив-поле содержит хотя бы одно из.
|
|
33
|
-
// hasAll - массив-поле содержит все.
|
|
34
|
-
// exists - поле присутствует (true) / отсутствует (false) в data.
|
|
35
|
-
// isNull - поле NULL или отсутствует.
|
|
36
|
-
// not - НЕ-условие по полю (скаляр → «не равно»).
|
|
37
|
-
// or - ИЛИ на уровне фильтра шага.
|
|
38
|
-
// END_MODULE_MAP
|
|
39
|
-
//
|
|
40
|
-
// START_CHANGE_SUMMARY
|
|
41
|
-
// LAST_CHANGE: [v1.0.0 - Documented existing module: reverse-engineered contract + markup]
|
|
42
|
-
// END_CHANGE_SUMMARY
|
|
43
6
|
import { OP } from './types.js';
|
|
44
7
|
const op = (name, ...args) => ({ [OP]: name, args });
|
|
45
8
|
export const isOp = (v) => typeof v === 'object' && v !== null && OP in v;
|
|
@@ -71,18 +34,11 @@ export const has = (v) => op('has', v);
|
|
|
71
34
|
export const hasAny = (vs) => op('hasAny', vs);
|
|
72
35
|
/** массив-поле содержит все */
|
|
73
36
|
export const hasAll = (vs) => op('hasAll', vs);
|
|
74
|
-
/** поле присутствует (true)
|
|
37
|
+
/** поле присутствует (true) или отсутствует (false) */
|
|
75
38
|
export const exists = (yes = true) => op('exists', yes);
|
|
76
|
-
/** поле
|
|
39
|
+
/** поле null или отсутствует */
|
|
77
40
|
export const isNull = () => op('isNull');
|
|
78
|
-
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр
|
|
41
|
+
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр — «не равно» */
|
|
79
42
|
export const not = (v) => op('not', v);
|
|
80
|
-
|
|
81
|
-
// PURPOSE: Логическое ИЛИ на уровне фильтра шага — объединяет несколько фильтров-объектов в один узел Op.
|
|
82
|
-
// INPUTS: { filters: Record<string, unknown>[] - варьируемое число фильтров-объектов (каждый — свой набор условий) }
|
|
83
|
-
// OUTPUTS: { Op - узел { [OP]: 'or', args: [filters] } }
|
|
84
|
-
// SIDE_EFFECTS: none (чистая фабрика через op())
|
|
85
|
-
// LINKS: M-OPS, V-M-OPS
|
|
86
|
-
// END_CONTRACT: or
|
|
87
|
-
/** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
|
|
43
|
+
/** ИЛИ на уровне фильтра шага: t.booking(or({ notes: 'a' }, { notes: 'b' })) */
|
|
88
44
|
export const or = (...filters) => op('or', filters);
|
package/dist/pglite.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* База для разработки без установленного PostgreSQL (решение 6 точки А): PGlite (PostgreSQL 18 в
|
|
3
|
+
* WebAssembly) с pgcrypto, доступный postgres.js через pglite-socket. Одно соединение: годится для
|
|
4
|
+
* разработки и быстрых тестов логики, но не для гонок и не вместо postgres:18 в CI; LISTEN из
|
|
5
|
+
* второго соединения нет — connect({ listen: false, max: 1 }).
|
|
6
|
+
*
|
|
7
|
+
* Пакеты @electric-sql/pglite и @electric-sql/pglite-socket — необязательные peer-зависимости:
|
|
8
|
+
* основная точка входа их не импортирует, up({ pglite }) загружает их по требованию.
|
|
9
|
+
*/
|
|
10
|
+
export interface PgliteOptions {
|
|
11
|
+
/** Каталог данных (по умолчанию — в памяти, данные теряются при остановке). */
|
|
12
|
+
dataDir?: string;
|
|
13
|
+
/** Порт сокета (по умолчанию — свободный). */
|
|
14
|
+
port?: number;
|
|
15
|
+
}
|
|
16
|
+
export interface PgliteServer {
|
|
17
|
+
/** Строка подключения postgres.js к сокету PGlite (пользователь и база postgres). */
|
|
18
|
+
dsn: string;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** Запустить PGlite с pgcrypto и сокетом для postgres.js. */
|
|
22
|
+
export declare function startPglite(opts?: PgliteOptions): Promise<PgliteServer>;
|
package/dist/pglite.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* База для разработки без установленного PostgreSQL (решение 6 точки А): PGlite (PostgreSQL 18 в
|
|
3
|
+
* WebAssembly) с pgcrypto, доступный postgres.js через pglite-socket. Одно соединение: годится для
|
|
4
|
+
* разработки и быстрых тестов логики, но не для гонок и не вместо postgres:18 в CI; LISTEN из
|
|
5
|
+
* второго соединения нет — connect({ listen: false, max: 1 }).
|
|
6
|
+
*
|
|
7
|
+
* Пакеты @electric-sql/pglite и @electric-sql/pglite-socket — необязательные peer-зависимости:
|
|
8
|
+
* основная точка входа их не импортирует, up({ pglite }) загружает их по требованию.
|
|
9
|
+
*/
|
|
10
|
+
/** Запустить PGlite с pgcrypto и сокетом для postgres.js. */
|
|
11
|
+
export async function startPglite(opts = {}) {
|
|
12
|
+
const load = async (name) => {
|
|
13
|
+
try {
|
|
14
|
+
return await import(name);
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new Error(`letopis.up: для pglite нужен пакет ${name} — npm i -D @electric-sql/pglite @electric-sql/pglite-socket`);
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
const { PGlite } = await load('@electric-sql/pglite');
|
|
21
|
+
const { pgcrypto } = await load('@electric-sql/pglite/contrib/pgcrypto');
|
|
22
|
+
const { PGLiteSocketServer } = await load('@electric-sql/pglite-socket');
|
|
23
|
+
const db = await PGlite.create(opts.dataDir, { extensions: { pgcrypto } });
|
|
24
|
+
const port = opts.port ?? (await freePort());
|
|
25
|
+
const server = new PGLiteSocketServer({ db, port, host: '127.0.0.1' });
|
|
26
|
+
await server.start();
|
|
27
|
+
return {
|
|
28
|
+
dsn: `postgres://postgres:postgres@127.0.0.1:${port}/postgres`,
|
|
29
|
+
async close() {
|
|
30
|
+
await server.stop();
|
|
31
|
+
await db.close();
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
async function freePort() {
|
|
36
|
+
const net = await import('node:net');
|
|
37
|
+
return new Promise((resolve, reject) => {
|
|
38
|
+
const s = net.createServer();
|
|
39
|
+
s.once('error', reject);
|
|
40
|
+
s.listen(0, '127.0.0.1', () => {
|
|
41
|
+
const p = s.address().port;
|
|
42
|
+
s.close(() => resolve(p));
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { JsonSchema } from './jsonschema.js';
|
|
2
|
+
import { type Validator } from './validate.js';
|
|
3
|
+
export interface EndInfo {
|
|
4
|
+
class: string | string[];
|
|
5
|
+
many?: boolean;
|
|
6
|
+
required?: boolean;
|
|
7
|
+
min?: number;
|
|
8
|
+
max?: number;
|
|
9
|
+
onDelete?: 'restrict' | 'cascade' | 'unset';
|
|
10
|
+
meta?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Тип листа данных для фильтров, сортировки и агрегатов (замена fieldTypeOf 0.21). */
|
|
13
|
+
export type LeafType = 'text' | 'numeric' | 'boolean' | 'timestamptz' | 'date' | 'uuid' | 'jsonb';
|
|
14
|
+
export interface ClassInfo {
|
|
15
|
+
name: string;
|
|
16
|
+
tenant: string;
|
|
17
|
+
rev: number;
|
|
18
|
+
kind: 'hub' | 'link';
|
|
19
|
+
alias?: string;
|
|
20
|
+
abstract: boolean;
|
|
21
|
+
/** Сам класс, затем предки до корня. */
|
|
22
|
+
ancestors: string[];
|
|
23
|
+
schema: JsonSchema;
|
|
24
|
+
ends: Record<string, EndInfo>;
|
|
25
|
+
key: string[];
|
|
26
|
+
history: unknown;
|
|
27
|
+
ownerDefault: 'tenant' | 'actor';
|
|
28
|
+
cache?: {
|
|
29
|
+
ttl?: number;
|
|
30
|
+
};
|
|
31
|
+
meta: Record<string, unknown>;
|
|
32
|
+
/** Валидатор на TypeScript (компилируется при первом обращении). */
|
|
33
|
+
validator(): Validator;
|
|
34
|
+
}
|
|
35
|
+
interface ClassRow {
|
|
36
|
+
tenant: string;
|
|
37
|
+
rev: number;
|
|
38
|
+
data: {
|
|
39
|
+
alias?: string;
|
|
40
|
+
resolved: Record<string, unknown>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export declare class Registry {
|
|
44
|
+
private byName;
|
|
45
|
+
private byAlias;
|
|
46
|
+
/** Заменить содержимое строками Class (собственные классы арендатора и системные). */
|
|
47
|
+
load(rows: readonly ClassRow[]): void;
|
|
48
|
+
has(name: string): boolean;
|
|
49
|
+
find(name: string): ClassInfo | undefined;
|
|
50
|
+
resolve(name: string): ClassInfo;
|
|
51
|
+
all(): ClassInfo[];
|
|
52
|
+
/** Класс и все его потомки (семейство). */
|
|
53
|
+
family(name: string): ClassInfo[];
|
|
54
|
+
/** Тип листа по пути полей класса: по JSON Schema (type и format). */
|
|
55
|
+
leafType(cls: string, path: readonly string[]): LeafType;
|
|
56
|
+
}
|
|
57
|
+
export {};
|