@ultimabase/postgres 0.0.0-stage → 0.1.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 +12 -0
- package/README.md +4 -2
- package/dist/adapter.d.ts +26 -0
- package/dist/adapter.d.ts.map +1 -0
- package/dist/adapter.js +254 -0
- package/dist/aggregate.d.ts +8 -0
- package/dist/aggregate.d.ts.map +1 -0
- package/dist/aggregate.js +138 -0
- package/dist/decode.d.ts +5 -0
- package/dist/decode.d.ts.map +1 -0
- package/dist/decode.js +30 -0
- package/dist/fills.d.ts +16 -0
- package/dist/fills.d.ts.map +1 -0
- package/dist/fills.js +37 -0
- package/dist/filter.d.ts +50 -0
- package/dist/filter.d.ts.map +1 -0
- package/dist/filter.js +247 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/json-filter.d.ts +12 -0
- package/dist/json-filter.d.ts.map +1 -0
- package/dist/json-filter.js +65 -0
- package/dist/listen.d.ts +10 -0
- package/dist/listen.d.ts.map +1 -0
- package/dist/listen.js +92 -0
- package/dist/logic.d.ts +7 -0
- package/dist/logic.d.ts.map +1 -0
- package/dist/logic.js +18 -0
- package/dist/migrations.d.ts +23 -0
- package/dist/migrations.d.ts.map +1 -0
- package/dist/migrations.js +217 -0
- package/dist/names.d.ts +66 -0
- package/dist/names.d.ts.map +1 -0
- package/dist/names.js +175 -0
- package/dist/notify.d.ts +19 -0
- package/dist/notify.d.ts.map +1 -0
- package/dist/notify.js +75 -0
- package/dist/numbers.d.ts +13 -0
- package/dist/numbers.d.ts.map +1 -0
- package/dist/numbers.js +49 -0
- package/dist/parts.d.ts +36 -0
- package/dist/parts.d.ts.map +1 -0
- package/dist/parts.js +99 -0
- package/dist/plan-indexes.d.ts +12 -0
- package/dist/plan-indexes.d.ts.map +1 -0
- package/dist/plan-indexes.js +49 -0
- package/dist/plan-parts.d.ts +16 -0
- package/dist/plan-parts.d.ts.map +1 -0
- package/dist/plan-parts.js +9 -0
- package/dist/plan.d.ts +6 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +278 -0
- package/dist/pool.d.ts +5 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +41 -0
- package/dist/read.d.ts +47 -0
- package/dist/read.d.ts.map +1 -0
- package/dist/read.js +227 -0
- package/dist/scratch.d.ts +22 -0
- package/dist/scratch.d.ts.map +1 -0
- package/dist/scratch.js +77 -0
- package/dist/snapshot.d.ts +66 -0
- package/dist/snapshot.d.ts.map +1 -0
- package/dist/snapshot.js +272 -0
- package/dist/write.d.ts +17 -0
- package/dist/write.d.ts.map +1 -0
- package/dist/write.js +296 -0
- package/package.json +45 -3
- package/src/adapter.ts +349 -0
- package/src/aggregate.ts +168 -0
- package/src/decode.ts +46 -0
- package/src/fills.ts +52 -0
- package/src/filter.ts +306 -0
- package/src/index.ts +20 -0
- package/src/json-filter.ts +74 -0
- package/src/listen.ts +97 -0
- package/src/logic.ts +28 -0
- package/src/migrations.ts +275 -0
- package/src/names.ts +217 -0
- package/src/notify.ts +101 -0
- package/src/numbers.ts +64 -0
- package/src/parts.ts +143 -0
- package/src/plan-indexes.ts +100 -0
- package/src/plan-parts.ts +35 -0
- package/src/plan.ts +389 -0
- package/src/pool.ts +63 -0
- package/src/read.ts +287 -0
- package/src/scratch.ts +92 -0
- package/src/snapshot.ts +390 -0
- package/src/write.ts +384 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# @ultimabase/postgres
|
|
2
|
+
|
|
3
|
+
## 0.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0e885c4: The first release: the kernel, its adapters, transports and plugins, the testing helpers and the starter.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [0e885c4]
|
|
12
|
+
- ultimabase@0.1.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ultimabase/postgres
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The PostgreSQL adapter for UltimaBase, PostgreSQL 15 or later. In development, each start syncs the tables to the models; in production, migrations written as SQL files are applied by `ultimabase migrate deploy`.
|
|
4
|
+
|
|
5
|
+
Documentation: [PostgreSQL](https://ultimabase.dev/docs/postgres).
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { LogEvent } from 'kysely';
|
|
2
|
+
import type { Adapter } from 'ultimabase';
|
|
3
|
+
export type Options = {
|
|
4
|
+
readonly url: string | undefined;
|
|
5
|
+
/** How many connections the pool opens at most (D-47). */
|
|
6
|
+
readonly connections?: number;
|
|
7
|
+
/** The milliseconds a query waits for a connection, or to open one. */
|
|
8
|
+
readonly wait?: number;
|
|
9
|
+
readonly log?: (event: LogEvent) => void;
|
|
10
|
+
/** The text search configuration of every `_search`, `'simple'` by default (D-148). */
|
|
11
|
+
readonly searchLanguage?: string;
|
|
12
|
+
};
|
|
13
|
+
/** What `postgres()` returns: an adapter that also runs raw SQL (D-137). */
|
|
14
|
+
export type Postgres = Adapter & {
|
|
15
|
+
/**
|
|
16
|
+
* Runs one statement, in the open transaction if any, and returns its
|
|
17
|
+
* rows; each `${value}` is a parameter. No rule or hook runs.
|
|
18
|
+
*/
|
|
19
|
+
readonly sql: <R extends object = Record<string, unknown>>(strings: TemplateStringsArray, ...values: readonly unknown[]) => Promise<R[]>;
|
|
20
|
+
};
|
|
21
|
+
/** Refuses a server below PostgreSQL 15 (D-50). */
|
|
22
|
+
export declare function requireVersion(version: string | undefined): void;
|
|
23
|
+
/** What a case-insensitive filter misses when `lower('É') <> 'é'` (D-140). */
|
|
24
|
+
export declare function foldWarning(ctype: string): string;
|
|
25
|
+
export declare function createAdapter(options: Options): Postgres;
|
|
26
|
+
//# sourceMappingURL=adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/adapter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACX,QAAQ,EAGR,MAAM,QAAQ,CAAA;AAKf,OAAO,KAAK,EACX,OAAO,EASP,MAAM,YAAY,CAAA;AAuBnB,MAAM,MAAM,OAAO,GAAG;IACrB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,CAAA;IAChC,0DAA0D;IAC1D,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,uEAAuE;IACvE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,IAAI,CAAA;IACxC,uFAAuF;IACvF,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAA;CAChC,CAAA;AAED,4EAA4E;AAC5E,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG;IAChC;;;OAGG;IACH,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACxD,OAAO,EAAE,oBAAoB,EAC7B,GAAG,MAAM,EAAE,SAAS,OAAO,EAAE,KACzB,OAAO,CAAC,CAAC,EAAE,CAAC,CAAA;CACjB,CAAA;AAmED,mDAAmD;AACnD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAIhE;AAED,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CA2MxD"}
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Kysely, PostgresDialect, sql } from 'kysely';
|
|
2
|
+
import { Pool } from 'pg';
|
|
3
|
+
import { CheckFailed, Refused } from 'ultimabase';
|
|
4
|
+
import { joining, scratchable } from 'ultimabase/internal';
|
|
5
|
+
import { aggregate } from './aggregate.js';
|
|
6
|
+
import { decodeRows } from './decode.js';
|
|
7
|
+
import { apply, remoteOf, reset, stored } from './migrations.js';
|
|
8
|
+
import { Catalog } from './names.js';
|
|
9
|
+
import { overflowed } from './numbers.js';
|
|
10
|
+
import { fillsOf } from './fills.js';
|
|
11
|
+
import { channelOf, listening } from './listen.js';
|
|
12
|
+
import { plan } from './plan.js';
|
|
13
|
+
import { pooled } from './pool.js';
|
|
14
|
+
import { compileCount, compileFind } from './read.js';
|
|
15
|
+
import { connecting, dropped, emptied, scratchName, trigrams, } from './scratch.js';
|
|
16
|
+
import { parseSnapshot, snapshotOf } from './snapshot.js';
|
|
17
|
+
import { compileWrite, translate } from './write.js';
|
|
18
|
+
/** What the callback a rollback ends throws, which `rollback()` swallows. */
|
|
19
|
+
const rolledBack = new Error('The transaction was rolled back.');
|
|
20
|
+
/** `key` of a statement's first row, if it returned one. */
|
|
21
|
+
function firstOf(rows, key) {
|
|
22
|
+
const [first] = rows;
|
|
23
|
+
if (typeof first !== 'object' || first === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
const value = Reflect.get(first, key);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
async function find(catalog, db, read) {
|
|
29
|
+
const { rows } = await compileFind(catalog, read).execute(db);
|
|
30
|
+
return decodeRows(catalog, read.model, firstOf(rows, 'rows'), read);
|
|
31
|
+
}
|
|
32
|
+
async function count(catalog, db, read) {
|
|
33
|
+
const { rows } = await compileCount(catalog, read).execute(db);
|
|
34
|
+
const value = firstOf(rows, 'count');
|
|
35
|
+
if (typeof value !== 'number')
|
|
36
|
+
throw new Error('The count is not a number.');
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
/** Runs a write's statements; the one that reads returns the rows. */
|
|
40
|
+
async function run(catalog, db, given, statements) {
|
|
41
|
+
let rows;
|
|
42
|
+
try {
|
|
43
|
+
for (const statement of statements) {
|
|
44
|
+
const [first] = (await statement.sql.execute(db)).rows;
|
|
45
|
+
for (let at = 0; at < statement.checks; at += 1) {
|
|
46
|
+
if (first?.[`c${at}`] !== true)
|
|
47
|
+
throw new CheckFailed(at);
|
|
48
|
+
}
|
|
49
|
+
if (first !== undefined && 'rows' in first)
|
|
50
|
+
rows = first['rows'];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw translate(catalog, overflowed(given, error));
|
|
55
|
+
}
|
|
56
|
+
const read = given.read;
|
|
57
|
+
// A `before` read of a model the write deletes nothing of reads none.
|
|
58
|
+
if (read === undefined || rows === undefined)
|
|
59
|
+
return [];
|
|
60
|
+
return decodeRows(catalog, read.model, rows, read);
|
|
61
|
+
}
|
|
62
|
+
async function write(catalog, db, given, channel) {
|
|
63
|
+
return run(catalog, db, given, compileWrite(catalog, given, channel));
|
|
64
|
+
}
|
|
65
|
+
/** Refuses a server below PostgreSQL 15 (D-50). */
|
|
66
|
+
export function requireVersion(version) {
|
|
67
|
+
if (!(Number(version) >= 150000)) {
|
|
68
|
+
throw new Refused(`UltimaBase needs PostgreSQL 15 or later.`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** What a case-insensitive filter misses when `lower('É') <> 'é'` (D-140). */
|
|
72
|
+
export function foldWarning(ctype) {
|
|
73
|
+
return `PostgreSQL folds case under its ${ctype} locale for ASCII only: a case-insensitive filter matches É and é in memory() but not here. Create the database with a UTF-8 locale, such as en_US.UTF-8.`;
|
|
74
|
+
}
|
|
75
|
+
export function createAdapter(options) {
|
|
76
|
+
const { url, connections = 10, wait = 10_000, searchLanguage } = options;
|
|
77
|
+
const remote = url ? remoteOf(url) : undefined;
|
|
78
|
+
let state;
|
|
79
|
+
/** A test file's schema, and whether a start created it. */
|
|
80
|
+
let scratch;
|
|
81
|
+
/** Each open transaction's own, for `sql` to join. */
|
|
82
|
+
const opens = new WeakMap();
|
|
83
|
+
const started = () => {
|
|
84
|
+
if (state === undefined)
|
|
85
|
+
throw new Error('postgres() is not started.');
|
|
86
|
+
return state;
|
|
87
|
+
};
|
|
88
|
+
const adapter = {
|
|
89
|
+
start: async (schema, logger, feed) => {
|
|
90
|
+
if (!url) {
|
|
91
|
+
throw new Refused('postgres() was given no url: is the variable it reads set?');
|
|
92
|
+
}
|
|
93
|
+
if (!Number.isInteger(connections) || connections < 1) {
|
|
94
|
+
throw new Refused(`postgres() takes connections as a whole number, 1 or more, not ${connections}.`);
|
|
95
|
+
}
|
|
96
|
+
const catalog = new Catalog(schema, searchLanguage);
|
|
97
|
+
// Refuses two things stored under one name before it connects.
|
|
98
|
+
const { tables } = snapshotOf(catalog);
|
|
99
|
+
const trigram = Object.values(tables).some((table) => Object.keys(table.trigrams).length > 0);
|
|
100
|
+
const extensions = scratch !== undefined && trigram ? [await trigrams(url)] : [];
|
|
101
|
+
const config = connecting(url, scratch?.name, extensions);
|
|
102
|
+
const pool = new Pool({
|
|
103
|
+
...config,
|
|
104
|
+
max: connections,
|
|
105
|
+
connectionTimeoutMillis: wait,
|
|
106
|
+
});
|
|
107
|
+
// A connection the server ends would crash the process unheard. An
|
|
108
|
+
// idle one the pool drops, and the next query opens another; one a
|
|
109
|
+
// transaction holds fails its next statement, and the pool drops it
|
|
110
|
+
// once given back.
|
|
111
|
+
pool.on('error', () => undefined);
|
|
112
|
+
pool.on('connect', (client) => {
|
|
113
|
+
client.on('error', () => undefined);
|
|
114
|
+
// Compiling a nested read costs a hundred times more than running
|
|
115
|
+
// it; queued, this runs before the client's first query (D-47).
|
|
116
|
+
client.query('set jit = off').catch(() => undefined);
|
|
117
|
+
});
|
|
118
|
+
const dialect = new PostgresDialect({
|
|
119
|
+
pool: pooled(url, pool, connections, wait),
|
|
120
|
+
});
|
|
121
|
+
const db = new Kysely({
|
|
122
|
+
dialect,
|
|
123
|
+
...(options.log === undefined ? {} : { log: options.log }),
|
|
124
|
+
});
|
|
125
|
+
try {
|
|
126
|
+
const { rows } = await sql `select current_setting('server_version_num') as version, current_schema() as schema,
|
|
127
|
+
lower('É') = 'é' as folds,
|
|
128
|
+
(select datctype from pg_database where datname = current_database()) as ctype,
|
|
129
|
+
exists (select from pg_ts_config as c join pg_namespace as n on n.oid = c.cfgnamespace where (c.cfgname = ${catalog.language} and pg_ts_config_is_visible(c.oid)) or n.nspname || '.' || c.cfgname = ${catalog.language}) as language`.execute(db);
|
|
130
|
+
const [server] = rows;
|
|
131
|
+
requireVersion(server?.version);
|
|
132
|
+
if (server?.language === false) {
|
|
133
|
+
throw new Refused(`postgres() searches in '${catalog.language}', a text search configuration this server does not have.`);
|
|
134
|
+
}
|
|
135
|
+
if (server !== undefined && !server.folds) {
|
|
136
|
+
logger.warn(foldWarning(server.ctype));
|
|
137
|
+
}
|
|
138
|
+
if (scratch !== undefined) {
|
|
139
|
+
scratch.made = true;
|
|
140
|
+
await emptied(db, scratch.name);
|
|
141
|
+
}
|
|
142
|
+
const channel = channelOf(scratch?.name ?? server?.schema ?? '');
|
|
143
|
+
state =
|
|
144
|
+
feed === undefined
|
|
145
|
+
? { db, catalog }
|
|
146
|
+
: {
|
|
147
|
+
db,
|
|
148
|
+
catalog,
|
|
149
|
+
channel,
|
|
150
|
+
listener: await listening(config, channel, feed, logger),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
await db.destroy();
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
stop: async () => {
|
|
159
|
+
const stopping = state;
|
|
160
|
+
state = undefined;
|
|
161
|
+
await stopping?.listener?.stop();
|
|
162
|
+
await stopping?.db.destroy();
|
|
163
|
+
},
|
|
164
|
+
find: async (read) => find(started().catalog, started().db, read),
|
|
165
|
+
count: async (read) => count(started().catalog, started().db, read),
|
|
166
|
+
aggregate: async (read) => aggregate(started().catalog, started().db, read),
|
|
167
|
+
write: async (given) => {
|
|
168
|
+
const { catalog, db, channel } = started();
|
|
169
|
+
const statements = compileWrite(catalog, given, channel);
|
|
170
|
+
// One statement is atomic alone: no BEGIN, no COMMIT (D-99).
|
|
171
|
+
if (statements.length <= 1)
|
|
172
|
+
return run(catalog, db, given, statements);
|
|
173
|
+
try {
|
|
174
|
+
return await db
|
|
175
|
+
.transaction()
|
|
176
|
+
.execute(async (trx) => run(catalog, trx, given, statements));
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
// A deferred foreign key checks at the COMMIT (D-99).
|
|
180
|
+
throw translate(catalog, error);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
migrations: {
|
|
184
|
+
snapshot: (schema) => snapshotOf(new Catalog(schema, searchLanguage)),
|
|
185
|
+
plan: (from, schema) => {
|
|
186
|
+
const catalog = new Catalog(schema, searchLanguage);
|
|
187
|
+
return plan(from === undefined ? undefined : parseSnapshot(from), snapshotOf(catalog), fillsOf(catalog));
|
|
188
|
+
},
|
|
189
|
+
stored: async () => stored(started().db),
|
|
190
|
+
apply: async (given) => apply(started().db, given),
|
|
191
|
+
reset: async () => reset(started().db),
|
|
192
|
+
...(remote === undefined ? {} : { remote }),
|
|
193
|
+
},
|
|
194
|
+
begin: async () => {
|
|
195
|
+
const { catalog, db, channel } = started();
|
|
196
|
+
const opened = Promise.withResolvers();
|
|
197
|
+
const ending = Promise.withResolvers();
|
|
198
|
+
// Kysely gives the connection back however its transaction ends, a
|
|
199
|
+
// failed ROLLBACK included; a COMMIT that fails rolls back.
|
|
200
|
+
const ended = db.transaction().execute(async (trx) => {
|
|
201
|
+
opened.resolve(trx);
|
|
202
|
+
if (!(await ending.promise))
|
|
203
|
+
throw rolledBack;
|
|
204
|
+
});
|
|
205
|
+
// A BEGIN that fails rejects `opened` too.
|
|
206
|
+
void ended.catch(opened.reject);
|
|
207
|
+
const trx = await opened.promise;
|
|
208
|
+
const end = async (commit) => {
|
|
209
|
+
ending.resolve(commit);
|
|
210
|
+
await ended;
|
|
211
|
+
};
|
|
212
|
+
const tx = {
|
|
213
|
+
find: async (read) => find(catalog, trx, read),
|
|
214
|
+
count: async (read) => count(catalog, trx, read),
|
|
215
|
+
aggregate: async (read) => aggregate(catalog, trx, read),
|
|
216
|
+
write: async (given) => write(catalog, trx, given, channel),
|
|
217
|
+
commit: async () => {
|
|
218
|
+
try {
|
|
219
|
+
await end(true);
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
throw translate(catalog, error);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
rollback: async () => {
|
|
226
|
+
try {
|
|
227
|
+
await end(false);
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
if (error !== rolledBack)
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
opens.set(tx, trx);
|
|
236
|
+
return tx;
|
|
237
|
+
},
|
|
238
|
+
sql: async (strings, ...values) => joining(adapter, async (tx) => {
|
|
239
|
+
const db = (tx && opens.get(tx)) ?? started().db;
|
|
240
|
+
return (await sql(strings, ...values).execute(db)).rows;
|
|
241
|
+
}),
|
|
242
|
+
};
|
|
243
|
+
return scratchable(adapter, () => {
|
|
244
|
+
scratch = { name: scratchName(), made: false };
|
|
245
|
+
const own = scratch;
|
|
246
|
+
return {
|
|
247
|
+
drop: async () => {
|
|
248
|
+
scratch = undefined;
|
|
249
|
+
if (own.made && url)
|
|
250
|
+
await dropped(url, own.name);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { QueryExecutorProvider } from 'kysely';
|
|
2
|
+
import type { AggregateRead } from 'ultimabase';
|
|
3
|
+
import type { Sql } from './filter.ts';
|
|
4
|
+
import type { Catalog } from './names.ts';
|
|
5
|
+
export declare function compileAggregate(catalog: Catalog, read: AggregateRead): Sql;
|
|
6
|
+
/** Each group's values, a date back as a `Date`. */
|
|
7
|
+
export declare function aggregate(catalog: Catalog, db: QueryExecutorProvider, read: AggregateRead): Promise<unknown[][]>;
|
|
8
|
+
//# sourceMappingURL=aggregate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"aggregate.d.ts","sourceRoot":"","sources":["../src/aggregate.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAA;AAEnD,OAAO,KAAK,EAAa,aAAa,EAAoB,MAAM,YAAY,CAAA;AAC5E,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,aAAa,CAAA;AAEtC,OAAO,KAAK,EAAE,OAAO,EAAe,MAAM,YAAY,CAAA;AAmDtD,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,aAAa,GAAG,GAAG,CA8E3E;AAED,oDAAoD;AACpD,wBAAsB,SAAS,CAC9B,OAAO,EAAE,OAAO,EAChB,EAAE,EAAE,qBAAqB,EACzB,IAAI,EAAE,aAAa,GACjB,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAqBtB"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import { all, any, isFilters } from './filter.js';
|
|
3
|
+
import { Compiler, sorted } from './read.js';
|
|
4
|
+
/** A value as jsonb keeps it: a decimal as text, a date in UTC. */
|
|
5
|
+
function shown(kind, item) {
|
|
6
|
+
if (kind === 'decimal')
|
|
7
|
+
return sql `cast(${item} as text)`;
|
|
8
|
+
return kind === 'dateTime' ? sql `(${item} at time zone 'UTC')` : item;
|
|
9
|
+
}
|
|
10
|
+
/** The aggregate of `column`, as PostgreSQL computes it. */
|
|
11
|
+
function computed({ operation, field }, column) {
|
|
12
|
+
if (field === undefined)
|
|
13
|
+
return sql `count(*)`;
|
|
14
|
+
return sql `${sql.raw(operation)}(${column})`;
|
|
15
|
+
}
|
|
16
|
+
/** The aggregate as jsonb keeps it; a sum and an average as float8. */
|
|
17
|
+
function value(kind, asked, column) {
|
|
18
|
+
const result = computed(asked, column);
|
|
19
|
+
const { operation, field } = asked;
|
|
20
|
+
if (field === undefined || operation === 'count')
|
|
21
|
+
return result;
|
|
22
|
+
if (kind === 'decimal' && operation !== 'avg')
|
|
23
|
+
return shown(kind, result);
|
|
24
|
+
if (operation === 'sum' || operation === 'avg') {
|
|
25
|
+
return sql `cast(${result} as float8)`;
|
|
26
|
+
}
|
|
27
|
+
return shown(kind, result);
|
|
28
|
+
}
|
|
29
|
+
/** The kind of a key's value: its field's, on the model `through` reaches. */
|
|
30
|
+
function kindOf(catalog, table, key) {
|
|
31
|
+
let at = table;
|
|
32
|
+
for (const step of key.through ?? []) {
|
|
33
|
+
const field = at.fields.get(step.relation);
|
|
34
|
+
if (field === undefined || field.kind === 'scalar') {
|
|
35
|
+
throw new Error(`${at.model.name}.${step.relation} is not a relation.`);
|
|
36
|
+
}
|
|
37
|
+
at = catalog.table(field.target);
|
|
38
|
+
}
|
|
39
|
+
return catalog.kind(at, key.field);
|
|
40
|
+
}
|
|
41
|
+
/** Each aggregate's kind, `undefined` for a count of rows. */
|
|
42
|
+
function kinds(catalog, table, read) {
|
|
43
|
+
return read.aggregates.map(({ field }) => field === undefined ? undefined : catalog.kind(table, field));
|
|
44
|
+
}
|
|
45
|
+
export function compileAggregate(catalog, read) {
|
|
46
|
+
const compiler = new Compiler(catalog);
|
|
47
|
+
const { filters } = compiler;
|
|
48
|
+
const alias = filters.alias();
|
|
49
|
+
const grouped = filters.alias();
|
|
50
|
+
const table = catalog.table(read.model);
|
|
51
|
+
const by = read.by ?? [];
|
|
52
|
+
const inner = [
|
|
53
|
+
...by.map((key, at) => sql `${compiler.key(table, alias, key)} as ${sql.id(`k${at}`)}`),
|
|
54
|
+
...read.aggregates.flatMap(({ field }, at) => field === undefined
|
|
55
|
+
? []
|
|
56
|
+
: [sql `${filters.column(table, alias, field)} as ${sql.id(`c${at}`)}`]),
|
|
57
|
+
];
|
|
58
|
+
const keys = by.map((_, at) => sql.id(grouped, `k${at}`));
|
|
59
|
+
const types = kinds(catalog, table, read);
|
|
60
|
+
const aggregates = read.aggregates.map((each, at) => {
|
|
61
|
+
const column = sql.id(grouped, `c${at}`);
|
|
62
|
+
return { kind: types[at], each, column };
|
|
63
|
+
});
|
|
64
|
+
const values = [
|
|
65
|
+
...by.map((key, at) => shown(kindOf(catalog, table, key), sql.id(grouped, `k${at}`))),
|
|
66
|
+
...aggregates.map(({ kind, each, column }) => value(kind, each, column)),
|
|
67
|
+
];
|
|
68
|
+
// PostgreSQL takes at most 100 arguments per call.
|
|
69
|
+
const arrays = [];
|
|
70
|
+
for (let at = 0; at < values.length; at += 100) {
|
|
71
|
+
arrays.push(sql `jsonb_build_array(${sql.join(values.slice(at, at + 100))})`);
|
|
72
|
+
}
|
|
73
|
+
const where = filters.where(read.model, alias, read.where);
|
|
74
|
+
const rows = sql `(select ${sql.join([sql `1`, ...inner])} from ${sql.id(table.name)} as ${sql.id(alias)} where ${where}) as ${sql.id(grouped)}`;
|
|
75
|
+
const group = by.length === 0 ? sql `` : sql ` group by ${sql.join(keys)}`;
|
|
76
|
+
// A key or an aggregate, by its position.
|
|
77
|
+
const at = (index) => {
|
|
78
|
+
const found = aggregates[index - by.length];
|
|
79
|
+
const key = by[index];
|
|
80
|
+
if (found === undefined) {
|
|
81
|
+
if (key === undefined)
|
|
82
|
+
throw new Error(`No value at ${index}.`);
|
|
83
|
+
const kind = kindOf(catalog, table, key);
|
|
84
|
+
return { item: sql.id(grouped, `k${index}`), kind };
|
|
85
|
+
}
|
|
86
|
+
const { operation } = found.each;
|
|
87
|
+
const kind = operation === 'min' || operation === 'max'
|
|
88
|
+
? (found.kind ?? 'number')
|
|
89
|
+
: operation === 'sum' && found.kind === 'decimal'
|
|
90
|
+
? 'decimal'
|
|
91
|
+
: 'number';
|
|
92
|
+
return { item: computed(found.each, found.column), kind };
|
|
93
|
+
};
|
|
94
|
+
const kept = (filter) => all(Object.entries(filter).map(([key, part]) => condition(key, part)));
|
|
95
|
+
function condition(key, part) {
|
|
96
|
+
if (part === undefined)
|
|
97
|
+
return sql `true`;
|
|
98
|
+
if (!isFilters(part)) {
|
|
99
|
+
const { item, kind } = at(Number(key));
|
|
100
|
+
return filters.compare(item, kind, part);
|
|
101
|
+
}
|
|
102
|
+
const list = part.map((each) => kept(each));
|
|
103
|
+
if (key === 'AND')
|
|
104
|
+
return all(list);
|
|
105
|
+
if (key === 'OR')
|
|
106
|
+
return any(list);
|
|
107
|
+
return all(list.map((each) => sql `(not ${each})`));
|
|
108
|
+
}
|
|
109
|
+
const having = read.having === undefined ? sql `` : sql ` having ${kept(read.having)}`;
|
|
110
|
+
const sorts = (read.orderBy ?? []).map((each) => sorted(at(each.at).item, each));
|
|
111
|
+
const order = sorts.length === 0 ? sql `` : sql ` order by ${sql.join(sorts)}`;
|
|
112
|
+
const limit = read.take === undefined ? sql `` : sql ` limit ${read.take}`;
|
|
113
|
+
const offset = read.skip === undefined ? sql `` : sql ` offset ${read.skip}`;
|
|
114
|
+
return sql `select ${sql.join(arrays, sql ` || `)} as "values" from ${rows}${group}${having}${order}${limit}${offset}`;
|
|
115
|
+
}
|
|
116
|
+
/** Each group's values, a date back as a `Date`. */
|
|
117
|
+
export async function aggregate(catalog, db, read) {
|
|
118
|
+
const { rows } = await compileAggregate(catalog, read).execute(db);
|
|
119
|
+
const table = catalog.table(read.model);
|
|
120
|
+
const by = read.by ?? [];
|
|
121
|
+
const types = [
|
|
122
|
+
...by.map((key) => kindOf(catalog, table, key)),
|
|
123
|
+
...kinds(catalog, table, read),
|
|
124
|
+
];
|
|
125
|
+
return rows.map((row) => {
|
|
126
|
+
const values = typeof row === 'object' && row !== null
|
|
127
|
+
? Reflect.get(row, 'values')
|
|
128
|
+
: undefined;
|
|
129
|
+
if (!Array.isArray(values))
|
|
130
|
+
throw new Error('The values are not a list.');
|
|
131
|
+
return types.map((kind, index) => {
|
|
132
|
+
const item = values[index];
|
|
133
|
+
return typeof item === 'string' && kind === 'dateTime'
|
|
134
|
+
? new Date(`${item}Z`)
|
|
135
|
+
: item;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
package/dist/decode.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { Nested, Row } from 'ultimabase';
|
|
2
|
+
import type { Catalog } from './names.ts';
|
|
3
|
+
/** The rows of a jsonb array of `model`, with the relations `nested` includes. */
|
|
4
|
+
export declare function decodeRows(catalog: Catalog, model: string, value: unknown, nested: Nested): Row[];
|
|
5
|
+
//# sourceMappingURL=decode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"decode.d.ts","sourceRoot":"","sources":["../src/decode.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAE7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AA+BzC,kFAAkF;AAClF,wBAAgB,UAAU,CACzB,OAAO,EAAE,OAAO,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,MAAM,GACZ,GAAG,EAAE,CAIP"}
|
package/dist/decode.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { isRow } from 'ultimabase/internal';
|
|
2
|
+
const dates = new Set(['createdAt', 'updatedAt']);
|
|
3
|
+
function decodeRow(catalog, model, value, nested) {
|
|
4
|
+
if (!isRow(value))
|
|
5
|
+
throw new Error(`A ${model} row has no id.`);
|
|
6
|
+
const table = catalog.table(model);
|
|
7
|
+
const row = { ...value };
|
|
8
|
+
for (const [name, item] of Object.entries(value)) {
|
|
9
|
+
const field = table.fields.get(name);
|
|
10
|
+
const inner = nested.include?.[name];
|
|
11
|
+
if (typeof item === 'string' &&
|
|
12
|
+
(dates.has(name) ||
|
|
13
|
+
(field?.kind === 'scalar' && field.type === 'dateTime'))) {
|
|
14
|
+
row[name] = new Date(`${item}Z`);
|
|
15
|
+
}
|
|
16
|
+
else if (inner !== undefined && field?.kind === 'many') {
|
|
17
|
+
row[name] = decodeRows(catalog, field.target, item, inner);
|
|
18
|
+
}
|
|
19
|
+
else if (inner !== undefined && field?.kind === 'one' && item !== null) {
|
|
20
|
+
row[name] = decodeRow(catalog, field.target, item, inner);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return row;
|
|
24
|
+
}
|
|
25
|
+
/** The rows of a jsonb array of `model`, with the relations `nested` includes. */
|
|
26
|
+
export function decodeRows(catalog, model, value, nested) {
|
|
27
|
+
if (!Array.isArray(value))
|
|
28
|
+
throw new Error(`The ${model} rows are not a list.`);
|
|
29
|
+
return value.map((item) => decodeRow(catalog, model, item, nested));
|
|
30
|
+
}
|
package/dist/fills.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Catalog } from './names.ts';
|
|
2
|
+
export declare const literal: (value: string) => string;
|
|
3
|
+
/** A fixed default as SQL, and as the log shows it: `'draft'`. */
|
|
4
|
+
export type Fill = {
|
|
5
|
+
readonly sql: string;
|
|
6
|
+
readonly shown: string;
|
|
7
|
+
};
|
|
8
|
+
/** Each table's columns with a fixed default (D-105). */
|
|
9
|
+
export type Fills = {
|
|
10
|
+
readonly [table: string]: {
|
|
11
|
+
readonly [column: string]: Fill;
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
/** The columns whose field has a fixed default, which rows take when added. */
|
|
15
|
+
export declare function fillsOf(catalog: Catalog): Fills;
|
|
16
|
+
//# sourceMappingURL=fills.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fills.d.ts","sourceRoot":"","sources":["../src/fills.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAQ,MAAM,YAAY,CAAA;AAE/C,eAAO,MAAM,OAAO,UAAW,MAAM,KAAG,MACL,CAAA;AAEnC,kEAAkE;AAClE,MAAM,MAAM,IAAI,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAA;AAEnE,yDAAyD;AACzD,MAAM,MAAM,KAAK,GAAG;IACnB,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG;QAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAA;CAC7D,CAAA;AAsBD,+EAA+E;AAC/E,wBAAgB,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,KAAK,CAe/C"}
|
package/dist/fills.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export const literal = (value) => `'${value.replaceAll("'", "''")}'`;
|
|
2
|
+
function fillOf(kind, value) {
|
|
3
|
+
if (kind.endsWith('[]') && Array.isArray(value)) {
|
|
4
|
+
const items = value.map((item) => `"${String(item).replaceAll(/[\\"]/g, '\\$&')}"`);
|
|
5
|
+
const text = literal(`{${items.join(',')}}`);
|
|
6
|
+
return { sql: `${text}::${kind}`, shown: text };
|
|
7
|
+
}
|
|
8
|
+
if (kind === 'json') {
|
|
9
|
+
const text = literal(JSON.stringify(value));
|
|
10
|
+
return { sql: `${text}::jsonb`, shown: text };
|
|
11
|
+
}
|
|
12
|
+
if (value instanceof Date) {
|
|
13
|
+
const text = literal(value.toISOString());
|
|
14
|
+
return { sql: `${text}::timestamptz`, shown: text };
|
|
15
|
+
}
|
|
16
|
+
const text = typeof value === 'string' ? literal(value) : String(value);
|
|
17
|
+
return { sql: text, shown: text };
|
|
18
|
+
}
|
|
19
|
+
/** The columns whose field has a fixed default, which rows take when added. */
|
|
20
|
+
export function fillsOf(catalog) {
|
|
21
|
+
const fills = {};
|
|
22
|
+
for (const table of catalog.tables.values()) {
|
|
23
|
+
const columns = {};
|
|
24
|
+
for (const field of table.model.fields) {
|
|
25
|
+
if (field.kind !== 'scalar')
|
|
26
|
+
continue;
|
|
27
|
+
// A list's rows hold `[]` when nothing else is given (D-120).
|
|
28
|
+
const given = field.list ? (field.default ?? []) : field.default;
|
|
29
|
+
if (given === undefined)
|
|
30
|
+
continue;
|
|
31
|
+
const kind = catalog.kind(table, field.name);
|
|
32
|
+
columns[catalog.column(table, field.name)] = fillOf(kind, given);
|
|
33
|
+
}
|
|
34
|
+
fills[table.name] = columns;
|
|
35
|
+
}
|
|
36
|
+
return fills;
|
|
37
|
+
}
|
package/dist/filter.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Condition, Filter, SchemaField } from 'ultimabase';
|
|
2
|
+
import type { Sql } from './logic.ts';
|
|
3
|
+
import type { Catalog, Kind, Table } from './names.ts';
|
|
4
|
+
export type { Sql } from './logic.ts';
|
|
5
|
+
export { all, any } from './logic.ts';
|
|
6
|
+
type Relation = Extract<SchemaField, {
|
|
7
|
+
readonly kind: 'one' | 'many';
|
|
8
|
+
}>;
|
|
9
|
+
/** The rows a relation reaches: `from`, then what ties them to the row. */
|
|
10
|
+
export type Reach = {
|
|
11
|
+
readonly from: Sql;
|
|
12
|
+
readonly on: Sql;
|
|
13
|
+
};
|
|
14
|
+
export declare const uuid: RegExp;
|
|
15
|
+
/**
|
|
16
|
+
* A value as a parameter of its column's type. A date goes in UTC: `pg`
|
|
17
|
+
* writes it in local time to the minute, and a local offset of an old date
|
|
18
|
+
* has seconds, as Paris's +00:09:21 before 1911.
|
|
19
|
+
*/
|
|
20
|
+
export declare function value(kind: Kind, item: unknown): Sql;
|
|
21
|
+
export declare function isFilters(item: Condition | readonly Filter[]): item is readonly Filter[];
|
|
22
|
+
export declare class Filters {
|
|
23
|
+
readonly catalog: Catalog;
|
|
24
|
+
/** The tables the compiled conditions reach, besides their own. */
|
|
25
|
+
readonly tables: Set<string>;
|
|
26
|
+
/** A write's change index → the CTE of its delete, for `removedBy`. */
|
|
27
|
+
readonly removed: ReadonlyMap<number, string>;
|
|
28
|
+
private next;
|
|
29
|
+
constructor(catalog: Catalog, removed?: ReadonlyMap<number, string>);
|
|
30
|
+
/** A table alias no other part of the statement uses. */
|
|
31
|
+
alias(): string;
|
|
32
|
+
column(table: Table, alias: string, name: string): Sql;
|
|
33
|
+
where(model: string, alias: string, filter: Filter | undefined): Sql;
|
|
34
|
+
/** `text` as `websearch_to_tsquery` reads it, in the catalog's language (D-148). */
|
|
35
|
+
query(text: string): Sql;
|
|
36
|
+
/** The rows of `field`'s target, aliased `to`, that `alias`'s row reaches. */
|
|
37
|
+
reach(table: Table, alias: string, field: Relation, to: string): Reach;
|
|
38
|
+
private exists;
|
|
39
|
+
private relation;
|
|
40
|
+
private scalar;
|
|
41
|
+
/** A value of `kind`, a column or an aggregate (D-149), meets `condition`. */
|
|
42
|
+
compare(column: Sql, kind: Kind, condition: Condition): Sql;
|
|
43
|
+
/** A string that is not an id equals no id, as in `memory()` (D-98). */
|
|
44
|
+
private equal;
|
|
45
|
+
/** `in`: no item is false; an item no known value equals is dropped. */
|
|
46
|
+
private member;
|
|
47
|
+
/** `in` ignoring case: one `ilike` per item (D-140). */
|
|
48
|
+
private like;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=filter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter.d.ts","sourceRoot":"","sources":["../src/filter.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,YAAY,CAAA;AAEhE,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAEtD,YAAY,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AACrC,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA;AAErC,KAAK,QAAQ,GAAG,OAAO,CAAC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,CAAA;CAAE,CAAC,CAAA;AAEvE,2EAA2E;AAC3E,MAAM,MAAM,KAAK,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC;IAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAA;CAAE,CAAA;AAE5D,eAAO,MAAM,IAAI,EAAE,MAC8C,CAAA;AAEjE;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,GAAG,GAAG,CAQpD;AAED,wBAAgB,SAAS,CACxB,IAAI,EAAE,SAAS,GAAG,SAAS,MAAM,EAAE,GACjC,IAAI,IAAI,SAAS,MAAM,EAAE,CAE3B;AAED,qBAAa,OAAO;IACnB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAY;IACxC,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC7C,OAAO,CAAC,IAAI,CAAI;IAEhB,YACC,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,WAAW,CAAC,MAAM,EAAE,MAAM,CAAa,EAIhD;IAED,yDAAyD;IACzD,KAAK,IAAI,MAAM,CAGd;IAED,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,GAAG,CAErD;IAED,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,GAAG,CA6BnE;IAED,oFAAoF;IACpF,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,GAAG,CAEvB;IAED,8EAA8E;IAC9E,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,GAAG,KAAK,CAyBrE;IAED,OAAO,CAAC,MAAM;IAad,OAAO,CAAC,QAAQ;IA4ChB,OAAO,CAAC,MAAM;IAUd,8EAA8E;IAC9E,OAAO,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,GAAG,GAAG,CAuE1D;IAED,wEAAwE;IACxE,OAAO,CAAC,KAAK;IAOb,wEAAwE;IACxE,OAAO,CAAC,MAAM;IAed,wDAAwD;IACxD,OAAO,CAAC,IAAI;CAQZ"}
|