letopis 0.5.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/CHANGELOG.md +77 -0
- package/README.md +799 -0
- package/dist/chain.d.ts +126 -0
- package/dist/chain.js +278 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +43 -0
- package/dist/ops.d.ts +42 -0
- package/dist/ops.js +43 -0
- package/dist/schema.d.ts +15 -0
- package/dist/schema.js +133 -0
- package/dist/sql.d.ts +92 -0
- package/dist/sql.js +480 -0
- package/dist/tables.d.ts +81 -0
- package/dist/tables.js +148 -0
- package/dist/tx.d.ts +13 -0
- package/dist/tx.js +31 -0
- package/dist/types.d.ts +166 -0
- package/dist/types.js +5 -0
- package/dist/write.d.ts +60 -0
- package/dist/write.js +302 -0
- package/package.json +46 -0
package/dist/chain.d.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dot-цепочки: db.Сотрудник({name:'Вася'}).навык().Услуга().execute()
|
|
3
|
+
* db и цепочка — Proxy над реестром классов (id и alias работают одинаково).
|
|
4
|
+
*
|
|
5
|
+
* Выборка настраивается модификаторами: .limit(n) .offset(n) .sort(field, dir?)
|
|
6
|
+
* Колонки — модификаторами: .tags(…) .account(…) .owner(…); ключ путей — .alias(…).
|
|
7
|
+
* Запись: .set(data) — связи задаются контекст-шагами цепочки; .delete().
|
|
8
|
+
*/
|
|
9
|
+
import type { Row, Path, Filter } from './types.js';
|
|
10
|
+
import { type Ctx } from './sql.js';
|
|
11
|
+
import { type BatchOp } from './write.js';
|
|
12
|
+
import { type Tables } from './tables.js';
|
|
13
|
+
import type { Registry } from './schema.js';
|
|
14
|
+
export interface ChainCore {
|
|
15
|
+
/** Пути: [{шаг1: Row, шаг2: Row, …}, …] — все узлы каждого варианта. */
|
|
16
|
+
execute(): Promise<Path[]>;
|
|
17
|
+
/** Уникальные сущности последнего шага. */
|
|
18
|
+
rows(): Promise<Row[]>;
|
|
19
|
+
first(): Promise<Row | null>;
|
|
20
|
+
ids(): Promise<string[]>;
|
|
21
|
+
count(): Promise<number>;
|
|
22
|
+
/** Максимум строк/путей. */
|
|
23
|
+
limit(n: number): Chain;
|
|
24
|
+
/** Пропустить N. */
|
|
25
|
+
offset(n: number): Chain;
|
|
26
|
+
/** Сортировка по полю последнего шага: 'updated' | 'data.<поле>' (каст по типу из Schema). */
|
|
27
|
+
sort(field: string, dir?: 'asc' | 'desc' | boolean): Chain;
|
|
28
|
+
/** Чтение «как было на момент T» (ISO-строка или Date): версии позже T невидимы. */
|
|
29
|
+
asOf(t: string | Date): Chain;
|
|
30
|
+
/** Keyset-пагинация: строго после курсора (см. cursorOf). Требует .sort(). */
|
|
31
|
+
after(cursor: import('./types.js').Cursor): Chain;
|
|
32
|
+
/** ВСЕ версии сущностей последнего шага (включая tombstone → $deleted), по возрастанию updated. */
|
|
33
|
+
versions(): Promise<Row[]>;
|
|
34
|
+
/** Рекурсивный self-обход: дети любой глубины (тот же класс), $depth в Row. */
|
|
35
|
+
deep(max?: number): Chain;
|
|
36
|
+
/** Агрегации по полю последнего шага ('data.<путь>'), считает БД. */
|
|
37
|
+
sum(field: string): Promise<number | null>;
|
|
38
|
+
avg(field: string): Promise<number | null>;
|
|
39
|
+
min(field: string): Promise<unknown>;
|
|
40
|
+
max(field: string): Promise<unknown>;
|
|
41
|
+
countBy(field: string): Promise<Record<string, number>>;
|
|
42
|
+
/** GDPR: новая версия с затёртыми string-полями ('[erased]') + тег 'anonymized'. История остаётся (см. README). */
|
|
43
|
+
anonymize(fields: string[]): Promise<Row[]>;
|
|
44
|
+
/** Переименовать ключ ТЕКУЩЕГО шага в выводе путей. */
|
|
45
|
+
alias(name: string): Chain;
|
|
46
|
+
/** Колонка tags: строка | string[] (все) | has/hasAny/hasAll. В set() — значение тегов. */
|
|
47
|
+
tags(v: string | string[] | object): Chain;
|
|
48
|
+
/** Колонка account (uuid или Row/Account). В set()-insert — значение. */
|
|
49
|
+
account(v: string | {
|
|
50
|
+
id: string;
|
|
51
|
+
}): Chain;
|
|
52
|
+
/** Колонка owner (uuid или Row/Account). В set()-insert — значение. */
|
|
53
|
+
owner(v: string | {
|
|
54
|
+
id: string;
|
|
55
|
+
}): Chain;
|
|
56
|
+
/**
|
|
57
|
+
* Записать. Связи — контекст-шагами до set() И/ИЛИ класс-вызовами после:
|
|
58
|
+
* await db.Сотрудник(s).занятость(id).set({kind}).Окно(w)
|
|
59
|
+
* const z = db.занятость(id).set({kind}); z.Сотрудник(s); z.Окно(w); await z
|
|
60
|
+
* Исполняется ОДНИМ INSERT при первом await (промис кешируется).
|
|
61
|
+
* Пустой фильтр без data.id → INSERT; id (фильтр или data.id) → UPSERT;
|
|
62
|
+
* фильтр-объект → версия каждого найденного.
|
|
63
|
+
*/
|
|
64
|
+
set(data?: Record<string, unknown>): SetChain;
|
|
65
|
+
/** Серверный tombstone + каскад; возвращает всё удалённое с $deleted: true. */
|
|
66
|
+
delete(): Promise<Row[]>;
|
|
67
|
+
}
|
|
68
|
+
/** Ленивый билдер записи: довешивание связей + thenable (исполнение при await). */
|
|
69
|
+
export type SetChain = PromiseLike<Row[]> & {
|
|
70
|
+
[className: string]: (target: string | {
|
|
71
|
+
id: string;
|
|
72
|
+
}) => SetChain;
|
|
73
|
+
};
|
|
74
|
+
export type Chain = ChainCore & {
|
|
75
|
+
[className: string]: (filter?: Filter) => Chain;
|
|
76
|
+
};
|
|
77
|
+
export interface DbCore {
|
|
78
|
+
/** Открыть транзакцию: tr — тот же API + commit/rollback/lock. */
|
|
79
|
+
begin(): Promise<EntityTx>;
|
|
80
|
+
commit(tr: EntityTx): Promise<void>;
|
|
81
|
+
rollback(tr: EntityTx): Promise<void>;
|
|
82
|
+
/** Именованный батч: те же цепочки, set/delete копятся до execute(). */
|
|
83
|
+
batch(name: string): Batch;
|
|
84
|
+
/**
|
|
85
|
+
* Realtime: события каждой вставленной версии (insert/update/delete-tombstone).
|
|
86
|
+
* watch(cb) — все классы; watch('Запись', cb) — один. Возврат — stop-функция.
|
|
87
|
+
*/
|
|
88
|
+
watch(classOrCb: string | ((e: WatchEvent) => void), cb?: (e: WatchEvent) => void): Promise<() => void>;
|
|
89
|
+
close(): Promise<void>;
|
|
90
|
+
registry: Registry;
|
|
91
|
+
/** Голый postgres-клиент (тесты, EXPLAIN). */
|
|
92
|
+
sql: Ctx['sql'];
|
|
93
|
+
/** Служебные таблицы auth/ACL (обычные, вне версий/цепочек). */
|
|
94
|
+
accounts: Tables['accounts'];
|
|
95
|
+
credentials: Tables['credentials'];
|
|
96
|
+
resources: Tables['resources'];
|
|
97
|
+
rules: Tables['rules'];
|
|
98
|
+
}
|
|
99
|
+
export type EntityDb = DbCore & {
|
|
100
|
+
[className: string]: (filter?: Filter) => Chain;
|
|
101
|
+
};
|
|
102
|
+
export type EntityTx = EntityDb & {
|
|
103
|
+
commit(): Promise<void>;
|
|
104
|
+
rollback(): Promise<void>;
|
|
105
|
+
lock(...keys: (string | number)[]): Promise<void>;
|
|
106
|
+
};
|
|
107
|
+
/** Событие watch(): факт новой версии; данные дочитываются запросом при надобности. */
|
|
108
|
+
export interface WatchEvent {
|
|
109
|
+
partition: string;
|
|
110
|
+
class: string;
|
|
111
|
+
id: string;
|
|
112
|
+
updated: string;
|
|
113
|
+
deleted: boolean;
|
|
114
|
+
}
|
|
115
|
+
export type Batch = {
|
|
116
|
+
execute(): Promise<Row[][]>;
|
|
117
|
+
discard(): void;
|
|
118
|
+
size(): number;
|
|
119
|
+
} & {
|
|
120
|
+
[className: string]: (filter?: Filter) => Chain;
|
|
121
|
+
};
|
|
122
|
+
interface RootState {
|
|
123
|
+
batches: Map<string, BatchOp[]>;
|
|
124
|
+
}
|
|
125
|
+
export declare function makeDb(ctx: Ctx, root?: RootState): EntityDb;
|
|
126
|
+
export {};
|
package/dist/chain.js
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import { buildRead, runQuery } from './sql.js';
|
|
2
|
+
import { setOp, delOp, anonymizeOp, executeBatch, toRow } from './write.js';
|
|
3
|
+
import { beginTx, lock } from './tx.js';
|
|
4
|
+
import { makeTables } from './tables.js';
|
|
5
|
+
/** Фильтр-аргумент принимает и Row/Account-объект — берётся его id. */
|
|
6
|
+
function normalizeFilter(f) {
|
|
7
|
+
if (f !== undefined &&
|
|
8
|
+
typeof f === 'object' &&
|
|
9
|
+
!Array.isArray(f) &&
|
|
10
|
+
typeof f.id === 'string' &&
|
|
11
|
+
(typeof f.class === 'string' || 'categories' in f)) {
|
|
12
|
+
return f.id;
|
|
13
|
+
}
|
|
14
|
+
return f;
|
|
15
|
+
}
|
|
16
|
+
const clsOf = (steps) => steps.map((s) => s.cls.id);
|
|
17
|
+
async function runPaths(ctx, steps, mods) {
|
|
18
|
+
const q = buildRead(ctx, steps, mods, 'paths');
|
|
19
|
+
const res = await runQuery(ctx, q.text, q.params, 'paths', clsOf(steps));
|
|
20
|
+
return res.map((r) => {
|
|
21
|
+
const out = {};
|
|
22
|
+
for (const [k, v] of Object.entries(r.path))
|
|
23
|
+
out[k] = toRow(v);
|
|
24
|
+
return out;
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
async function runRows(ctx, steps, mods) {
|
|
28
|
+
const q = buildRead(ctx, steps, mods, 'rows');
|
|
29
|
+
const res = await runQuery(ctx, q.text, q.params, 'rows', clsOf(steps));
|
|
30
|
+
return res.map((r) => {
|
|
31
|
+
const row = toRow(r.row);
|
|
32
|
+
const d = r.row.depth;
|
|
33
|
+
return d != null ? { ...row, $depth: d } : row;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const idOf = (t) => (typeof t === 'object' ? t.id : t);
|
|
37
|
+
/**
|
|
38
|
+
* Ленивый билдер set(): класс-вызовы довешивают связи (мутирует), await исполняет один раз.
|
|
39
|
+
* run() — исполнение (сразу setOp или отложенно через батч); links — общая ссылка,
|
|
40
|
+
* которую батч читает на execute().
|
|
41
|
+
*/
|
|
42
|
+
function makeSetChain(registry, links, run) {
|
|
43
|
+
let done;
|
|
44
|
+
const handler = {
|
|
45
|
+
get(_t, prop) {
|
|
46
|
+
if (typeof prop === 'symbol')
|
|
47
|
+
return undefined;
|
|
48
|
+
if (prop === 'then' || prop === 'catch' || prop === 'finally') {
|
|
49
|
+
done ??= run();
|
|
50
|
+
const m = done[prop];
|
|
51
|
+
return m.bind(done);
|
|
52
|
+
}
|
|
53
|
+
if (registry.has(prop)) {
|
|
54
|
+
const cls = registry.resolve(prop);
|
|
55
|
+
return (target) => {
|
|
56
|
+
if (done)
|
|
57
|
+
throw new Error(`letopis: set() already executed — link "${prop}" before the first await`);
|
|
58
|
+
links[cls.id] = idOf(target);
|
|
59
|
+
return proxy;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
registry.resolve(prop);
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
const proxy = new Proxy({}, handler);
|
|
67
|
+
return proxy;
|
|
68
|
+
}
|
|
69
|
+
function makeChain(ctx, steps, mods, batchQueue) {
|
|
70
|
+
const withLast = (patch) => {
|
|
71
|
+
const next = steps.slice();
|
|
72
|
+
next[next.length - 1] = { ...next[next.length - 1], ...patch };
|
|
73
|
+
return makeChain(ctx, next, mods, batchQueue);
|
|
74
|
+
};
|
|
75
|
+
const withMods = (patch) => makeChain(ctx, steps, { ...mods, ...patch }, batchQueue);
|
|
76
|
+
const handler = {
|
|
77
|
+
get(_t, prop) {
|
|
78
|
+
if (typeof prop === 'symbol' || prop === 'then')
|
|
79
|
+
return undefined;
|
|
80
|
+
switch (prop) {
|
|
81
|
+
case 'execute':
|
|
82
|
+
return () => runPaths(ctx, steps, mods);
|
|
83
|
+
case 'rows':
|
|
84
|
+
return () => runRows(ctx, steps, mods);
|
|
85
|
+
case 'first':
|
|
86
|
+
return async () => (await runRows(ctx, steps, { ...mods, limit: 1 }))[0] ?? null;
|
|
87
|
+
case 'ids':
|
|
88
|
+
return async () => {
|
|
89
|
+
const q = buildRead(ctx, steps, mods, 'ids');
|
|
90
|
+
const res = await runQuery(ctx, q.text, q.params, 'ids', clsOf(steps));
|
|
91
|
+
return res.map((r) => r.id);
|
|
92
|
+
};
|
|
93
|
+
case 'count':
|
|
94
|
+
return async () => {
|
|
95
|
+
const q = buildRead(ctx, steps, mods, 'count');
|
|
96
|
+
const res = await runQuery(ctx, q.text, q.params, 'count', clsOf(steps));
|
|
97
|
+
return res[0].n;
|
|
98
|
+
};
|
|
99
|
+
case 'limit':
|
|
100
|
+
return (n) => withMods({ limit: n });
|
|
101
|
+
case 'offset':
|
|
102
|
+
return (n) => withMods({ offset: n });
|
|
103
|
+
case 'sort':
|
|
104
|
+
return (field, dir) => withMods({ order: field, desc: dir === true || dir === 'desc' });
|
|
105
|
+
case 'asOf':
|
|
106
|
+
return (t) => withMods({ asOf: t instanceof Date ? t.toISOString() : t });
|
|
107
|
+
case 'deep':
|
|
108
|
+
return (max = 32) => withLast({ deepMax: max });
|
|
109
|
+
case 'sum':
|
|
110
|
+
case 'avg':
|
|
111
|
+
case 'min':
|
|
112
|
+
case 'max':
|
|
113
|
+
return async (field) => {
|
|
114
|
+
const q = buildRead(ctx, steps, { ...mods, aggFn: prop, aggField: field }, 'agg');
|
|
115
|
+
const res = await runQuery(ctx, q.text, q.params, 'agg', clsOf(steps));
|
|
116
|
+
const v = res[0]?.v;
|
|
117
|
+
return v === null || v === undefined ? null : typeof v === 'string' && prop !== 'min' && prop !== 'max' ? Number(v) : v;
|
|
118
|
+
};
|
|
119
|
+
case 'countBy':
|
|
120
|
+
return async (field) => {
|
|
121
|
+
const q = buildRead(ctx, steps, { ...mods, aggFn: 'countBy', aggField: field }, 'agg');
|
|
122
|
+
const res = await runQuery(ctx, q.text, q.params, 'agg', clsOf(steps));
|
|
123
|
+
return Object.fromEntries(res.map((r) => [r.k ?? 'null', r.n]));
|
|
124
|
+
};
|
|
125
|
+
case 'after':
|
|
126
|
+
return (cursor) => withMods({ after: cursor });
|
|
127
|
+
case 'versions':
|
|
128
|
+
return async () => {
|
|
129
|
+
const q = buildRead(ctx, steps, mods, 'versions');
|
|
130
|
+
const res = await runQuery(ctx, q.text, q.params, 'versions', clsOf(steps));
|
|
131
|
+
return res.map((r) => {
|
|
132
|
+
const raw = r.row;
|
|
133
|
+
const row = toRow(r.row);
|
|
134
|
+
return raw.deleted ? { ...row, $deleted: true } : row;
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
case 'anonymize':
|
|
138
|
+
return (fields) => anonymizeOp(ctx, steps, mods, fields);
|
|
139
|
+
case 'alias':
|
|
140
|
+
return (name) => withLast({ aliasKey: name });
|
|
141
|
+
case 'tags':
|
|
142
|
+
return (v) => withLast({ tagsFilter: v });
|
|
143
|
+
case 'account':
|
|
144
|
+
return (v) => withLast({ accountFilter: typeof v === 'object' ? v.id : v });
|
|
145
|
+
case 'owner':
|
|
146
|
+
return (v) => withLast({ ownerFilter: typeof v === 'object' ? v.id : v });
|
|
147
|
+
case 'set':
|
|
148
|
+
return (data = {}) => {
|
|
149
|
+
const extraLinks = {};
|
|
150
|
+
if (batchQueue) {
|
|
151
|
+
const op = { kind: 'set', steps, mods, data, extraLinks };
|
|
152
|
+
batchQueue.push(op);
|
|
153
|
+
const index = batchQueue.length - 1;
|
|
154
|
+
// билдер для довешивания связей; await возвращает № в очереди
|
|
155
|
+
return makeSetChain(ctx.registry, extraLinks, async () => index);
|
|
156
|
+
}
|
|
157
|
+
return makeSetChain(ctx.registry, extraLinks, () => setOp(ctx, steps, mods, data, extraLinks));
|
|
158
|
+
};
|
|
159
|
+
case 'delete':
|
|
160
|
+
return () => {
|
|
161
|
+
if (batchQueue) {
|
|
162
|
+
batchQueue.push({ kind: 'delete', steps, mods });
|
|
163
|
+
return batchQueue.length - 1;
|
|
164
|
+
}
|
|
165
|
+
return delOp(ctx, steps, mods);
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
if (ctx.registry.has(prop)) {
|
|
169
|
+
const cls = ctx.registry.resolve(prop);
|
|
170
|
+
return (filter) => makeChain(ctx, [...steps, { name: prop, cls, filter: normalizeFilter(filter) }], mods, batchQueue);
|
|
171
|
+
}
|
|
172
|
+
ctx.registry.resolve(prop); // неизвестный класс — понятная ошибка со списком
|
|
173
|
+
return undefined;
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
return new Proxy({}, handler);
|
|
177
|
+
}
|
|
178
|
+
function makeBatch(ctx, queue) {
|
|
179
|
+
const handler = {
|
|
180
|
+
get(_t, prop) {
|
|
181
|
+
if (typeof prop === 'symbol' || prop === 'then')
|
|
182
|
+
return undefined;
|
|
183
|
+
switch (prop) {
|
|
184
|
+
case 'execute':
|
|
185
|
+
return async () => {
|
|
186
|
+
const ops = queue.splice(0, queue.length);
|
|
187
|
+
return executeBatch(ctx, ops);
|
|
188
|
+
};
|
|
189
|
+
case 'discard':
|
|
190
|
+
return () => void queue.splice(0, queue.length);
|
|
191
|
+
case 'size':
|
|
192
|
+
return () => queue.length;
|
|
193
|
+
}
|
|
194
|
+
if (ctx.registry.has(prop)) {
|
|
195
|
+
const cls = ctx.registry.resolve(prop);
|
|
196
|
+
return (filter) => makeChain(ctx, [{ name: String(prop), cls, filter: normalizeFilter(filter) }], {}, queue);
|
|
197
|
+
}
|
|
198
|
+
ctx.registry.resolve(prop);
|
|
199
|
+
return undefined;
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
return new Proxy({}, handler);
|
|
203
|
+
}
|
|
204
|
+
export function makeDb(ctx, root) {
|
|
205
|
+
const state = root ?? { batches: new Map() };
|
|
206
|
+
let tables; // лениво, на ctx этого фасада (работает и в tr)
|
|
207
|
+
const handler = {
|
|
208
|
+
get(_t, prop) {
|
|
209
|
+
if (typeof prop === 'symbol' || prop === 'then')
|
|
210
|
+
return undefined;
|
|
211
|
+
if (prop === 'accounts' || prop === 'credentials' || prop === 'resources' || prop === 'rules') {
|
|
212
|
+
tables ??= makeTables(ctx);
|
|
213
|
+
return tables[prop];
|
|
214
|
+
}
|
|
215
|
+
switch (prop) {
|
|
216
|
+
case 'begin':
|
|
217
|
+
return async () => makeDb(await beginTx(ctx), state);
|
|
218
|
+
case 'commit':
|
|
219
|
+
return (tr) => {
|
|
220
|
+
if (tr)
|
|
221
|
+
return tr.commit();
|
|
222
|
+
const c = ctx;
|
|
223
|
+
if (typeof c.commit === 'function')
|
|
224
|
+
return c.commit();
|
|
225
|
+
throw new Error('letopis: commit() needs a transaction: db.commit(tr) or tr.commit()');
|
|
226
|
+
};
|
|
227
|
+
case 'rollback':
|
|
228
|
+
return (tr) => {
|
|
229
|
+
if (tr)
|
|
230
|
+
return tr.rollback();
|
|
231
|
+
const c = ctx;
|
|
232
|
+
if (typeof c.rollback === 'function')
|
|
233
|
+
return c.rollback();
|
|
234
|
+
throw new Error('letopis: rollback() needs a transaction: db.rollback(tr) or tr.rollback()');
|
|
235
|
+
};
|
|
236
|
+
case 'lock':
|
|
237
|
+
return (...keys) => lock(ctx, ...keys);
|
|
238
|
+
case 'batch':
|
|
239
|
+
return (name) => {
|
|
240
|
+
let q = state.batches.get(name);
|
|
241
|
+
if (!q) {
|
|
242
|
+
q = [];
|
|
243
|
+
state.batches.set(name, q);
|
|
244
|
+
}
|
|
245
|
+
return makeBatch(ctx, q);
|
|
246
|
+
};
|
|
247
|
+
case 'watch':
|
|
248
|
+
return async (a, b) => {
|
|
249
|
+
const cls = typeof a === 'string' ? ctx.registry.resolve(a).id : undefined;
|
|
250
|
+
const cb = typeof a === 'function' ? a : b;
|
|
251
|
+
const listener = (payload) => {
|
|
252
|
+
const e = JSON.parse(payload);
|
|
253
|
+
if (e.partition !== ctx.partition)
|
|
254
|
+
return;
|
|
255
|
+
if (cls && e.class !== cls)
|
|
256
|
+
return;
|
|
257
|
+
cb(e);
|
|
258
|
+
};
|
|
259
|
+
const { unlisten } = await ctx.sql.listen(ctx.pgSchema, listener);
|
|
260
|
+
return unlisten;
|
|
261
|
+
};
|
|
262
|
+
case 'close':
|
|
263
|
+
return () => ctx.sql.end();
|
|
264
|
+
case 'registry':
|
|
265
|
+
return ctx.registry;
|
|
266
|
+
case 'sql':
|
|
267
|
+
return ctx.sql;
|
|
268
|
+
}
|
|
269
|
+
if (ctx.registry.has(prop)) {
|
|
270
|
+
const cls = ctx.registry.resolve(prop);
|
|
271
|
+
return (filter) => makeChain(ctx, [{ name: String(prop), cls, filter: normalizeFilter(filter) }], {});
|
|
272
|
+
}
|
|
273
|
+
ctx.registry.resolve(prop);
|
|
274
|
+
return undefined;
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
return new Proxy({}, handler);
|
|
278
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type EntityDb } from './chain.js';
|
|
2
|
+
import type { ConnectOpts } from './types.js';
|
|
3
|
+
export declare function connect(opts: ConnectOpts): Promise<EntityDb>;
|
|
4
|
+
/** Курсор keyset-пагинации из последней строки страницы (field как в .sort()). */
|
|
5
|
+
export declare function cursorOf(row: import('./types.js').Row, field?: string): import('./types.js').Cursor;
|
|
6
|
+
export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, } from './ops.js';
|
|
7
|
+
export type { Row, Path, Filter, ChainMods, Cursor, ConnectOpts, QueryEvent, Account, Credential, Resource, Rule, } from './types.js';
|
|
8
|
+
export type { EntityDb, EntityTx, Chain, Batch } from './chain.js';
|
|
9
|
+
export type { Tables, AccountsApi, CredentialsApi, ResourcesApi, RulesApi } from './tables.js';
|
|
10
|
+
export { ValidationError } from './write.js';
|
|
11
|
+
export { Registry } from './schema.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* letopis: dot-цепочки над append-only Entity-хранилищем (TimescaleDB).
|
|
3
|
+
*
|
|
4
|
+
* const db = await connect({ dsn, schema: 'booking' })
|
|
5
|
+
* await db.Сотрудник({ name: 'Вася' }).навык().Услуга().execute()
|
|
6
|
+
*/
|
|
7
|
+
import postgres from 'postgres';
|
|
8
|
+
import { loadRegistry } from './schema.js';
|
|
9
|
+
import { makeDb } from './chain.js';
|
|
10
|
+
export async function connect(opts) {
|
|
11
|
+
// timestamptz — строкой (JS Date режет микросекунды → ломал бы asOf/cursorOf по updated)
|
|
12
|
+
const sql = postgres(opts.dsn, {
|
|
13
|
+
max: opts.max ?? 10,
|
|
14
|
+
types: { timestamptz: { to: 1184, from: [1184], serialize: (v) => v, parse: (v) => v } },
|
|
15
|
+
});
|
|
16
|
+
const partition = opts.partition ?? 'entity';
|
|
17
|
+
const registry = await loadRegistry(sql, opts.schema, partition);
|
|
18
|
+
// Entity.account NOT NULL: fallback — System-аккаунт (семантика legacy-движка)
|
|
19
|
+
const ident = `"${opts.schema.replace(/"/g, '""')}"`;
|
|
20
|
+
const sys = (await sql.unsafe(`SELECT id FROM ${ident}."Account" WHERE 'System' = ANY(categories) LIMIT 1`));
|
|
21
|
+
const ctx = {
|
|
22
|
+
sql,
|
|
23
|
+
registry,
|
|
24
|
+
pgSchema: opts.schema,
|
|
25
|
+
partition,
|
|
26
|
+
account: opts.account,
|
|
27
|
+
owner: opts.owner,
|
|
28
|
+
systemAccount: sys[0]?.id,
|
|
29
|
+
enforceAccount: opts.enforceAccount,
|
|
30
|
+
onQuery: opts.onQuery,
|
|
31
|
+
slowMs: opts.slowMs,
|
|
32
|
+
};
|
|
33
|
+
return makeDb(ctx);
|
|
34
|
+
}
|
|
35
|
+
/** Курсор keyset-пагинации из последней строки страницы (field как в .sort()). */
|
|
36
|
+
export function cursorOf(row, field = 'updated') {
|
|
37
|
+
const v = field === 'updated' ? row.updated : row.data[field.slice(5)];
|
|
38
|
+
return { v, id: row.id };
|
|
39
|
+
}
|
|
40
|
+
// операторы фильтров
|
|
41
|
+
export { ne, gt, gte, lt, lte, between, inList, like, ilike, starts, ends, has, hasAny, hasAll, exists, isNull, not, or, } from './ops.js';
|
|
42
|
+
export { ValidationError } from './write.js';
|
|
43
|
+
export { Registry } from './schema.js';
|
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
|
|
3
|
+
* Касты по типам полей делает sql.ts на основе Schema.attributes.
|
|
4
|
+
*/
|
|
5
|
+
import { type Op, type Scalar } from './types.js';
|
|
6
|
+
export declare const isOp: (v: unknown) => v is Op;
|
|
7
|
+
/** ≠ (IS DISTINCT FROM) */
|
|
8
|
+
export declare const ne: (v: Scalar) => Op;
|
|
9
|
+
/** > */
|
|
10
|
+
export declare const gt: (v: Scalar) => Op;
|
|
11
|
+
/** ≥ */
|
|
12
|
+
export declare const gte: (v: Scalar) => Op;
|
|
13
|
+
/** < */
|
|
14
|
+
export declare const lt: (v: Scalar) => Op;
|
|
15
|
+
/** ≤ */
|
|
16
|
+
export declare const lte: (v: Scalar) => Op;
|
|
17
|
+
/** a ≤ x ≤ b */
|
|
18
|
+
export declare const between: (a: Scalar, b: Scalar) => Op;
|
|
19
|
+
/** значение из списка */
|
|
20
|
+
export declare const inList: (vs: Scalar[]) => Op;
|
|
21
|
+
/** LIKE (шаблон с % и _) */
|
|
22
|
+
export declare const like: (s: string) => Op;
|
|
23
|
+
/** ILIKE (без регистра) */
|
|
24
|
+
export declare const ilike: (s: string) => Op;
|
|
25
|
+
/** начинается с */
|
|
26
|
+
export declare const starts: (s: string) => Op;
|
|
27
|
+
/** заканчивается на */
|
|
28
|
+
export declare const ends: (s: string) => Op;
|
|
29
|
+
/** массив-поле содержит значение */
|
|
30
|
+
export declare const has: (v: Scalar) => Op;
|
|
31
|
+
/** массив-поле содержит хотя бы одно из */
|
|
32
|
+
export declare const hasAny: (vs: Scalar[]) => Op;
|
|
33
|
+
/** массив-поле содержит все */
|
|
34
|
+
export declare const hasAll: (vs: Scalar[]) => Op;
|
|
35
|
+
/** поле присутствует (true) / отсутствует (false) в data */
|
|
36
|
+
export declare const exists: (yes?: boolean) => Op;
|
|
37
|
+
/** поле NULL или отсутствует */
|
|
38
|
+
export declare const isNull: () => Op;
|
|
39
|
+
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр → «не равно» */
|
|
40
|
+
export declare const not: (v: Scalar | Op) => Op;
|
|
41
|
+
/** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
|
|
42
|
+
export declare const or: (...filters: Record<string, unknown>[]) => Op;
|
package/dist/ops.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Операторы фильтров. Symbol-tagged объекты — не пересекаются с данными.
|
|
3
|
+
* Касты по типам полей делает sql.ts на основе Schema.attributes.
|
|
4
|
+
*/
|
|
5
|
+
import { OP } from './types.js';
|
|
6
|
+
const op = (name, ...args) => ({ [OP]: name, args });
|
|
7
|
+
export const isOp = (v) => typeof v === 'object' && v !== null && OP in v;
|
|
8
|
+
/** ≠ (IS DISTINCT FROM) */
|
|
9
|
+
export const ne = (v) => op('ne', v);
|
|
10
|
+
/** > */
|
|
11
|
+
export const gt = (v) => op('gt', v);
|
|
12
|
+
/** ≥ */
|
|
13
|
+
export const gte = (v) => op('gte', v);
|
|
14
|
+
/** < */
|
|
15
|
+
export const lt = (v) => op('lt', v);
|
|
16
|
+
/** ≤ */
|
|
17
|
+
export const lte = (v) => op('lte', v);
|
|
18
|
+
/** a ≤ x ≤ b */
|
|
19
|
+
export const between = (a, b) => op('between', a, b);
|
|
20
|
+
/** значение из списка */
|
|
21
|
+
export const inList = (vs) => op('in', vs);
|
|
22
|
+
/** LIKE (шаблон с % и _) */
|
|
23
|
+
export const like = (s) => op('like', s);
|
|
24
|
+
/** ILIKE (без регистра) */
|
|
25
|
+
export const ilike = (s) => op('ilike', s);
|
|
26
|
+
/** начинается с */
|
|
27
|
+
export const starts = (s) => op('like', s + '%');
|
|
28
|
+
/** заканчивается на */
|
|
29
|
+
export const ends = (s) => op('like', '%' + s);
|
|
30
|
+
/** массив-поле содержит значение */
|
|
31
|
+
export const has = (v) => op('has', v);
|
|
32
|
+
/** массив-поле содержит хотя бы одно из */
|
|
33
|
+
export const hasAny = (vs) => op('hasAny', vs);
|
|
34
|
+
/** массив-поле содержит все */
|
|
35
|
+
export const hasAll = (vs) => op('hasAll', vs);
|
|
36
|
+
/** поле присутствует (true) / отсутствует (false) в data */
|
|
37
|
+
export const exists = (yes = true) => op('exists', yes);
|
|
38
|
+
/** поле NULL или отсутствует */
|
|
39
|
+
export const isNull = () => op('isNull');
|
|
40
|
+
/** НЕ-условие по полю: not(ilike('%тест%')); скаляр → «не равно» */
|
|
41
|
+
export const not = (v) => op('not', v);
|
|
42
|
+
/** ИЛИ на уровне фильтра шага: db.Запись(or({status:'created'}, {status:'confirmed'})) */
|
|
43
|
+
export const or = (...filters) => op('or', filters);
|
package/dist/schema.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Sql } from 'postgres';
|
|
2
|
+
import type { ClassDef, FieldType } from './types.js';
|
|
3
|
+
export declare class Registry {
|
|
4
|
+
private byName;
|
|
5
|
+
readonly all: ClassDef[];
|
|
6
|
+
add(def: ClassDef): void;
|
|
7
|
+
/** Класс по id или alias; undefined если нет. */
|
|
8
|
+
find(name: string): ClassDef | undefined;
|
|
9
|
+
/** Класс по id или alias; иначе понятная ошибка со списком. */
|
|
10
|
+
resolve(name: string): ClassDef;
|
|
11
|
+
has(name: string): boolean;
|
|
12
|
+
}
|
|
13
|
+
/** Тип поля из fastest-validator DSL — для каста в SQL. */
|
|
14
|
+
export declare function fieldTypeOf(attr: unknown): FieldType;
|
|
15
|
+
export declare function loadRegistry(sql: Sql, pgSchema: string, partition: string): Promise<Registry>;
|