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
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Нормализация JSON Schema моделей (план, §2.6, §2.13, этап 1, п. 5; этап 2, п. 2).
|
|
3
|
+
*
|
|
4
|
+
* Вход — вывод `z.toJSONSchema()` (или JSON Schema, написанная руками); выход — схема подмножества,
|
|
5
|
+
* которое компилирует база (`jp_compile` в lib/sql/30-validate.sql) и валидатор на TypeScript:
|
|
6
|
+
* - `$schema` убирается, нерекурсивные `$ref`/`$defs` разворачиваются, рекурсия — ошибка модели;
|
|
7
|
+
* - ключевые слова JSON Schema вне подмножества — ошибка с названием (правило не пропадёт молча);
|
|
8
|
+
* прочие ключи (например, из `.meta()` Zod) переносятся в `x-meta`;
|
|
9
|
+
* - у `format` шаблон Zod сохраняется (переведённым): он несёт правила модели — например, ровно
|
|
10
|
+
* три знака после запятой и пояс Z у `z.iso.datetime({ precision: 3 })`, — и без него база
|
|
11
|
+
* приняла бы то, что отклоняет Zod;
|
|
12
|
+
* - `pattern` переводится в явные классы символов, одинаковые для JavaScript и PostgreSQL
|
|
13
|
+
* (`\d` → `[0-9]`, `\w` → `[A-Za-z0-9_]`, `\s` → `[ \t\n\r\f\v]`); `\b`, `\p{…}`, именованные
|
|
14
|
+
* группы и обратные ссылки отклоняются;
|
|
15
|
+
* - имена полей верхнего уровня, начинающиеся с `$`, отклоняются (ими загрузчик помечает служебные ключи).
|
|
16
|
+
*/
|
|
17
|
+
import { LetopisError } from './errors.js';
|
|
18
|
+
/** Подмножество, которое понимают база и валидатор. */
|
|
19
|
+
export const SUPPORTED = new Set([
|
|
20
|
+
'type', 'enum', 'const', 'minLength', 'maxLength', 'pattern', 'format', 'minimum', 'maximum',
|
|
21
|
+
'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf', 'items', 'minItems', 'maxItems', 'uniqueItems',
|
|
22
|
+
'properties', 'required', 'additionalProperties', 'propertyNames', 'anyOf', 'oneOf', 'default',
|
|
23
|
+
'minProperties', 'maxProperties', 'title', 'description', 'examples', 'deprecated',
|
|
24
|
+
]);
|
|
25
|
+
/** Ключевые слова JSON Schema 2020-12 — их нельзя молча перенести в x-meta. */
|
|
26
|
+
const VOCABULARY = new Set([
|
|
27
|
+
'$id', '$schema', '$ref', '$anchor', '$dynamicRef', '$dynamicAnchor', '$vocabulary', '$comment', '$defs', 'definitions',
|
|
28
|
+
'allOf', 'anyOf', 'oneOf', 'not', 'if', 'then', 'else', 'dependentSchemas', 'prefixItems', 'items', 'contains',
|
|
29
|
+
'properties', 'patternProperties', 'additionalProperties', 'propertyNames', 'unevaluatedItems', 'unevaluatedProperties',
|
|
30
|
+
'type', 'enum', 'const', 'multipleOf', 'maximum', 'exclusiveMaximum', 'minimum', 'exclusiveMinimum', 'maxLength',
|
|
31
|
+
'minLength', 'pattern', 'maxItems', 'minItems', 'uniqueItems', 'maxContains', 'minContains', 'maxProperties',
|
|
32
|
+
'minProperties', 'required', 'dependentRequired', 'format', 'contentEncoding', 'contentMediaType', 'contentSchema',
|
|
33
|
+
'title', 'description', 'default', 'deprecated', 'readOnly', 'writeOnly', 'examples',
|
|
34
|
+
]);
|
|
35
|
+
const FORMATS = new Set(['uuid', 'date', 'date-time', 'email']);
|
|
36
|
+
/** Ключи, которые Zod выводит сам и которые ничего не проверяют: их можно опустить. */
|
|
37
|
+
const DROPPED = new Set(['$schema', '$comment', 'readOnly', 'writeOnly', 'id']);
|
|
38
|
+
const SPACE_CLASS = ' \\t\\n\\r\\f\\v';
|
|
39
|
+
/**
|
|
40
|
+
* Перевод шаблона JavaScript в форму, которую одинаково понимают JavaScript (без флага u) и
|
|
41
|
+
* PostgreSQL: обозначения классов заменяются явными наборами, остальное не трогается.
|
|
42
|
+
*/
|
|
43
|
+
export function translatePattern(p, where = 'pattern') {
|
|
44
|
+
let out = '';
|
|
45
|
+
let inClass = false;
|
|
46
|
+
for (let i = 0; i < p.length; i++) {
|
|
47
|
+
const c = p[i];
|
|
48
|
+
if (c === '\\') {
|
|
49
|
+
const n = p[i + 1];
|
|
50
|
+
if (n === undefined)
|
|
51
|
+
throw bad(where, p, 'обратная косая черта в конце шаблона');
|
|
52
|
+
i++;
|
|
53
|
+
if (n === 'd')
|
|
54
|
+
out += inClass ? '0-9' : '[0-9]';
|
|
55
|
+
else if (n === 'w')
|
|
56
|
+
out += inClass ? 'A-Za-z0-9_' : '[A-Za-z0-9_]';
|
|
57
|
+
else if (n === 's')
|
|
58
|
+
out += inClass ? SPACE_CLASS : `[${SPACE_CLASS}]`;
|
|
59
|
+
else if (n === 'D' || n === 'W' || n === 'S') {
|
|
60
|
+
if (inClass)
|
|
61
|
+
throw bad(where, p, `\\${n} внутри набора [ ] не переводится`);
|
|
62
|
+
out += n === 'D' ? '[^0-9]' : n === 'W' ? '[^A-Za-z0-9_]' : `[^${SPACE_CLASS}]`;
|
|
63
|
+
}
|
|
64
|
+
else if (n === 'b' || n === 'B')
|
|
65
|
+
throw bad(where, p, `\\${n} в PostgreSQL значит другое`);
|
|
66
|
+
else if (n === 'p' || n === 'P')
|
|
67
|
+
throw bad(where, p, '\\p{…} не поддерживается');
|
|
68
|
+
else if (n === 'k' || /[1-9]/.test(n))
|
|
69
|
+
throw bad(where, p, 'обратные ссылки не поддерживаются');
|
|
70
|
+
else
|
|
71
|
+
out += `\\${n}`;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!inClass && c === '(' && p.startsWith('(?<', i) && p[i + 3] !== '=' && p[i + 3] !== '!') {
|
|
75
|
+
throw bad(where, p, 'именованные группы не поддерживаются');
|
|
76
|
+
}
|
|
77
|
+
if (c === '[' && !inClass)
|
|
78
|
+
inClass = true;
|
|
79
|
+
else if (c === ']' && inClass)
|
|
80
|
+
inClass = false;
|
|
81
|
+
out += c;
|
|
82
|
+
}
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
function bad(where, p, why) {
|
|
86
|
+
return new LetopisError('invalid_class', `${where}: шаблон ${p} несовместим с регулярными выражениями PostgreSQL (${why})`);
|
|
87
|
+
}
|
|
88
|
+
/** Нормализует JSON Schema; бросает LetopisError('invalid_class') с названием поля и ключевого слова. */
|
|
89
|
+
export function normalizeSchema(input, opts = {}) {
|
|
90
|
+
const where = opts.where ?? 'модель';
|
|
91
|
+
const defs = (input.$defs ?? input.definitions ?? {});
|
|
92
|
+
const resolve = (ref, seen) => {
|
|
93
|
+
const m = /^#\/(?:\$defs|definitions)\/(.+)$/.exec(ref);
|
|
94
|
+
if (!m)
|
|
95
|
+
throw new LetopisError('invalid_class', `${where}: ссылка ${ref} не поддерживается (только #/$defs/…)`);
|
|
96
|
+
const name = decodeURIComponent(m[1].replace(/~1/g, '/').replace(/~0/g, '~'));
|
|
97
|
+
if (seen.includes(name))
|
|
98
|
+
throw new LetopisError('invalid_class', `${where}: рекурсивная модель через ${[...seen, name].join(' → ')}`);
|
|
99
|
+
const target = defs[name];
|
|
100
|
+
if (!target)
|
|
101
|
+
throw new LetopisError('invalid_class', `${where}: определение ${name} не найдено`);
|
|
102
|
+
return node(target, [...seen, name], name);
|
|
103
|
+
};
|
|
104
|
+
const node = (s, seen, path) => {
|
|
105
|
+
if (s === true)
|
|
106
|
+
return {};
|
|
107
|
+
if (s === null || typeof s !== 'object' || Array.isArray(s)) {
|
|
108
|
+
throw new LetopisError('invalid_class', `${where}: описание узла ${path} — не объект`);
|
|
109
|
+
}
|
|
110
|
+
if (typeof s.$ref === 'string') {
|
|
111
|
+
const rest = Object.keys(s).filter((k) => k !== '$ref' && !DROPPED.has(k));
|
|
112
|
+
const base = resolve(s.$ref, seen);
|
|
113
|
+
if (!rest.length)
|
|
114
|
+
return base;
|
|
115
|
+
// $ref с соседними ключами: соседние пометки поверх развёрнутого описания
|
|
116
|
+
return { ...base, ...node(Object.fromEntries(rest.map((k) => [k, s[k]])), seen, path) };
|
|
117
|
+
}
|
|
118
|
+
const out = {};
|
|
119
|
+
const meta = {};
|
|
120
|
+
for (const [k, v] of Object.entries(s)) {
|
|
121
|
+
if (DROPPED.has(k) || k === '$defs' || k === 'definitions')
|
|
122
|
+
continue;
|
|
123
|
+
if (k.startsWith('x-')) {
|
|
124
|
+
out[k] = v;
|
|
125
|
+
}
|
|
126
|
+
else if (SUPPORTED.has(k)) {
|
|
127
|
+
out[k] = v;
|
|
128
|
+
}
|
|
129
|
+
else if (VOCABULARY.has(k)) {
|
|
130
|
+
throw new LetopisError('invalid_class', `${where}: ключевое слово ${k} не поддерживается (узел ${path})`);
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
meta[k] = v; // пометки .meta() Zod и прочие свободные ключи
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(meta).length)
|
|
137
|
+
out['x-meta'] = { ...(out['x-meta'] ?? {}), ...meta };
|
|
138
|
+
if (typeof out.format === 'string') {
|
|
139
|
+
if (!FORMATS.has(out.format))
|
|
140
|
+
throw new LetopisError('invalid_class', `${where}: format ${out.format} не поддерживается (узел ${path})`);
|
|
141
|
+
}
|
|
142
|
+
if (typeof out.pattern === 'string')
|
|
143
|
+
out.pattern = translatePattern(out.pattern, `${where}: ${path}`);
|
|
144
|
+
if (out.properties && typeof out.properties === 'object' && !Array.isArray(out.properties)) {
|
|
145
|
+
out.properties = Object.fromEntries(Object.entries(out.properties).map(([k, v]) => [k, node(v, seen, `${path}.${k}`)]));
|
|
146
|
+
}
|
|
147
|
+
for (const k of ['items', 'propertyNames'])
|
|
148
|
+
if (out[k] !== undefined)
|
|
149
|
+
out[k] = node(out[k], seen, `${path}.${k}`);
|
|
150
|
+
if (out.additionalProperties !== undefined && typeof out.additionalProperties !== 'boolean') {
|
|
151
|
+
out.additionalProperties = node(out.additionalProperties, seen, `${path}.*`);
|
|
152
|
+
}
|
|
153
|
+
for (const k of ['anyOf', 'oneOf']) {
|
|
154
|
+
if (Array.isArray(out[k]))
|
|
155
|
+
out[k] = out[k].map((b, i) => node(b, seen, `${path}.${k}[${i}]`));
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
};
|
|
159
|
+
const out = node(input, [], '$');
|
|
160
|
+
if (opts.topLevel && out.properties && typeof out.properties === 'object') {
|
|
161
|
+
for (const k of Object.keys(out.properties)) {
|
|
162
|
+
if (k.startsWith('$'))
|
|
163
|
+
throw new LetopisError('invalid_class', `${where}: имя поля ${k} начинается с $ — так загрузчик помечает служебные ключи`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out;
|
|
167
|
+
}
|
package/dist/load.d.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type postgres from 'postgres';
|
|
2
|
+
import { type Issue } from './errors.js';
|
|
3
|
+
type Sql = postgres.Sql<Record<string, unknown>>;
|
|
4
|
+
type Raw = Record<string, unknown>;
|
|
5
|
+
export interface LoadOptions {
|
|
6
|
+
/** Повтор id в загрузке: отменить загрузку (по умолчанию), оставить первую или последнюю строку. */
|
|
7
|
+
duplicates?: 'error' | 'first' | 'last';
|
|
8
|
+
/** Объект уже есть в базе: отменить (по умолчанию), пропустить или записать новую версию. */
|
|
9
|
+
existing?: 'error' | 'skip' | 'update';
|
|
10
|
+
/** Строк в пачке COPY (по умолчанию 10 000). */
|
|
11
|
+
batch?: number;
|
|
12
|
+
/** Фиксировать порциями примерно по N строк (порция — классы в порядке зависимостей, цикл — целиком). */
|
|
13
|
+
commitEvery?: number;
|
|
14
|
+
/** Режим истории (импорт): строки — версии с $rev, $at и $op; пишутся волнами. */
|
|
15
|
+
withHistory?: boolean;
|
|
16
|
+
/** В конце перепроверить загруженные строки проверкой базы. */
|
|
17
|
+
verify?: boolean;
|
|
18
|
+
/** Класс строк без $class (CLI --class). */
|
|
19
|
+
class?: string;
|
|
20
|
+
/** Арендатор строк (по умолчанию — арендатор сессии). */
|
|
21
|
+
tenant?: string;
|
|
22
|
+
/** Агент версий (по умолчанию «letopis load»; импорт — «letopis import»). */
|
|
23
|
+
agent?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Загрузка от имени пользователя (§2.6, п. 10): автор версий — он; арендатор (по умолчанию —
|
|
26
|
+
* собственный аккаунта) доступен ему; право WRITE на каждый класс и условия ACL на новые (и в
|
|
27
|
+
* existing: 'update' — прежние) значения строк; цели ссылок ему видны; владелец — по ownerDefault.
|
|
28
|
+
*/
|
|
29
|
+
as?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface LoadReport {
|
|
32
|
+
/** Строк во входе. */
|
|
33
|
+
rows: number;
|
|
34
|
+
/** Записано объектов (новых и воскрешённых). */
|
|
35
|
+
loaded: number;
|
|
36
|
+
/** Новых версий существующих объектов (existing: 'update'). */
|
|
37
|
+
updated: number;
|
|
38
|
+
/** Пропущено существующих (existing: 'skip') и без изменений (existing: 'update'). */
|
|
39
|
+
skipped: number;
|
|
40
|
+
/** Пропущено повторов id (duplicates: 'first' | 'last'). */
|
|
41
|
+
duplicates: number;
|
|
42
|
+
/** Записано по классам. */
|
|
43
|
+
byClass: Record<string, number>;
|
|
44
|
+
/** Разных целей вне загрузки, сверенных запросом в конце. */
|
|
45
|
+
external: number;
|
|
46
|
+
/** Зафиксировано порций. */
|
|
47
|
+
portions: number;
|
|
48
|
+
ms: number;
|
|
49
|
+
}
|
|
50
|
+
/** Нарушение строки в отчёте ошибки: номер строки входа (с 1), код и подробности. */
|
|
51
|
+
export interface LoadIssue {
|
|
52
|
+
line: number;
|
|
53
|
+
code: string;
|
|
54
|
+
message: string;
|
|
55
|
+
id?: string;
|
|
56
|
+
class?: string;
|
|
57
|
+
issues?: Issue[];
|
|
58
|
+
}
|
|
59
|
+
export type LoadInput = Record<string, readonly Raw[]> | readonly Raw[] | Iterable<Raw> | AsyncIterable<Raw>;
|
|
60
|
+
export interface LoadEnv {
|
|
61
|
+
/** Пул администраторского подключения. */
|
|
62
|
+
sql: Sql;
|
|
63
|
+
schema: string;
|
|
64
|
+
tenant: string | null;
|
|
65
|
+
/** Подключение может стать владельцем (set role) — иначе admin_required. */
|
|
66
|
+
admin: boolean;
|
|
67
|
+
ownerRole: string;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Загрузить строки (§2.6). Одна транзакция (или порции commitEvery): описания классов под
|
|
71
|
+
* разделяемыми блокировками семейств, проверки в коде, COPY пачками, запрос по целям вне загрузки,
|
|
72
|
+
* сигнал, ANALYZE для больших загрузок. Ошибка любой строки отменяет загрузку (порцию) — отчёт
|
|
73
|
+
* называет строки и нарушения.
|
|
74
|
+
*/
|
|
75
|
+
export declare function load(env: LoadEnv, input: LoadInput, opts?: LoadOptions): Promise<LoadReport>;
|
|
76
|
+
export {};
|