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.
Files changed (89) hide show
  1. package/AGENT-CHEATSHEET.en.md +368 -0
  2. package/AGENT-CHEATSHEET.md +354 -0
  3. package/CHANGELOG.md +348 -0
  4. package/MIGRATION.md +190 -0
  5. package/README.en.md +1937 -0
  6. package/README.md +1493 -3466
  7. package/dist/acl.d.ts +26 -50
  8. package/dist/acl.js +22 -267
  9. package/dist/admin.d.ts +138 -0
  10. package/dist/admin.js +170 -0
  11. package/dist/auth.d.ts +120 -73
  12. package/dist/auth.js +121 -306
  13. package/dist/cache.d.ts +73 -0
  14. package/dist/cache.js +148 -0
  15. package/dist/chain.d.ts +124 -191
  16. package/dist/chain.js +369 -551
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +164 -0
  19. package/dist/demo/booking.d.ts +289 -0
  20. package/dist/demo/booking.js +159 -0
  21. package/dist/errors.d.ts +29 -0
  22. package/dist/errors.js +70 -0
  23. package/dist/import.d.ts +179 -0
  24. package/dist/import.js +792 -0
  25. package/dist/index.d.ts +172 -26
  26. package/dist/index.js +304 -178
  27. package/dist/jsonschema.d.ts +22 -0
  28. package/dist/jsonschema.js +167 -0
  29. package/dist/load.d.ts +76 -0
  30. package/dist/load.js +884 -0
  31. package/dist/model.d.ts +166 -0
  32. package/dist/model.js +224 -0
  33. package/dist/ops.d.ts +7 -6
  34. package/dist/ops.js +7 -51
  35. package/dist/pglite.d.ts +22 -0
  36. package/dist/pglite.js +45 -0
  37. package/dist/registry.d.ts +57 -0
  38. package/dist/registry.js +82 -0
  39. package/dist/sql.d.ts +59 -142
  40. package/dist/sql.js +568 -654
  41. package/dist/sync.d.ts +31 -0
  42. package/dist/sync.js +108 -0
  43. package/dist/tx.d.ts +129 -8
  44. package/dist/tx.js +300 -73
  45. package/dist/typed.d.ts +97 -0
  46. package/dist/typed.js +1 -0
  47. package/dist/types.d.ts +71 -250
  48. package/dist/types.js +27 -108
  49. package/dist/up.d.ts +140 -47
  50. package/dist/up.js +339 -267
  51. package/dist/uuid.d.ts +21 -6
  52. package/dist/uuid.js +48 -64
  53. package/dist/validate.d.ts +24 -0
  54. package/dist/validate.js +251 -0
  55. package/dist/watch.d.ts +62 -0
  56. package/dist/watch.js +168 -0
  57. package/dist/write.d.ts +117 -74
  58. package/dist/write.js +658 -720
  59. package/llms.txt +26 -0
  60. package/package.json +49 -19
  61. package/sql/10-core.sql +136 -0
  62. package/sql/15-errors.sql +60 -0
  63. package/sql/20-context.sql +153 -0
  64. package/sql/30-validate.sql +423 -0
  65. package/sql/40-class.sql +259 -0
  66. package/sql/50-acl.sql +539 -0
  67. package/sql/60-write.sql +1369 -0
  68. package/sql/70-read.sql +245 -0
  69. package/sql/80-auth.sql +827 -0
  70. package/sql/90-time.sql +957 -0
  71. package/sql/95-seed.system.sql +178 -0
  72. package/sql/99-revision.sql +3 -0
  73. package/sql/README.md +56 -0
  74. package/sql/seed.booking.sql +39 -112
  75. package/dist/schema.d.ts +0 -15
  76. package/dist/schema.js +0 -351
  77. package/dist/sessions.d.ts +0 -32
  78. package/dist/sessions.js +0 -114
  79. package/dist/tables.d.ts +0 -105
  80. package/dist/tables.js +0 -248
  81. package/docker/Dockerfile +0 -40
  82. package/docker/start.sh +0 -18
  83. package/scripts/check-docs.mjs +0 -375
  84. package/scripts/gen-api-contract.mjs +0 -226
  85. package/scripts/gen-types.mjs +0 -350
  86. package/scripts/release-notes.mjs +0 -76
  87. package/scripts/schema-sync.mjs +0 -185
  88. package/sql/ddl.sql +0 -600
  89. package/sql/seed.auth.sql +0 -73
@@ -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
- * Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
3
- * Касты по типам полей делает sql.ts на основе Schema.attributes.
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) / отсутствует (false) в data */
36
+ /** поле присутствует (true) или отсутствует (false) */
36
37
  export declare const exists: (yes?: boolean) => Op;
37
- /** поле NULL или отсутствует */
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
- /** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
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
- * Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
3
- * Касты по типам полей делает sql.ts на основе Schema.attributes.
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) / отсутствует (false) в data */
37
+ /** поле присутствует (true) или отсутствует (false) */
75
38
  export const exists = (yes = true) => op('exists', yes);
76
- /** поле NULL или отсутствует */
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
- // START_CONTRACT: or
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);
@@ -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 {};