letopis 1.4.1 → 2.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.md +329 -278
- package/CHANGELOG.md +558 -1
- package/README.md +606 -1037
- package/dist/acl.d.ts +78 -1
- package/dist/acl.js +264 -13
- package/dist/admin.d.ts +10 -141
- package/dist/admin.js +24 -175
- package/dist/auth.d.ts +2 -2
- package/dist/auth.js +5 -15
- package/dist/cache.d.ts +8 -1
- package/dist/cache.js +33 -10
- package/dist/chain.d.ts +19 -41
- package/dist/chain.js +169 -201
- package/dist/cli.js +51 -88
- package/dist/demo/booking.d.ts +352 -36
- package/dist/describe.d.ts +47 -0
- package/dist/describe.js +195 -0
- package/dist/errors.d.ts +59 -13
- package/dist/errors.js +190 -29
- package/dist/index.d.ts +52 -71
- package/dist/index.js +245 -130
- package/dist/internal.d.ts +12 -0
- package/dist/internal.js +5 -0
- package/dist/jsonschema.js +18 -17
- package/dist/load.d.ts +20 -12
- package/dist/load.js +225 -186
- package/dist/mcp.d.ts +5 -7
- package/dist/mcp.js +171 -215
- package/dist/messages/access.d.ts +90 -0
- package/dist/messages/access.js +113 -0
- package/dist/messages/api.d.ts +171 -0
- package/dist/messages/api.js +215 -0
- package/dist/messages/boot.d.ts +201 -0
- package/dist/messages/boot.js +256 -0
- package/dist/messages/codes.d.ts +172 -0
- package/dist/messages/codes.js +173 -0
- package/dist/messages/index.d.ts +95 -0
- package/dist/messages/index.js +118 -0
- package/dist/messages/mcp.d.ts +306 -0
- package/dist/messages/mcp.js +387 -0
- package/dist/messages/model.d.ts +233 -0
- package/dist/messages/model.js +283 -0
- package/dist/messages/query.d.ts +131 -0
- package/dist/messages/query.js +158 -0
- package/dist/messages/sql-access.d.ts +70 -0
- package/dist/messages/sql-access.js +87 -0
- package/dist/messages/sql-core.d.ts +150 -0
- package/dist/messages/sql-core.js +181 -0
- package/dist/messages/sql-time.d.ts +71 -0
- package/dist/messages/sql-time.js +88 -0
- package/dist/messages/sql-write.d.ts +246 -0
- package/dist/messages/sql-write.js +270 -0
- package/dist/messages/time.d.ts +23 -0
- package/dist/messages/time.js +29 -0
- package/dist/messages/types.d.ts +27 -0
- package/dist/messages/types.js +5 -0
- package/dist/messages/write.d.ts +405 -0
- package/dist/messages/write.js +509 -0
- package/dist/model.d.ts +73 -60
- package/dist/model.js +26 -29
- package/dist/ops.d.ts +35 -0
- package/dist/ops.js +101 -0
- package/dist/pglite.d.ts +0 -9
- package/dist/pglite.js +3 -1
- package/dist/registry.d.ts +3 -19
- package/dist/registry.js +3 -16
- package/dist/signals.d.ts +5 -9
- package/dist/signals.js +5 -9
- package/dist/sql.d.ts +13 -6
- package/dist/sql.js +176 -83
- package/dist/sync.d.ts +9 -3
- package/dist/sync.js +51 -21
- package/dist/tx.d.ts +26 -1
- package/dist/tx.js +44 -14
- package/dist/typed.d.ts +240 -45
- package/dist/types.d.ts +23 -91
- package/dist/types.js +12 -14
- package/dist/up.d.ts +53 -32
- package/dist/up.js +184 -73
- package/dist/uuid.js +5 -5
- package/dist/validate.d.ts +13 -1
- package/dist/validate.js +61 -29
- package/dist/watch.d.ts +11 -10
- package/dist/watch.js +33 -70
- package/dist/write.d.ts +9 -16
- package/dist/write.js +118 -187
- package/llms.txt +22 -25
- package/package.json +8 -11
- package/sql/10-core.sql +7 -50
- package/sql/15-errors.sql +64 -10
- package/sql/20-context.sql +7 -6
- package/sql/30-validate.sql +35 -27
- package/sql/40-class.sql +20 -114
- package/sql/50-acl.sql +8 -13
- package/sql/60-write.sql +220 -667
- package/sql/70-read.sql +20 -16
- package/sql/80-auth.sql +24 -27
- package/sql/90-time.sql +70 -859
- package/sql/95-seed.system.sql +7 -14
- package/sql/99-revision.sql +2 -1
- package/sql/README.md +32 -24
- package/sql/migrate/README.md +14 -0
- package/AGENT-CHEATSHEET.en.md +0 -485
- package/MIGRATION.md +0 -190
- package/README.en.md +0 -2859
- package/dist/import.d.ts +0 -185
- package/dist/import.js +0 -820
- package/dist/project.d.ts +0 -350
- package/dist/project.js +0 -1224
- package/sql/75-history.sql +0 -358
- package/sql/88-derived.sql +0 -1235
- package/sql/migrate/0006-changed-paths.sql +0 -3
- package/sql/migrate/0007-secret-account-fk.sql +0 -12
package/dist/model.d.ts
CHANGED
|
@@ -17,18 +17,20 @@ export type OnDelete = 'restrict' | 'cascade' | 'unset';
|
|
|
17
17
|
export interface ClassMeta {
|
|
18
18
|
[key: string]: unknown;
|
|
19
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
20
|
type Target = AnyModel | string;
|
|
30
|
-
/**
|
|
31
|
-
|
|
21
|
+
/** Имя класса цели: у модели — её имя, у строки — сама строка. */
|
|
22
|
+
type TargetName<T> = T extends string ? T : T extends {
|
|
23
|
+
readonly name: infer N extends string;
|
|
24
|
+
} ? N : string;
|
|
25
|
+
/**
|
|
26
|
+
* Обязателен ли множественный конец по `min` (для типов): `min` больше нуля — да, нет `min` или ноль — нет, число,
|
|
27
|
+
* известное только во время выполнения, — неизвестно (`boolean`).
|
|
28
|
+
*/
|
|
29
|
+
type MinRequired<O> = O extends {
|
|
30
|
+
min: infer N;
|
|
31
|
+
} ? (N extends 0 | undefined ? false : number extends N ? boolean : true) : false;
|
|
32
|
+
/** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
|
|
33
|
+
export declare class End<Many extends boolean = boolean, Required extends boolean = boolean, T extends string = string> {
|
|
32
34
|
readonly targets: readonly Target[];
|
|
33
35
|
readonly many: Many;
|
|
34
36
|
readonly required: Required;
|
|
@@ -36,6 +38,8 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
|
|
|
36
38
|
readonly max?: number;
|
|
37
39
|
readonly onDeleteRule?: OnDelete;
|
|
38
40
|
readonly metaData?: Record<string, unknown>;
|
|
41
|
+
/** Только для вывода типов: имена классов целей — строгие переходы и слоты цепочек (план 2.0, этап 4, п. 1). */
|
|
42
|
+
readonly __targets?: T;
|
|
39
43
|
constructor(targets: readonly Target[], opts: {
|
|
40
44
|
many: Many;
|
|
41
45
|
required: Required;
|
|
@@ -46,22 +50,25 @@ export declare class End<Many extends boolean = boolean, Required extends boolea
|
|
|
46
50
|
});
|
|
47
51
|
private with;
|
|
48
52
|
/** Необязательный конец. */
|
|
49
|
-
optional(): End<Many, false>;
|
|
53
|
+
optional(): End<Many, false, T>;
|
|
50
54
|
/** Правило при удалении цели: restrict (по умолчанию), cascade, unset. */
|
|
51
|
-
onDelete(rule: OnDelete): End<Many, Required>;
|
|
55
|
+
onDelete(rule: OnDelete): End<Many, Required, T>;
|
|
52
56
|
/** Метаданные конца (§2.13). */
|
|
53
|
-
meta(meta: Record<string, unknown>): End<Many, Required>;
|
|
57
|
+
meta(meta: Record<string, unknown>): End<Many, Required, T>;
|
|
54
58
|
/** Имена классов целей. */
|
|
55
59
|
classNames(): string[];
|
|
56
60
|
}
|
|
57
61
|
/** Одиночный конец; несколько классов — союз: `one(Service, Product)`. По умолчанию обязательный. */
|
|
58
|
-
export declare function one(...targets:
|
|
59
|
-
/**
|
|
60
|
-
|
|
62
|
+
export declare function one<const T extends readonly Target[]>(...targets: T): End<false, true, TargetName<T[number]>>;
|
|
63
|
+
/**
|
|
64
|
+
* Множественный конец: список целей в одном конце (§4 предложения). Обязателен при `min` больше нуля — `unset()` у него
|
|
65
|
+
* в цепочке типизированного подключения нет.
|
|
66
|
+
*/
|
|
67
|
+
export declare function many<const T extends Target | readonly Target[], const O extends {
|
|
61
68
|
min?: number;
|
|
62
69
|
max?: number;
|
|
63
70
|
onDelete?: OnDelete;
|
|
64
|
-
}): End<true,
|
|
71
|
+
} = Record<never, never>>(target: T, opts?: O): End<true, MinRequired<O>, TargetName<T extends readonly (infer X)[] ? X : T>>;
|
|
65
72
|
type EndsInput = Record<string, End | AnyModel>;
|
|
66
73
|
type EndOf<E> = E extends End<infer M, infer R> ? End<M, R> : End<false, true>;
|
|
67
74
|
type LinkValue<E> = EndOf<E> extends End<true, boolean> ? string[] : string;
|
|
@@ -79,39 +86,14 @@ export interface ClassDef<S extends z.ZodRawShape = z.ZodRawShape, E extends End
|
|
|
79
86
|
fields?: z.ZodObject<S> | S;
|
|
80
87
|
key?: readonly string[];
|
|
81
88
|
onDelete?: Partial<Record<keyof E & string, OnDelete>>;
|
|
82
|
-
|
|
89
|
+
/** Вести ли журнал версий (по умолчанию да). */
|
|
90
|
+
history?: boolean;
|
|
83
91
|
ownerDefault?: 'tenant' | 'actor';
|
|
84
92
|
cache?: {
|
|
85
93
|
ttl?: number;
|
|
86
94
|
};
|
|
87
95
|
meta?: ClassMeta;
|
|
88
|
-
/** Производный класс (план 1.1, §2.4): строки пишет только проектор с этим именем; наследуется. */
|
|
89
|
-
derivedBy?: string;
|
|
90
|
-
/**
|
|
91
|
-
* Агрегаты в базе (план 1.1, §2.4): поле — сумма поля (`sum`) или число (`count: true`) живых строк класса `of` того
|
|
92
|
-
* же арендатора, чей одиночный конец `via` ведёт на строку; `where` — JSON Schema над данными строки-источника.
|
|
93
|
-
* Поле считает база, приложение его не пишет. Наследуется, потомок заменяет целиком.
|
|
94
|
-
*/
|
|
95
|
-
aggregate?: Record<string, AggregateDef>;
|
|
96
96
|
}
|
|
97
|
-
/** Описание одного агрегатного поля: `sum` или `count: true`; `of` — класс-источник (имя или модель). */
|
|
98
|
-
export type AggregateDef = {
|
|
99
|
-
sum: string;
|
|
100
|
-
count?: never;
|
|
101
|
-
of: string | {
|
|
102
|
-
readonly name: string;
|
|
103
|
-
};
|
|
104
|
-
via: string;
|
|
105
|
-
where?: JsonSchema;
|
|
106
|
-
} | {
|
|
107
|
-
count: true;
|
|
108
|
-
sum?: never;
|
|
109
|
-
of: string | {
|
|
110
|
-
readonly name: string;
|
|
111
|
-
};
|
|
112
|
-
via: string;
|
|
113
|
-
where?: JsonSchema;
|
|
114
|
-
};
|
|
115
97
|
/** Описание класса в строке `Class` (метасхема `class_metaschema()` в lib/sql/40-class.sql). */
|
|
116
98
|
export interface ClassDescription {
|
|
117
99
|
name: string;
|
|
@@ -133,20 +115,12 @@ export interface ClassDescription {
|
|
|
133
115
|
meta?: Record<string, unknown>;
|
|
134
116
|
}>;
|
|
135
117
|
key?: string[];
|
|
136
|
-
history?: boolean
|
|
118
|
+
history?: boolean;
|
|
137
119
|
ownerDefault?: 'tenant' | 'actor';
|
|
138
120
|
cache?: {
|
|
139
121
|
ttl?: number;
|
|
140
122
|
};
|
|
141
123
|
meta?: ClassMeta;
|
|
142
|
-
derivedBy?: string;
|
|
143
|
-
aggregate?: Record<string, {
|
|
144
|
-
sum?: string;
|
|
145
|
-
count?: true;
|
|
146
|
-
of: string;
|
|
147
|
-
via: string;
|
|
148
|
-
where?: JsonSchema;
|
|
149
|
-
}>;
|
|
150
124
|
}
|
|
151
125
|
/** Правило refine, которое не попадёт в базу: проверяется только в библиотеке. */
|
|
152
126
|
export interface LostRule {
|
|
@@ -155,7 +129,36 @@ export interface LostRule {
|
|
|
155
129
|
}
|
|
156
130
|
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>>;
|
|
157
131
|
type AllLinks<P, E extends EndsInput> = P extends Model<string, unknown, infer PL> ? Omit<PL, keyof E> & LinksOf<E> : LinksOf<E>;
|
|
158
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Концы класса для строгих типов цепочек (план 2.0, этап 4, п. 1): роль → классы целей, множественный ли, обязательный
|
|
134
|
+
* ли; с концами предков (потомок заменяет конец целиком). Тот же вид выдаёт `letopis types` (generateTypes, sync.ts).
|
|
135
|
+
*/
|
|
136
|
+
type EndSpecOf<V> = V extends End<infer Mn, infer R, infer T> ? {
|
|
137
|
+
targets: T;
|
|
138
|
+
many: Mn;
|
|
139
|
+
required: R;
|
|
140
|
+
} : V extends {
|
|
141
|
+
readonly name: infer N extends string;
|
|
142
|
+
} ? {
|
|
143
|
+
targets: N;
|
|
144
|
+
many: false;
|
|
145
|
+
required: true;
|
|
146
|
+
} : never;
|
|
147
|
+
type EndsSpecOf<E> = {
|
|
148
|
+
[K in keyof E]: EndSpecOf<E[K]>;
|
|
149
|
+
};
|
|
150
|
+
type AllEnds<P, E extends EndsInput> = P extends Model<string, unknown, unknown, string, infer PE, string> ? Omit<PE, keyof E> & EndsSpecOf<E> : EndsSpecOf<E>;
|
|
151
|
+
/** Имя класса и его предков — как `ancestors` реестра (семейства и переходы строгих типов). */
|
|
152
|
+
type Lineage<N extends string, P> = N | (P extends Model<infer PN, unknown, unknown, string, unknown, infer PP> ? PN | PP : never);
|
|
153
|
+
/** Концы описания класса: `ends` или пусто. */
|
|
154
|
+
type EndsIn<D> = D extends {
|
|
155
|
+
ends?: infer E extends EndsInput;
|
|
156
|
+
} ? E : Record<never, never>;
|
|
157
|
+
/**
|
|
158
|
+
* Модель класса. `N` — имя, `D` — данные строки, `L` — концы строки, `A` — алиас, `E` — концы класса (роль → классы
|
|
159
|
+
* целей, множественный ли, обязательный ли), `P` — имя класса и его предков: всё это — для строгих типов цепочек.
|
|
160
|
+
*/
|
|
161
|
+
export interface Model<N extends string = string, D = unknown, L = unknown, A extends string = string, E = unknown, P extends string = N> {
|
|
159
162
|
readonly name: N;
|
|
160
163
|
readonly kind: 'hub' | 'link';
|
|
161
164
|
readonly alias?: A;
|
|
@@ -180,9 +183,11 @@ export interface Model<N extends string = string, D = unknown, L = unknown, A ex
|
|
|
180
183
|
readonly __types?: {
|
|
181
184
|
data: D;
|
|
182
185
|
links: L;
|
|
186
|
+
ends: E;
|
|
187
|
+
ancestors: P;
|
|
183
188
|
};
|
|
184
189
|
}
|
|
185
|
-
export type AnyModel = Model<string, any, any, any>;
|
|
190
|
+
export type AnyModel = Model<string, any, any, any, any, any>;
|
|
186
191
|
/** Тип данных строки класса: `Infer<typeof Master>`. */
|
|
187
192
|
export type Infer<M> = M extends Model<string, infer D, unknown> ? D : never;
|
|
188
193
|
/** Тип концов строки класса. */
|
|
@@ -191,13 +196,21 @@ type ShapeOf<F> = F extends z.ZodObject<infer S> ? S : F extends z.ZodRawShape ?
|
|
|
191
196
|
type AliasOf<D> = D extends {
|
|
192
197
|
alias: infer A extends string;
|
|
193
198
|
} ? A : string;
|
|
199
|
+
/** Предок из описания класса (`extends`); нет — unknown. */
|
|
200
|
+
type ExtendsOf<D> = D extends {
|
|
201
|
+
extends?: infer P;
|
|
202
|
+
} ? P : unknown;
|
|
203
|
+
/** Описание класса без полей — третьим аргументом короткой формы. */
|
|
204
|
+
type ClassOpts = Omit<ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>, 'fields'>;
|
|
205
|
+
/** Тип модели по имени, описанию и собственным полям: данные и концы — с предками. */
|
|
206
|
+
type ModelOf<N extends string, D, S extends z.ZodRawShape> = Model<N, DataOf<ExtendsOf<D>, S>, AllLinks<ExtendsOf<D>, EndsIn<D>>, AliasOf<D>, AllEnds<ExtendsOf<D>, EndsIn<D>>, Lineage<N, ExtendsOf<D>>>;
|
|
194
207
|
/**
|
|
195
208
|
* Описывает класс объектов (хаб): поля, ключ, концы и свойства класса. Класс-связь объектов со своими данными
|
|
196
209
|
* описывает `link()`.
|
|
197
210
|
*/
|
|
198
|
-
export declare function hub<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts:
|
|
199
|
-
export declare function hub<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D):
|
|
211
|
+
export declare function hub<N extends string, S extends z.ZodRawShape, const O extends ClassOpts>(name: N, shape: S, opts: O): ModelOf<N, O, S>;
|
|
212
|
+
export declare function hub<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): ModelOf<N, D, ShapeOf<D['fields']>>;
|
|
200
213
|
/** Связь — отношение между объектами со своими данными (бронь, навык). */
|
|
201
|
-
export declare function link<N extends string, S extends z.ZodRawShape>(name: N, shape: S, opts:
|
|
202
|
-
export declare function link<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D):
|
|
214
|
+
export declare function link<N extends string, S extends z.ZodRawShape, const O extends ClassOpts>(name: N, shape: S, opts: O): ModelOf<N, O, S>;
|
|
215
|
+
export declare function link<N extends string, const D extends ClassDef<z.ZodRawShape, EndsInput, AnyModel | undefined>>(name: N, def?: D): ModelOf<N, D, ShapeOf<D['fields']>>;
|
|
203
216
|
export { normalizeSchema, translatePattern, type JsonSchema } from './jsonschema.js';
|
package/dist/model.js
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
* Поля, концы и ключ наследуются по правилу 0.21 «потомок заменяет поле целиком» (`extends`).
|
|
12
12
|
*/
|
|
13
13
|
import { z } from 'zod';
|
|
14
|
-
import {
|
|
14
|
+
import { lerr } from './errors.js';
|
|
15
15
|
import { normalizeSchema } from './jsonschema.js';
|
|
16
|
-
|
|
16
|
+
import { RESERVED_CLASS_NAMES } from './types.js';
|
|
17
|
+
/** Конец класса: `one(X)`, `one(A, B)`, `many(X, { min, max })`. `T` — имена классов целей (только для типов). */
|
|
17
18
|
export class End {
|
|
18
19
|
targets;
|
|
19
20
|
many;
|
|
@@ -24,7 +25,7 @@ export class End {
|
|
|
24
25
|
metaData;
|
|
25
26
|
constructor(targets, opts) {
|
|
26
27
|
if (!targets.length)
|
|
27
|
-
throw
|
|
28
|
+
throw lerr('model.end_no_targets');
|
|
28
29
|
this.targets = targets;
|
|
29
30
|
this.many = opts.many;
|
|
30
31
|
this.required = opts.required;
|
|
@@ -58,12 +59,17 @@ export class End {
|
|
|
58
59
|
export function one(...targets) {
|
|
59
60
|
return new End(targets, { many: false, required: true });
|
|
60
61
|
}
|
|
61
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Множественный конец: список целей в одном конце (§4 предложения). Обязателен при `min` больше нуля — `unset()` у него
|
|
64
|
+
* в цепочке типизированного подключения нет.
|
|
65
|
+
*/
|
|
62
66
|
export function many(target, opts = {}) {
|
|
63
67
|
const targets = Array.isArray(target) ? target : [target];
|
|
64
68
|
return new End(targets, { many: true, required: (opts.min ?? 0) > 0, min: opts.min, max: opts.max, onDelete: opts.onDelete });
|
|
65
69
|
}
|
|
66
70
|
const NAME = /^[A-Za-zА-Яа-яЁё_][A-Za-zА-Яа-яЁё0-9_]{0,62}$/;
|
|
71
|
+
/** Ключи полной формы описания класса (ClassDef). */
|
|
72
|
+
const DEF_KEYS = new Set(['alias', 'extends', 'abstract', 'ends', 'fields', 'key', 'onDelete', 'history', 'ownerDefault', 'cache', 'meta']);
|
|
67
73
|
function isZod(v) {
|
|
68
74
|
return typeof v === 'object' && v !== null && '_zod' in v;
|
|
69
75
|
}
|
|
@@ -91,10 +97,18 @@ function collectLost(schema, path, out) {
|
|
|
91
97
|
}
|
|
92
98
|
function makeModel(kind, name, defArg, optsArg) {
|
|
93
99
|
if (!NAME.test(name))
|
|
94
|
-
throw
|
|
100
|
+
throw lerr('model.class_name_invalid', { name });
|
|
101
|
+
// имя метода цепочки или корня db: шаг с таким именем до хаба не дойдёт (план 2.0, этап 4, п. 2; 7.10 №3); связь с
|
|
102
|
+
// таким именем отклоняет база
|
|
103
|
+
if (kind === 'hub' && RESERVED_CLASS_NAMES.includes(name))
|
|
104
|
+
throw lerr('model.hub_name_reserved', { name });
|
|
95
105
|
// короткая форма: hub(name, { поле: z…, … }, { key, meta, … })
|
|
96
106
|
let def;
|
|
97
|
-
|
|
107
|
+
// { fields: z.object(…) } без других ключей описания — полная форма, а не класс с единственным полем fields (план 2.0,
|
|
108
|
+
// 7.5 №2); короткая форма — все значения Zod и хотя бы один ключ вне описания класса
|
|
109
|
+
const fullForm = !!defArg && typeof defArg === 'object' && defArg.fields instanceof z.ZodObject
|
|
110
|
+
&& Object.keys(defArg).every((k) => DEF_KEYS.has(k));
|
|
111
|
+
const isShape = !fullForm && defArg && typeof defArg === 'object' && Object.values(defArg).length > 0 && Object.values(defArg).every(isZod);
|
|
98
112
|
if (optsArg !== undefined || isShape) {
|
|
99
113
|
def = { ...optsArg, fields: (defArg ?? {}) };
|
|
100
114
|
}
|
|
@@ -105,12 +119,12 @@ function makeModel(kind, name, defArg, optsArg) {
|
|
|
105
119
|
const ownShape = !fields ? {} : fields instanceof z.ZodObject ? fields.shape : fields;
|
|
106
120
|
for (const k of Object.keys(ownShape)) {
|
|
107
121
|
if (k.startsWith('$'))
|
|
108
|
-
throw
|
|
122
|
+
throw lerr('model.field_name_dollar', { cls: name, field: k });
|
|
109
123
|
}
|
|
110
124
|
const ownEnds = {};
|
|
111
125
|
for (const [role, v] of Object.entries(def.ends ?? {})) {
|
|
112
126
|
if (!NAME.test(role))
|
|
113
|
-
throw
|
|
127
|
+
throw lerr('model.end_name_invalid', { cls: name, role });
|
|
114
128
|
let end = v instanceof End ? v : one(v);
|
|
115
129
|
const rule = def.onDelete?.[role];
|
|
116
130
|
if (rule)
|
|
@@ -130,28 +144,21 @@ function makeModel(kind, name, defArg, optsArg) {
|
|
|
130
144
|
if (zd.type === 'default')
|
|
131
145
|
zd = zd.innerType._zod.def;
|
|
132
146
|
if (zd.type !== 'string')
|
|
133
|
-
throw
|
|
147
|
+
throw lerr('model.key_field_type', { cls: name, field: k });
|
|
134
148
|
}
|
|
135
149
|
else if (ends[k]) {
|
|
136
150
|
if (ends[k].many || !ends[k].required)
|
|
137
|
-
throw
|
|
151
|
+
throw lerr('model.key_end_type', { cls: name, end: k });
|
|
138
152
|
}
|
|
139
153
|
else {
|
|
140
|
-
throw
|
|
154
|
+
throw lerr('model.key_unknown_name', { cls: name, name: k });
|
|
141
155
|
}
|
|
142
156
|
}
|
|
143
157
|
for (const [role, e] of Object.entries(ends)) {
|
|
144
158
|
if (e.onDeleteRule === 'unset' && (e.required || (e.min ?? 0) > 0)) {
|
|
145
|
-
throw
|
|
159
|
+
throw lerr('model.unset_required_end', { cls: name, end: role });
|
|
146
160
|
}
|
|
147
161
|
}
|
|
148
|
-
// агрегаты (план 1.1, §2.4): остальное — роль, поле суммы, цикл — сверяет база по описаниям источников
|
|
149
|
-
for (const [field, a] of Object.entries(def.aggregate ?? {})) {
|
|
150
|
-
if (!(field in shape))
|
|
151
|
-
throw new LetopisError('invalid_class', `${name}: агрегатного поля ${field} нет среди полей класса`);
|
|
152
|
-
if ((a.sum === undefined) === (a.count === undefined))
|
|
153
|
-
throw new LetopisError('invalid_class', `${name}: агрегат ${field} — ровно одно из sum и count: true`);
|
|
154
|
-
}
|
|
155
162
|
// строгий верхний уровень — как в базе (resolved.schema: additionalProperties false)
|
|
156
163
|
const schema = z.strictObject(shape);
|
|
157
164
|
const model = {
|
|
@@ -211,16 +218,6 @@ function makeModel(kind, name, defArg, optsArg) {
|
|
|
211
218
|
desc.cache = def.cache;
|
|
212
219
|
if (def.meta)
|
|
213
220
|
desc.meta = def.meta;
|
|
214
|
-
if (def.derivedBy)
|
|
215
|
-
desc.derivedBy = def.derivedBy;
|
|
216
|
-
if (def.aggregate) {
|
|
217
|
-
desc.aggregate = Object.fromEntries(Object.entries(def.aggregate).map(([field, a]) => [field, {
|
|
218
|
-
...(a.sum !== undefined ? { sum: a.sum } : { count: true }),
|
|
219
|
-
of: typeof a.of === 'string' ? a.of : a.of.name,
|
|
220
|
-
via: a.via,
|
|
221
|
-
...(a.where ? { where: a.where } : {}),
|
|
222
|
-
}]));
|
|
223
|
-
}
|
|
224
221
|
return desc;
|
|
225
222
|
},
|
|
226
223
|
lostRules() {
|
package/dist/ops.d.ts
CHANGED
|
@@ -81,3 +81,38 @@ export declare const not: (v: Scalar | Op) => Op;
|
|
|
81
81
|
* вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и».
|
|
82
82
|
*/
|
|
83
83
|
export declare const or: (...filters: Record<string, unknown>[]) => Op;
|
|
84
|
+
/**
|
|
85
|
+
* Значение ключа в JSON-фильтре MCP: `value` — скаляр, `list` — массив скаляров, `range` — массив из двух скаляров,
|
|
86
|
+
* `text` — строка, `flag` — true или false, `true` — только true, `negation` — оператор или скаляр, `filters` — массив
|
|
87
|
+
* фильтров вместо всего фильтра шага (`$or`). По нему mcp.ts проверяет значение, а таблицы показывают форму.
|
|
88
|
+
*/
|
|
89
|
+
export type OpArg = 'value' | 'list' | 'range' | 'text' | 'flag' | 'true' | 'negation' | 'filters';
|
|
90
|
+
/** Оператор фильтра в реестре. */
|
|
91
|
+
export interface OperatorSpec {
|
|
92
|
+
/** Имя фабрики в TypeScript: `import { gt } from 'letopis'`. */
|
|
93
|
+
readonly ts: string;
|
|
94
|
+
/** Сама фабрика. */
|
|
95
|
+
readonly make: (...args: never[]) => Op;
|
|
96
|
+
/** Ключ в JSON-фильтре MCP (`letopis_query`): `{ "price": { "$gt": 1000 } }`. */
|
|
97
|
+
readonly mcp: string;
|
|
98
|
+
/** Значение ключа MCP. */
|
|
99
|
+
readonly arg: OpArg;
|
|
100
|
+
/** Смысл для таблиц: README, шпаргалка, глава «Чтение» и справочник сайта. */
|
|
101
|
+
readonly ru: string;
|
|
102
|
+
}
|
|
103
|
+
/** Все 18 операторов в порядке таблиц. */
|
|
104
|
+
export declare const OPERATORS: readonly OperatorSpec[];
|
|
105
|
+
/** Вызов фабрики в таблицах: `between(a, b)`. */
|
|
106
|
+
export declare function opCall(o: OperatorSpec): string;
|
|
107
|
+
/** Ключ MCP со значением в таблицах: `{ "$between": [a, b] }`. */
|
|
108
|
+
export declare function opJson(o: OperatorSpec): string;
|
|
109
|
+
/** Ключ MCP с формой значения для описания letopis_query: `$between [a, b]`; у value, text и negation — только ключ. */
|
|
110
|
+
export declare function opHint(o: OperatorSpec): string;
|
|
111
|
+
/**
|
|
112
|
+
* Таблица операторов в Markdown — README, шпаргалка и справочник сайта: TypeScript, MCP, смысл; `mcp: false` — без
|
|
113
|
+
* колонки MCP (глава «Чтение» сайта: там цепочки на TypeScript). Вертикальная черта в ячейке экранируется, иначе она
|
|
114
|
+
* разрезала бы строку таблицы.
|
|
115
|
+
*/
|
|
116
|
+
export declare function operatorTable({ mcp }?: {
|
|
117
|
+
mcp?: boolean;
|
|
118
|
+
}): string;
|
package/dist/ops.js
CHANGED
|
@@ -82,3 +82,104 @@ export const not = (v) => op('not', v);
|
|
|
82
82
|
* вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и».
|
|
83
83
|
*/
|
|
84
84
|
export const or = (...filters) => op('or', filters);
|
|
85
|
+
/** Все 18 операторов в порядке таблиц. */
|
|
86
|
+
export const OPERATORS = [
|
|
87
|
+
{
|
|
88
|
+
ts: 'ne', make: ne, mcp: '$ne', arg: 'value',
|
|
89
|
+
ru: 'Не равно. Подходят и строки без поля или с `null`; `ne(null)` — строки, где у поля есть значение',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
ts: 'gt', make: gt, mcp: '$gt', arg: 'value',
|
|
93
|
+
ru: 'Больше. Сравнение — по типу поля из описания класса (числа — как числа, даты — как даты), поле без описания — как текст; строки без поля не подходят',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
ts: 'gte', make: gte, mcp: '$gte', arg: 'value',
|
|
97
|
+
ru: 'Больше или равно; сравнение — как у `gt`',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
ts: 'lt', make: lt, mcp: '$lt', arg: 'value',
|
|
101
|
+
ru: 'Меньше; сравнение — как у `gt`',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
ts: 'lte', make: lte, mcp: '$lte', arg: 'value',
|
|
105
|
+
ru: 'Меньше или равно; сравнение — как у `gt`',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
ts: 'between', make: between, mcp: '$between', arg: 'range',
|
|
109
|
+
ru: 'От `a` до `b` включительно; сравнение — как у `gt`. Если `a` больше `b`, не подходит ничего',
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
ts: 'inList', make: inList, mcp: '$in', arg: 'list',
|
|
113
|
+
ru: 'Одно из значений списка. Пустой список не находит ничего; `null` в списке не находит строк без значения — для них `isNull`',
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
ts: 'like', make: like, mcp: '$like', arg: 'text',
|
|
117
|
+
ru: 'Текст по шаблону с учётом регистра: `%` — любые символы, в том числе ни одного, `_` — ровно один; буквальные `%` и `_` — с обратной косой чертой',
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
ts: 'ilike', make: ilike, mcp: '$ilike', arg: 'text',
|
|
121
|
+
ru: 'Как `like`, но без учёта регистра',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
ts: 'starts', make: starts, mcp: '$starts', arg: 'text',
|
|
125
|
+
ru: 'Текст начинается с `s`, с учётом регистра. Это `like(s + \'%\')`: `%` и `_` внутри `s` не экранируются и работают как в шаблоне',
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
ts: 'ends', make: ends, mcp: '$ends', arg: 'text',
|
|
129
|
+
ru: 'Текст заканчивается на `s`, с учётом регистра. Это `like(\'%\' + s)`: `%` и `_` внутри `s` не экранируются и работают как в шаблоне',
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
ts: 'has', make: has, mcp: '$has', arg: 'value',
|
|
133
|
+
ru: 'Поле-массив содержит элемент `v`. Тип важен: `has(5)` найдёт число 5, но не строку `\'5\'`',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
ts: 'hasAny', make: hasAny, mcp: '$hasAny', arg: 'list',
|
|
137
|
+
ru: 'Поле-массив содержит хотя бы один элемент списка. Находит только строковые элементы; пустой список не находит ничего',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
ts: 'hasAll', make: hasAll, mcp: '$hasAll', arg: 'list',
|
|
141
|
+
ru: 'Поле-массив содержит все элементы списка, в любом порядке. Тип важен, как у `has`; пустой список подходит любой строке, даже без поля',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
ts: 'exists', make: exists, mcp: '$exists', arg: 'flag',
|
|
145
|
+
ru: 'Поле есть в данных (`true`, по умолчанию) или его нет (`false`); значение не важно — поле с `null` тоже есть',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
ts: 'isNull', make: isNull, mcp: '$isNull', arg: 'true',
|
|
149
|
+
ru: 'В поле `null` или поля нет совсем; отличить одно от другого помогает `exists`',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
ts: 'not', make: not, mcp: '$not', arg: 'negation',
|
|
153
|
+
ru: 'Отрицание: `x` — оператор (`not(gte(1000))` — меньше 1000 или поля нет) или простое значение — тогда это «не равно», как `ne`',
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
ts: 'or', make: or, mcp: '$or', arg: 'filters',
|
|
157
|
+
ru: 'Хотя бы один из фильтров. Ставится вместо всего фильтра шага, а не значением поля; условия внутри одного фильтра соединяются через «и»',
|
|
158
|
+
},
|
|
159
|
+
];
|
|
160
|
+
/** Вызов фабрики в таблицах: `between(a, b)`. */
|
|
161
|
+
export function opCall(o) {
|
|
162
|
+
const args = { value: 'v', list: '[v, …]', range: 'a, b', text: 's', flag: 'b = true', true: '', negation: 'x', filters: 'f1, f2, …' };
|
|
163
|
+
return `${o.ts}(${args[o.arg]})`;
|
|
164
|
+
}
|
|
165
|
+
/** Ключ MCP со значением в таблицах: `{ "$between": [a, b] }`. */
|
|
166
|
+
export function opJson(o) {
|
|
167
|
+
const vals = { value: 'v', list: '[v, …]', range: '[a, b]', text: '"s"', flag: 'b', true: 'true', negation: 'x', filters: '[f1, f2, …]' };
|
|
168
|
+
return `{ "${o.mcp}": ${vals[o.arg]} }`;
|
|
169
|
+
}
|
|
170
|
+
/** Ключ MCP с формой значения для описания letopis_query: `$between [a, b]`; у value, text и negation — только ключ. */
|
|
171
|
+
export function opHint(o) {
|
|
172
|
+
const vals = { list: '[..]', range: '[a, b]', flag: 'true|false', true: 'true', filters: '[filter, …]' };
|
|
173
|
+
return vals[o.arg] ? `${o.mcp} ${vals[o.arg]}` : o.mcp;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Таблица операторов в Markdown — README, шпаргалка и справочник сайта: TypeScript, MCP, смысл; `mcp: false` — без
|
|
177
|
+
* колонки MCP (глава «Чтение» сайта: там цепочки на TypeScript). Вертикальная черта в ячейке экранируется, иначе она
|
|
178
|
+
* разрезала бы строку таблицы.
|
|
179
|
+
*/
|
|
180
|
+
export function operatorTable({ mcp = true } = {}) {
|
|
181
|
+
const cell = (s) => s.replaceAll('|', '\\|');
|
|
182
|
+
const head = mcp ? '| TypeScript | MCP | Смысл |' : '| TypeScript | Смысл |';
|
|
183
|
+
const rows = OPERATORS.map((o) => `| ${[`\`${opCall(o)}\``, ...(mcp ? [`\`${opJson(o)}\``] : []), o.ru].map(cell).join(' | ')} |`);
|
|
184
|
+
return [head, mcp ? '|---|---|---|' : '|---|---|', ...rows].join('\n');
|
|
185
|
+
}
|
package/dist/pglite.d.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
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
1
|
export interface PgliteOptions {
|
|
11
2
|
/** Каталог данных (по умолчанию — в памяти, данные теряются при остановке). */
|
|
12
3
|
dataDir?: string;
|
package/dist/pglite.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* Пакеты @electric-sql/pglite и @electric-sql/pglite-socket — необязательные peer-зависимости:
|
|
8
8
|
* основная точка входа их не импортирует, up({ pglite }) загружает их по требованию.
|
|
9
9
|
*/
|
|
10
|
+
import { lmsg } from './errors.js';
|
|
10
11
|
/** Запустить PGlite с pgcrypto и сокетом для postgres.js. */
|
|
11
12
|
export async function startPglite(opts = {}) {
|
|
12
13
|
const load = async (name) => {
|
|
@@ -14,7 +15,8 @@ export async function startPglite(opts = {}) {
|
|
|
14
15
|
return await import(name);
|
|
15
16
|
}
|
|
16
17
|
catch {
|
|
17
|
-
|
|
18
|
+
// обычный Error, как у up(): подходящего кода нет
|
|
19
|
+
throw new Error(`letopis.up: ${lmsg('boot.pglite_package', { name })}`);
|
|
18
20
|
}
|
|
19
21
|
};
|
|
20
22
|
const { PGlite } = await load('@electric-sql/pglite');
|
package/dist/registry.d.ts
CHANGED
|
@@ -9,18 +9,7 @@ export interface EndInfo {
|
|
|
9
9
|
onDelete?: 'restrict' | 'cascade' | 'unset';
|
|
10
10
|
meta?: Record<string, unknown>;
|
|
11
11
|
}
|
|
12
|
-
/**
|
|
13
|
-
* Агрегат в базе (план 1.1, §2.4): поле — сумма поля sum или число (count) живых строк класса of с потомками того же
|
|
14
|
-
* арендатора, чей одиночный конец via ведёт на строку; where — JSON Schema над данными строки-источника.
|
|
15
|
-
*/
|
|
16
|
-
export interface AggregateInfo {
|
|
17
|
-
sum?: string;
|
|
18
|
-
count?: true;
|
|
19
|
-
of: string;
|
|
20
|
-
via: string;
|
|
21
|
-
where?: JsonSchema;
|
|
22
|
-
}
|
|
23
|
-
/** Тип листа данных для фильтров, сортировки и агрегатов (замена fieldTypeOf 0.21). */
|
|
12
|
+
/** Тип листа данных для фильтров, сортировки и агрегатов. */
|
|
24
13
|
export type LeafType = 'text' | 'numeric' | 'boolean' | 'timestamptz' | 'date' | 'uuid' | 'jsonb';
|
|
25
14
|
export interface ClassInfo {
|
|
26
15
|
name: string;
|
|
@@ -34,16 +23,13 @@ export interface ClassInfo {
|
|
|
34
23
|
schema: JsonSchema;
|
|
35
24
|
ends: Record<string, EndInfo>;
|
|
36
25
|
key: string[];
|
|
37
|
-
history:
|
|
26
|
+
/** Ведёт ли класс журнал версий (`history: false` — нет). */
|
|
27
|
+
history: boolean;
|
|
38
28
|
ownerDefault: 'tenant' | 'actor';
|
|
39
29
|
cache?: {
|
|
40
30
|
ttl?: number;
|
|
41
31
|
};
|
|
42
32
|
meta: Record<string, unknown>;
|
|
43
|
-
/** Производный класс: строки пишет только проектор с этим именем (план 1.1, §2.4). */
|
|
44
|
-
derivedBy?: string;
|
|
45
|
-
/** Агрегатные поля: их считает база, приложение не пишет (план 1.1, §2.4). */
|
|
46
|
-
aggregate?: Record<string, AggregateInfo>;
|
|
47
33
|
/** Валидатор на TypeScript (компилируется при первом обращении). */
|
|
48
34
|
validator(): Validator;
|
|
49
35
|
}
|
|
@@ -55,8 +41,6 @@ interface ClassRow {
|
|
|
55
41
|
resolved: Record<string, unknown>;
|
|
56
42
|
};
|
|
57
43
|
}
|
|
58
|
-
/** Агрегатные поля класса: их считает база (план 1.1, §2.4). */
|
|
59
|
-
export declare const aggregateFields: (cls: Pick<ClassInfo, "aggregate">) => string[];
|
|
60
44
|
export declare class Registry {
|
|
61
45
|
private byName;
|
|
62
46
|
private byAlias;
|
package/dist/registry.js
CHANGED
|
@@ -3,19 +3,8 @@
|
|
|
3
3
|
* (собственные классы арендатора и системные), поиск по имени и алиасу, предки и потомки,
|
|
4
4
|
* типы листьев для фильтров и валидатор на TypeScript, скомпилированный из того же описания.
|
|
5
5
|
*/
|
|
6
|
-
import {
|
|
6
|
+
import { lerr } from './errors.js';
|
|
7
7
|
import { compileValidator } from './validate.js';
|
|
8
|
-
/** Агрегатные поля класса: их считает база (план 1.1, §2.4). */
|
|
9
|
-
export const aggregateFields = (cls) => Object.keys(cls.aggregate ?? {});
|
|
10
|
-
/** Описание агрегатов из resolved — без скомпилированного условия (match): оно для базы. */
|
|
11
|
-
function aggregateOf(agg) {
|
|
12
|
-
return Object.fromEntries(Object.entries(agg).map(([field, d]) => [field, {
|
|
13
|
-
...(typeof d.sum === 'string' ? { sum: d.sum } : { count: true }),
|
|
14
|
-
of: d.of,
|
|
15
|
-
via: d.via,
|
|
16
|
-
...(d.where && typeof d.where === 'object' ? { where: d.where } : {}),
|
|
17
|
-
}]));
|
|
18
|
-
}
|
|
19
8
|
export class Registry {
|
|
20
9
|
byName = new Map();
|
|
21
10
|
byAlias = new Map();
|
|
@@ -37,12 +26,10 @@ export class Registry {
|
|
|
37
26
|
schema: res.schema,
|
|
38
27
|
ends: (res.ends ?? {}),
|
|
39
28
|
key: (res.key ?? []),
|
|
40
|
-
history: res.history,
|
|
29
|
+
history: res.history !== false,
|
|
41
30
|
ownerDefault: (res.ownerDefault ?? 'tenant'),
|
|
42
31
|
cache: (res.cache ?? undefined),
|
|
43
32
|
meta: (res.meta ?? {}),
|
|
44
|
-
...(typeof res.derivedBy === 'string' ? { derivedBy: res.derivedBy } : {}),
|
|
45
|
-
...(res.aggregate && typeof res.aggregate === 'object' ? { aggregate: aggregateOf(res.aggregate) } : {}),
|
|
46
33
|
validator: () => (compiled ??= compileValidator(res.schema)),
|
|
47
34
|
};
|
|
48
35
|
this.byName.set(info.name, info);
|
|
@@ -59,7 +46,7 @@ export class Registry {
|
|
|
59
46
|
resolve(name) {
|
|
60
47
|
const c = this.find(name);
|
|
61
48
|
if (!c)
|
|
62
|
-
throw
|
|
49
|
+
throw lerr('model.class_not_found', { name });
|
|
63
50
|
return c;
|
|
64
51
|
}
|
|
65
52
|
all() {
|