typion 0.0.0-stage → 0.1.1

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -2
  3. package/bin/typion.js +7 -0
  4. package/dist/auth/better-auth.d.ts +13 -0
  5. package/dist/auth/better-auth.js +11 -0
  6. package/dist/auth/better-auth.js.map +1 -0
  7. package/dist/cli/index.d.ts +14 -0
  8. package/dist/cli/index.js +883 -0
  9. package/dist/cli/index.js.map +1 -0
  10. package/dist/column-kinds-Bkm4NJI2.js +47 -0
  11. package/dist/column-kinds-Bkm4NJI2.js.map +1 -0
  12. package/dist/context-B4e2eREP.d.ts +2100 -0
  13. package/dist/contract/index.d.ts +137 -0
  14. package/dist/contract/index.js +42 -0
  15. package/dist/contract/index.js.map +1 -0
  16. package/dist/db/pglite.d.ts +12 -0
  17. package/dist/db/pglite.js +27 -0
  18. package/dist/db/pglite.js.map +1 -0
  19. package/dist/descriptor-Cc3mGp73.js +12 -0
  20. package/dist/descriptor-Cc3mGp73.js.map +1 -0
  21. package/dist/errors-ChDBxeS-.js +17 -0
  22. package/dist/errors-ChDBxeS-.js.map +1 -0
  23. package/dist/errors-D-CKO-px.js +114 -0
  24. package/dist/errors-D-CKO-px.js.map +1 -0
  25. package/dist/handler-BWVU8TCR.d.ts +23 -0
  26. package/dist/handler-_87bFkbP.js +565 -0
  27. package/dist/handler-_87bFkbP.js.map +1 -0
  28. package/dist/hono/index.d.ts +17 -0
  29. package/dist/hono/index.js +26 -0
  30. package/dist/hono/index.js.map +1 -0
  31. package/dist/http/index.d.ts +13 -0
  32. package/dist/http/index.js +2 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.js +1324 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/issues-DLqCBQT1.js +42 -0
  37. package/dist/issues-DLqCBQT1.js.map +1 -0
  38. package/dist/next/index.d.ts +8 -0
  39. package/dist/next/index.js +18 -0
  40. package/dist/next/index.js.map +1 -0
  41. package/dist/operations-D3rOacLX.js +1811 -0
  42. package/dist/operations-D3rOacLX.js.map +1 -0
  43. package/dist/schema/index.d.ts +18 -0
  44. package/dist/schema/index.js +3 -0
  45. package/dist/schema-Rzzq97cK.js +68 -0
  46. package/dist/schema-Rzzq97cK.js.map +1 -0
  47. package/dist/seed/index.d.ts +19 -0
  48. package/dist/seed/index.js +85 -0
  49. package/dist/seed/index.js.map +1 -0
  50. package/dist/structural-BZnjFmCs.js +247 -0
  51. package/dist/structural-BZnjFmCs.js.map +1 -0
  52. package/dist/testing/index.d.ts +26 -0
  53. package/dist/testing/index.js +92 -0
  54. package/dist/testing/index.js.map +1 -0
  55. package/dist/typion-Dh2BCSRz.js +1187 -0
  56. package/dist/typion-Dh2BCSRz.js.map +1 -0
  57. package/dist/validation/zod.d.ts +21 -0
  58. package/dist/validation/zod.js +84 -0
  59. package/dist/validation/zod.js.map +1 -0
  60. package/docs/README.md +145 -0
  61. package/docs/access.md +114 -0
  62. package/docs/auth.md +195 -0
  63. package/docs/collections.md +172 -0
  64. package/docs/database.md +76 -0
  65. package/docs/features/localization.md +112 -0
  66. package/docs/features/multi-tenant.md +166 -0
  67. package/docs/features/soft-delete.md +85 -0
  68. package/docs/features/versions.md +106 -0
  69. package/docs/hooks.md +106 -0
  70. package/docs/http.md +188 -0
  71. package/docs/queries.md +200 -0
  72. package/docs/testing.md +85 -0
  73. package/package.json +125 -3
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/seed/index.ts"],"sourcesContent":["import { is, sql } from 'drizzle-orm'\nimport {\n getTableConfig,\n type PgAsyncDatabase,\n type PgQueryResultHKT,\n PgTable,\n} from 'drizzle-orm/pg-core'\nimport { ConfigError } from '../errors.js'\nimport type { TypionConfig } from '../types/collection.js'\nimport type { EntityMeta } from '../types/meta.js'\nimport type { SeedColumnsInput, SeedGenerators } from '../types/plugin.js'\n\n// A thin wrapper over drizzle-seed. What it adds is what drizzle-seed cannot know: which tables belong to the\n// engine rather than to the application (the tables the plugins own or derive, the migrations),\n// and what the installed plugins want their own columns to hold. Nothing here knows what a draft or a tenant\n// is: a plugin that is not installed contributes nothing, and the rows are drizzle-seed's own.\n\ntype Db = PgAsyncDatabase<PgQueryResultHKT, any>\n\ntype SeedModule = {\n seed: (\n db: Db,\n schema: Record<string, PgTable>,\n options?: { count?: number; seed?: number },\n ) => {\n refine(callback: (funcs: SeedGenerators) => Record<string, unknown>): Promise<void>\n } & Promise<void>\n}\n\nexport type CollectionSeed = (\n generators: SeedGenerators,\n entity: EntityMeta,\n) => Record<string, unknown>\n\nexport type SeedOptions = {\n // Rows per collection.\n count?: number\n // Same number, same rows.\n seed?: number\n // By default every collection of the config except the ones a plugin owns.\n collections?: readonly string[]\n // Values the plugins read (the tenant of multi-tenancy, say).\n values?: Readonly<Record<string, unknown>>\n // The last word: drizzle-seed refinements, merged over everything else.\n refine?: (generators: SeedGenerators) => Record<string, unknown>\n db?: Db\n}\n\n// The build of drizzle-seed that runs on the Drizzle the engine is built on: its `latest` is for Drizzle 0.x, and its\n// release candidate wants a newer Drizzle.\nexport const DRIZZLE_SEED = '1.0.0-rc.5-ab785fc'\n\nasync function load(): Promise<SeedModule> {\n try {\n return (await import('drizzle-seed')) as unknown as SeedModule\n } catch (error) {\n throw new ConfigError(\n `Seeding needs drizzle-seed@${DRIZZLE_SEED} (\\`pnpm add -D drizzle-seed@${DRIZZLE_SEED}\\`)`,\n 'config',\n { cause: error },\n )\n }\n}\n\nfunction truncate(db: Db, tables: readonly PgTable[]): Promise<unknown> | undefined {\n if (tables.length === 0) return undefined\n const names = tables.map((table) => {\n const info = getTableConfig(table)\n return `\"${info.schema ?? 'public'}\".\"${info.name}\"`\n })\n return db.execute(sql.raw(`truncate table ${names.join(', ')} cascade`))\n}\n\nfunction connect(config: TypionConfig<any>, given?: Db): Db {\n if (given !== undefined) return given\n if (config.db === undefined) {\n throw new ConfigError('The config has no database adapter (`db` in defineConfig)')\n }\n return config.db.connect(config.definition.relations)\n}\n\nfunction collectionsOf(config: TypionConfig<any>, names?: readonly string[]): EntityMeta[] {\n const entities = Object.values(config.definition.entities) as EntityMeta[]\n const own = entities.filter((entity) => entity.owner === 'user')\n if (names === undefined) return own\n return names.map((name) => {\n const entity = own.find((candidate) => candidate.name === name)\n if (entity === undefined) throw new ConfigError(`'${name}' is not a collection of this config`)\n return entity\n })\n}\n\n// Plugins first, then what the collection says about itself, then what the caller says.\nfunction columnsFor(\n config: TypionConfig<any>,\n entity: EntityMeta,\n input: SeedColumnsInput,\n): Record<string, unknown> {\n const columns: Record<string, unknown> = {}\n for (const plugin of config.definition.plugins) {\n Object.assign(columns, plugin.spec.seed?.columns?.(entity, input))\n }\n const generators = input.generators\n const own = entity.pluginOptions.seed as CollectionSeed | undefined\n if (typeof own === 'function') Object.assign(columns, own(generators, entity))\n return columns\n}\n\nexport async function seedCollections(\n config: TypionConfig<any>,\n options: SeedOptions = {},\n): Promise<void> {\n const { seed } = await load()\n const defaults = config.seed ?? {}\n const db = connect(config, options.db)\n const entities = collectionsOf(config, options.collections ?? defaults.collections)\n const schema: Record<string, PgTable> = {}\n for (const entity of entities) schema[entity.name] = entity.table\n const values = { ...defaults.values, ...options.values }\n // drizzle-seed's own default, which the plugins need to know to give every row a distinct value.\n const count = options.count ?? defaults.count ?? 10\n const refine = options.refine ?? defaults.refine\n\n await seed(db, schema, { count, seed: options.seed ?? defaults.seed }).refine((generators) => {\n const refinements: Record<string, unknown> = {}\n for (const entity of entities) {\n const columns = columnsFor(config, entity, { generators, values, count })\n if (Object.keys(columns).length > 0) refinements[entity.name] = { columns }\n }\n return { ...refinements, ...refine?.(generators) }\n })\n}\n\n// Empties the collections (and, through their foreign keys, what the plugins derived from them).\nexport async function resetCollections(\n config: TypionConfig<any>,\n options: Pick<SeedOptions, 'collections' | 'db'> = {},\n): Promise<void> {\n const db = connect(config, options.db)\n await truncate(\n db,\n collectionsOf(config, options.collections).map((entity) => entity.table),\n )\n}\n\n// Every table of the config — the collections, their history, the plugins' tables, the config's `tables` — as\n// `typion db push` made them: starting again from a seed needs the auth library's users and the audit trail gone too.\nexport async function resetTables(\n config: TypionConfig<any>,\n options: Pick<SeedOptions, 'db'> = {},\n): Promise<void> {\n const tables = Object.values(config.schema).filter((value): value is PgTable =>\n is(value, PgTable),\n )\n await truncate(connect(config, options.db), tables)\n}\n"],"mappings":"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"}
@@ -0,0 +1,247 @@
1
+ import { n as ConfigError } from "./errors-D-CKO-px.js";
2
+ import { a as prefixIssues, n as isStandardSchema, o as runStandard, r as issueAt, t as isPromiseLike } from "./issues-DLqCBQT1.js";
3
+ //#region src/validation/structural.ts
4
+ const VENDOR = "typion";
5
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
6
+ const INTEGER_CONSTRAINTS = /* @__PURE__ */ new Set([
7
+ "int8",
8
+ "uint8",
9
+ "int16",
10
+ "uint16",
11
+ "int24",
12
+ "uint24",
13
+ "int32",
14
+ "uint32",
15
+ "int53",
16
+ "uint53",
17
+ "year"
18
+ ]);
19
+ function runtimeOf(column) {
20
+ return column.column;
21
+ }
22
+ function isPlainObject(value) {
23
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
24
+ const proto = Object.getPrototypeOf(value);
25
+ return proto === Object.prototype || proto === null;
26
+ }
27
+ function isJsonValue(value) {
28
+ const type = typeof value;
29
+ return type === "string" || type === "number" || type === "boolean" || type === "object";
30
+ }
31
+ function then(value, fn) {
32
+ return isPromiseLike(value) ? value.then(fn) : fn(value);
33
+ }
34
+ function typeMismatch(dataType, value, runtime, enumValues) {
35
+ const [type, constraint] = dataType.split(" ");
36
+ switch (type) {
37
+ case "string": {
38
+ if (typeof value !== "string") return "Expected a string";
39
+ if (constraint === "uuid") return UUID_RE.test(value) ? null : "Expected a uuid";
40
+ if (constraint === "enum") return enumValues?.includes(value) ? null : `Expected one of ${(enumValues ?? []).map((v) => `'${v}'`).join(", ")}`;
41
+ if (constraint === "int64" || constraint === "uint64") return (constraint === "int64" ? /^-?\d+$/ : /^\d+$/).test(value) ? null : "Expected an integer string";
42
+ if (constraint === "binary" && !/^[01]*$/.test(value)) return "Expected a bit string";
43
+ const { length, isLengthExact } = runtime;
44
+ if (length !== void 0) {
45
+ if (isLengthExact && value.length !== length) return `Expected exactly ${length} characters`;
46
+ if (!isLengthExact && value.length > length) return `Expected at most ${length} characters`;
47
+ }
48
+ return null;
49
+ }
50
+ case "number":
51
+ if (typeof value !== "number" || !Number.isFinite(value)) return "Expected a number";
52
+ if (constraint !== void 0 && INTEGER_CONSTRAINTS.has(constraint) && !Number.isInteger(value)) return "Expected an integer";
53
+ return null;
54
+ case "bigint": return typeof value === "bigint" ? null : "Expected a bigint";
55
+ case "boolean": return typeof value === "boolean" ? null : "Expected a boolean";
56
+ case "object":
57
+ if (constraint === "date") return value instanceof Date && !Number.isNaN(value.getTime()) ? null : "Expected a Date";
58
+ if (constraint === "buffer") return value instanceof Uint8Array ? null : "Expected a buffer";
59
+ if (constraint === "json") return isJsonValue(value) ? null : "Expected a JSON value";
60
+ return typeof value === "object" && value !== null ? null : "Expected an object";
61
+ case "array":
62
+ if (!Array.isArray(value)) return "Expected an array";
63
+ if (constraint === "basecolumn" && runtime.baseColumn !== void 0) {
64
+ const base = runtime.baseColumn;
65
+ for (const item of value) {
66
+ const message = typeMismatch(base.dataType, item, base, base.enumValues);
67
+ if (message !== null) return `Array item: ${message.toLowerCase()}`;
68
+ }
69
+ }
70
+ return null;
71
+ default: return null;
72
+ }
73
+ }
74
+ function checkDimensions(column, runtime, value, depth) {
75
+ if (depth === 0) return typeMismatch(column.dataType, value, runtime, column.enumValues);
76
+ if (!Array.isArray(value)) return "Expected an array";
77
+ for (const item of value) {
78
+ const message = checkDimensions(column, runtime, item, depth - 1);
79
+ if (message !== null) return message;
80
+ }
81
+ return null;
82
+ }
83
+ function scalarChecker(column, refinement) {
84
+ const runtime = runtimeOf(column);
85
+ const dimensions = runtime.dimensions ?? 0;
86
+ return (value) => {
87
+ const message = checkDimensions(column, runtime, value, dimensions);
88
+ if (message !== null) return { issues: [issueAt(message)] };
89
+ return refinement === void 0 ? { value } : runStandard(refinement, value);
90
+ };
91
+ }
92
+ function valueChecker(value) {
93
+ if (value === void 0) return (item) => isJsonValue(item) ? null : "Expected a JSON value";
94
+ const column = {
95
+ dataType: value.dataType,
96
+ enumValues: value.enumValues,
97
+ column: value
98
+ };
99
+ const runtime = runtimeOf(column);
100
+ const dimensions = runtime.dimensions ?? 0;
101
+ return (item) => checkDimensions(column, runtime, item, dimensions);
102
+ }
103
+ function capitalized(word) {
104
+ return word.charAt(0).toUpperCase() + word.slice(1);
105
+ }
106
+ function keyedChecker(shape, perKey, whole) {
107
+ const known = new Set(shape.keys);
108
+ const required = new Set(shape.required);
109
+ const nonNull = new Set(shape.nonNull);
110
+ const noun = shape.keyName;
111
+ const check = valueChecker(shape.value);
112
+ return (value) => {
113
+ if (!isPlainObject(value)) return { issues: [issueAt(`Expected an object keyed by ${noun}`)] };
114
+ const issues = [];
115
+ const output = {};
116
+ const pending = [];
117
+ for (const key of Object.keys(value)) if (!known.has(key)) issues.push(issueAt(`Unknown ${noun} '${key}'`, [key]));
118
+ for (const key of required) if (value[key] === void 0 || value[key] === null) issues.push(issueAt(`${capitalized(noun)} '${key}' is required`, [key]));
119
+ for (const key of shape.keys) {
120
+ const item = value[key];
121
+ if (item === void 0) continue;
122
+ if (item === null) {
123
+ if (nonNull.has(key)) continue;
124
+ output[key] = null;
125
+ continue;
126
+ }
127
+ const mismatch = check(item);
128
+ if (mismatch !== null) {
129
+ issues.push(issueAt(mismatch, [key]));
130
+ continue;
131
+ }
132
+ const refinement = perKey[key];
133
+ if (refinement === void 0) {
134
+ output[key] = item;
135
+ continue;
136
+ }
137
+ const result = then(runStandard(refinement, item), (r) => {
138
+ if (r.issues === void 0) output[key] = r.value;
139
+ else issues.push(...prefixIssues(r.issues, [key]));
140
+ });
141
+ if (isPromiseLike(result)) pending.push(result);
142
+ }
143
+ const finish = () => {
144
+ if (issues.length > 0) return { issues };
145
+ return whole === void 0 ? { value: output } : runStandard(whole, output);
146
+ };
147
+ return pending.length === 0 ? finish() : Promise.all(pending).then(finish);
148
+ };
149
+ }
150
+ function isRequired(column, mode) {
151
+ if (mode === "select") return true;
152
+ if (mode === "update") return false;
153
+ return column.notNull && !column.hasDefault && !column.generated;
154
+ }
155
+ function isGeneratedAlways(column) {
156
+ const runtime = runtimeOf(column);
157
+ return runtime.generated?.type === "always" || runtime.generatedIdentity?.type === "always";
158
+ }
159
+ function fieldSchema(key, required, nullable, keyed, check) {
160
+ return {
161
+ key,
162
+ required,
163
+ nullable,
164
+ keyed,
165
+ "~standard": {
166
+ version: 1,
167
+ vendor: VENDOR,
168
+ validate: (value) => {
169
+ if (value === void 0) return required ? { issues: [issueAt("Required")] } : { value: void 0 };
170
+ if (value === null) return nullable ? { value: null } : { issues: [issueAt("Expected a non-null value")] };
171
+ return check(value);
172
+ }
173
+ }
174
+ };
175
+ }
176
+ function fail(entity, message) {
177
+ throw new ConfigError(`Entity '${entity}': validate.fields ${message}`, "config", { entity });
178
+ }
179
+ function buildField(entity, column, mode, refinement) {
180
+ const shape = column.columnKind?.validation?.(column, mode);
181
+ const required = isRequired(column, mode);
182
+ const nullable = !column.notNull;
183
+ if (shape === void 0) {
184
+ if (refinement !== void 0 && !isStandardSchema(refinement)) fail(entity.name, `.${column.key} must be a Standard Schema`);
185
+ return fieldSchema(column.key, required, nullable, false, scalarChecker(column, refinement));
186
+ }
187
+ let whole;
188
+ let perKey = {};
189
+ if (isStandardSchema(refinement)) whole = refinement;
190
+ else if (refinement !== void 0) {
191
+ if (!isPlainObject(refinement)) fail(entity.name, `.${column.key} must be a Standard Schema or a per-${shape.keyName} record of them`);
192
+ for (const [key, schema] of Object.entries(refinement)) {
193
+ if (!shape.keys.includes(key)) fail(entity.name, `.${column.key}.${key} is not a configured ${shape.keyName}`);
194
+ if (schema !== void 0 && !isStandardSchema(schema)) fail(entity.name, `.${column.key}.${key} must be a Standard Schema`);
195
+ }
196
+ perKey = refinement;
197
+ }
198
+ return fieldSchema(column.key, required, nullable, true, keyedChecker(shape, perKey, whole));
199
+ }
200
+ function schemaKeys(entity, mode) {
201
+ if (mode === "select") return [...entity.visibleKeys];
202
+ return Object.keys(entity.columns).filter((key) => !isGeneratedAlways(entity.columns[key]));
203
+ }
204
+ function structuralSchema(entity, mode, refinements) {
205
+ for (const key of Object.keys(refinements ?? {})) if (!(key in entity.columns)) fail(entity.name, `.${key} does not name a column`);
206
+ const fields = {};
207
+ for (const key of schemaKeys(entity, mode)) {
208
+ const column = entity.columns[key];
209
+ fields[key] = buildField(entity, column, mode, refinements?.[key]);
210
+ }
211
+ const validate = (input) => {
212
+ if (!isPlainObject(input)) return { issues: [issueAt("Expected an object")] };
213
+ const issues = [];
214
+ const output = {};
215
+ const pending = [];
216
+ for (const key of Object.keys(input)) if (!(key in fields)) issues.push(issueAt(`Unknown key '${key}'`, [key]));
217
+ for (const [key, field] of Object.entries(fields)) {
218
+ const result = then(field["~standard"].validate(input[key]), (r) => {
219
+ if (r.issues !== void 0) issues.push(...prefixIssues(r.issues, [key]));
220
+ else if (r.value !== void 0) output[key] = r.value;
221
+ });
222
+ if (isPromiseLike(result)) pending.push(result);
223
+ }
224
+ const finish = () => issues.length > 0 ? { issues } : { value: output };
225
+ return pending.length === 0 ? finish() : Promise.all(pending).then(finish);
226
+ };
227
+ return {
228
+ entity: entity.name,
229
+ mode,
230
+ fields,
231
+ "~standard": {
232
+ version: 1,
233
+ vendor: VENDOR,
234
+ validate
235
+ }
236
+ };
237
+ }
238
+ function structuralValidation() {
239
+ return {
240
+ id: "structural",
241
+ base: (entity, mode, refinements) => structuralSchema(entity, mode, refinements)
242
+ };
243
+ }
244
+ //#endregion
245
+ export { structuralValidation as n, schemaKeys as t };
246
+
247
+ //# sourceMappingURL=structural-BZnjFmCs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structural-BZnjFmCs.js","names":[],"sources":["../src/validation/structural.ts"],"sourcesContent":["import type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { AnyPgColumn } from 'drizzle-orm/pg-core'\nimport { ConfigError } from '../errors.js'\nimport type { KeyedShape, ValidationMode } from '../types/kinds.js'\nimport type { ColumnMeta, EntityMeta } from '../types/meta.js'\nimport type { ValidationAdapter } from '../types/runtime.js'\nimport { isPromiseLike, isStandardSchema, issueAt, prefixIssues, runStandard } from './issues.js'\n\nexport type { ValidationMode }\n\nexport type StructuralFieldRefinement =\n StandardSchemaV1 | Readonly<Record<string, StandardSchemaV1 | undefined>>\n\nexport type StructuralRefinements = Readonly<Record<string, StructuralFieldRefinement | undefined>>\n\nexport interface StructuralFieldSchema extends StandardSchemaV1 {\n readonly key: string\n readonly required: boolean\n readonly nullable: boolean\n // The column's kind stores an object keyed per value (a localized column: one key per locale).\n readonly keyed: boolean\n}\n\nexport interface StructuralSchema extends StandardSchemaV1<\n Record<string, unknown>,\n Record<string, unknown>\n> {\n readonly entity: string\n readonly mode: ValidationMode\n readonly fields: Readonly<Record<string, StructuralFieldSchema>>\n}\n\nexport interface StructuralValidationAdapter extends ValidationAdapter {\n readonly id: 'structural'\n base(\n entity: EntityMeta,\n mode: ValidationMode,\n refinements: Record<string, unknown> | undefined,\n ): StructuralSchema\n}\n\ntype Result = StandardSchemaV1.Result<unknown>\ntype MaybeAsync<T> = T | Promise<T>\ntype Checker = (value: unknown) => MaybeAsync<Result>\n\nconst VENDOR = 'typion'\nconst UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i\nconst INTEGER_CONSTRAINTS = new Set([\n 'int8',\n 'uint8',\n 'int16',\n 'uint16',\n 'int24',\n 'uint24',\n 'int32',\n 'uint32',\n 'int53',\n 'uint53',\n 'year',\n])\n\n// Runtime column fields the type layer does not surface on `AnyPgColumn` (arrays carry their base column\n// and dimension count; strings their length constraint).\ntype ColumnRuntime = {\n dimensions?: number\n baseColumn?: ColumnRuntime & { dataType: string; enumValues?: readonly string[] | undefined }\n length?: number\n isLengthExact?: boolean\n generated?: { type?: string } | undefined\n generatedIdentity?: { type?: string } | undefined\n}\n\nfunction runtimeOf(column: ColumnMeta): ColumnRuntime {\n return column.column as unknown as ColumnRuntime\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n const proto = Object.getPrototypeOf(value)\n return proto === Object.prototype || proto === null\n}\n\n// JSON has no undefined, functions, symbols or bigints; null is handled by the nullability rules.\nfunction isJsonValue(value: unknown): boolean {\n const type = typeof value\n return type === 'string' || type === 'number' || type === 'boolean' || type === 'object'\n}\n\nfunction then<T, R>(value: MaybeAsync<T>, fn: (v: T) => MaybeAsync<R>): MaybeAsync<R> {\n return isPromiseLike(value) ? value.then(fn) : fn(value)\n}\n\n// Returns the failure message for a value that does not fit the column's storage type, or null.\nfunction typeMismatch(\n dataType: string,\n value: unknown,\n runtime: ColumnRuntime,\n enumValues: readonly string[] | undefined,\n): string | null {\n const [type, constraint] = dataType.split(' ')\n switch (type) {\n case 'string': {\n if (typeof value !== 'string') return 'Expected a string'\n if (constraint === 'uuid') return UUID_RE.test(value) ? null : 'Expected a uuid'\n if (constraint === 'enum') {\n return enumValues?.includes(value)\n ? null\n : `Expected one of ${(enumValues ?? []).map((v) => `'${v}'`).join(', ')}`\n }\n if (constraint === 'int64' || constraint === 'uint64') {\n return (constraint === 'int64' ? /^-?\\d+$/ : /^\\d+$/).test(value)\n ? null\n : 'Expected an integer string'\n }\n if (constraint === 'binary' && !/^[01]*$/.test(value)) return 'Expected a bit string'\n const { length, isLengthExact } = runtime\n if (length !== undefined) {\n if (isLengthExact && value.length !== length) return `Expected exactly ${length} characters`\n if (!isLengthExact && value.length > length) return `Expected at most ${length} characters`\n }\n return null\n }\n case 'number': {\n if (typeof value !== 'number' || !Number.isFinite(value)) return 'Expected a number'\n if (\n constraint !== undefined &&\n INTEGER_CONSTRAINTS.has(constraint) &&\n !Number.isInteger(value)\n ) {\n return 'Expected an integer'\n }\n return null\n }\n case 'bigint':\n return typeof value === 'bigint' ? null : 'Expected a bigint'\n case 'boolean':\n return typeof value === 'boolean' ? null : 'Expected a boolean'\n case 'object': {\n if (constraint === 'date') {\n return value instanceof Date && !Number.isNaN(value.getTime()) ? null : 'Expected a Date'\n }\n if (constraint === 'buffer') return value instanceof Uint8Array ? null : 'Expected a buffer'\n if (constraint === 'json') return isJsonValue(value) ? null : 'Expected a JSON value'\n return typeof value === 'object' && value !== null ? null : 'Expected an object'\n }\n case 'array': {\n if (!Array.isArray(value)) return 'Expected an array'\n if (constraint === 'basecolumn' && runtime.baseColumn !== undefined) {\n const base = runtime.baseColumn\n for (const item of value) {\n const message = typeMismatch(base.dataType, item, base, base.enumValues)\n if (message !== null) return `Array item: ${message.toLowerCase()}`\n }\n }\n return null\n }\n default:\n return null\n }\n}\n\n// pg arrays keep the element's dataType and count the nesting in `dimensions`.\nfunction checkDimensions(\n column: ColumnMeta,\n runtime: ColumnRuntime,\n value: unknown,\n depth: number,\n): string | null {\n if (depth === 0) return typeMismatch(column.dataType, value, runtime, column.enumValues)\n if (!Array.isArray(value)) return 'Expected an array'\n for (const item of value) {\n const message = checkDimensions(column, runtime, item, depth - 1)\n if (message !== null) return message\n }\n return null\n}\n\nfunction scalarChecker(column: ColumnMeta, refinement: StandardSchemaV1 | undefined): Checker {\n const runtime = runtimeOf(column)\n const dimensions = runtime.dimensions ?? 0\n return (value) => {\n const message = checkDimensions(column, runtime, value, dimensions)\n if (message !== null) return { issues: [issueAt(message)] }\n return refinement === undefined ? { value } : runStandard(refinement, value)\n }\n}\n\n// A keyed kind's values are checked as the column it names, with the same messages.\nfunction valueChecker(value: AnyPgColumn | undefined): (item: unknown) => string | null {\n if (value === undefined) return (item) => (isJsonValue(item) ? null : 'Expected a JSON value')\n const column = {\n dataType: value.dataType,\n enumValues: value.enumValues,\n column: value,\n } as unknown as ColumnMeta\n const runtime = runtimeOf(column)\n const dimensions = runtime.dimensions ?? 0\n return (item) => checkDimensions(column, runtime, item, dimensions)\n}\n\nfunction capitalized(word: string): string {\n return word.charAt(0).toUpperCase() + word.slice(1)\n}\n\nfunction keyedChecker(\n shape: KeyedShape,\n perKey: Readonly<Record<string, StandardSchemaV1 | undefined>>,\n whole: StandardSchemaV1 | undefined,\n): Checker {\n const known = new Set<string>(shape.keys)\n const required = new Set<string>(shape.required)\n const nonNull = new Set<string>(shape.nonNull)\n const noun = shape.keyName\n const check = valueChecker(shape.value)\n return (value) => {\n if (!isPlainObject(value)) return { issues: [issueAt(`Expected an object keyed by ${noun}`)] }\n const issues: StandardSchemaV1.Issue[] = []\n const output: Record<string, unknown> = {}\n const pending: Promise<void>[] = []\n for (const key of Object.keys(value)) {\n if (!known.has(key)) issues.push(issueAt(`Unknown ${noun} '${key}'`, [key]))\n }\n for (const key of required) {\n if (value[key] === undefined || value[key] === null) {\n issues.push(issueAt(`${capitalized(noun)} '${key}' is required`, [key]))\n }\n }\n for (const key of shape.keys) {\n const item = value[key]\n if (item === undefined) continue\n if (item === null) {\n if (nonNull.has(key)) continue\n output[key] = null\n continue\n }\n const mismatch = check(item)\n if (mismatch !== null) {\n issues.push(issueAt(mismatch, [key]))\n continue\n }\n const refinement = perKey[key]\n if (refinement === undefined) {\n output[key] = item\n continue\n }\n const result = then(runStandard(refinement, item), (r) => {\n if (r.issues === undefined) output[key] = r.value\n else issues.push(...prefixIssues(r.issues, [key]))\n })\n if (isPromiseLike(result)) pending.push(result)\n }\n const finish = (): MaybeAsync<Result> => {\n if (issues.length > 0) return { issues }\n return whole === undefined ? { value: output } : runStandard(whole, output)\n }\n return pending.length === 0 ? finish() : Promise.all(pending).then(finish)\n }\n}\n\nfunction isRequired(column: ColumnMeta, mode: ValidationMode): boolean {\n if (mode === 'select') return true\n if (mode === 'update') return false\n return column.notNull && !column.hasDefault && !column.generated\n}\n\nfunction isGeneratedAlways(column: ColumnMeta): boolean {\n const runtime = runtimeOf(column)\n return runtime.generated?.type === 'always' || runtime.generatedIdentity?.type === 'always'\n}\n\nfunction fieldSchema(\n key: string,\n required: boolean,\n nullable: boolean,\n keyed: boolean,\n check: Checker,\n): StructuralFieldSchema {\n return {\n key,\n required,\n nullable,\n keyed,\n '~standard': {\n version: 1,\n vendor: VENDOR,\n validate: (value) => {\n if (value === undefined) {\n return required ? { issues: [issueAt('Required')] } : { value: undefined }\n }\n if (value === null) {\n return nullable ? { value: null } : { issues: [issueAt('Expected a non-null value')] }\n }\n return check(value)\n },\n },\n }\n}\n\nfunction fail(entity: string, message: string): never {\n throw new ConfigError(`Entity '${entity}': validate.fields ${message}`, 'config', { entity })\n}\n\nfunction buildField(\n entity: EntityMeta,\n column: ColumnMeta,\n mode: ValidationMode,\n refinement: unknown,\n): StructuralFieldSchema {\n const shape = column.columnKind?.validation?.(column, mode)\n const required = isRequired(column, mode)\n const nullable = !column.notNull\n if (shape === undefined) {\n if (refinement !== undefined && !isStandardSchema(refinement)) {\n fail(entity.name, `.${column.key} must be a Standard Schema`)\n }\n return fieldSchema(column.key, required, nullable, false, scalarChecker(column, refinement))\n }\n let whole: StandardSchemaV1 | undefined\n let perKey: Readonly<Record<string, StandardSchemaV1 | undefined>> = {}\n if (isStandardSchema(refinement)) {\n whole = refinement\n } else if (refinement !== undefined) {\n if (!isPlainObject(refinement)) {\n fail(\n entity.name,\n `.${column.key} must be a Standard Schema or a per-${shape.keyName} record of them`,\n )\n }\n for (const [key, schema] of Object.entries(refinement)) {\n if (!shape.keys.includes(key)) {\n fail(entity.name, `.${column.key}.${key} is not a configured ${shape.keyName}`)\n }\n if (schema !== undefined && !isStandardSchema(schema)) {\n fail(entity.name, `.${column.key}.${key} must be a Standard Schema`)\n }\n }\n perKey = refinement as Readonly<Record<string, StandardSchemaV1 | undefined>>\n }\n return fieldSchema(column.key, required, nullable, true, keyedChecker(shape, perKey, whole))\n}\n\nexport function schemaKeys(entity: EntityMeta, mode: ValidationMode): string[] {\n if (mode === 'select') return [...entity.visibleKeys]\n return Object.keys(entity.columns).filter(\n (key) => !isGeneratedAlways(entity.columns[key] as ColumnMeta),\n )\n}\n\nexport function structuralSchema(\n entity: EntityMeta,\n mode: ValidationMode,\n refinements: Record<string, unknown> | undefined,\n): StructuralSchema {\n for (const key of Object.keys(refinements ?? {})) {\n if (!(key in entity.columns)) fail(entity.name, `.${key} does not name a column`)\n }\n const fields: Record<string, StructuralFieldSchema> = {}\n for (const key of schemaKeys(entity, mode)) {\n const column = entity.columns[key] as ColumnMeta\n fields[key] = buildField(entity, column, mode, refinements?.[key])\n }\n\n type RowResult = StandardSchemaV1.Result<Record<string, unknown>>\n const validate = (input: unknown): MaybeAsync<RowResult> => {\n if (!isPlainObject(input)) return { issues: [issueAt('Expected an object')] }\n const issues: StandardSchemaV1.Issue[] = []\n const output: Record<string, unknown> = {}\n const pending: Promise<void>[] = []\n for (const key of Object.keys(input)) {\n if (!(key in fields)) issues.push(issueAt(`Unknown key '${key}'`, [key]))\n }\n for (const [key, field] of Object.entries(fields)) {\n const result = then(field['~standard'].validate(input[key]), (r) => {\n if (r.issues !== undefined) issues.push(...prefixIssues(r.issues, [key]))\n else if (r.value !== undefined) output[key] = r.value\n })\n if (isPromiseLike(result)) pending.push(result)\n }\n const finish = (): RowResult => (issues.length > 0 ? { issues } : { value: output })\n return pending.length === 0 ? finish() : Promise.all(pending).then(finish)\n }\n\n return {\n entity: entity.name,\n mode,\n fields,\n '~standard': { version: 1, vendor: VENDOR, validate },\n }\n}\n\nexport function structuralValidation(): StructuralValidationAdapter {\n return {\n id: 'structural',\n base: (entity, mode, refinements) => structuralSchema(entity, mode, refinements),\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,26 @@
1
+ import { Gt as ContextFactoryInput, Mt as AfterCommitErrorHandler, Sa as PluginSystemOptions, a as MaybePromise, ba as PluginContextOptions, f as TypionApp, m as TypionTypes, n as ContextInput, p as TypionEngine, pn as TypionConfig, un as TypesOf } from "../context-B4e2eREP.js";
2
+ import { PgAsyncDatabase, PgQueryResultHKT } from "drizzle-orm/pg-core";
3
+ //#region src/testing/harness.d.ts
4
+ interface TestTypion<TE extends TypionTypes> {
5
+ readonly typion: TypionEngine<TE>;
6
+ readonly db: PgAsyncDatabase<PgQueryResultHKT, TE['relations']>;
7
+ reset(): Promise<void>;
8
+ close(): Promise<void>;
9
+ asUser(ctx: TE['ctx'], opts?: {
10
+ meta?: Record<string, unknown>;
11
+ } & PluginContextOptions<TE['plugins']>): TypionApp<TE, 'user'>;
12
+ asSystem(reason?: string, opts?: {
13
+ ctx?: Partial<TE['ctx']>;
14
+ meta?: Record<string, unknown>;
15
+ } & PluginContextOptions<TE['plugins']> & PluginSystemOptions<TE['plugins']>): TypionApp<TE, 'system'>;
16
+ }
17
+ type TestTypionOptions<TE extends TypionTypes, TInput = ContextInput> = {
18
+ context?: (input: TInput) => MaybePromise<TE['ctx']>;
19
+ onAfterCommitError?: AfterCommitErrorHandler;
20
+ checkSchema?: boolean;
21
+ now?: () => Date;
22
+ };
23
+ export declare function createTestTypion<const C extends TypionConfig<any>>(config: C, options?: TestTypionOptions<TypesOf<C>, ContextFactoryInput>): Promise<TestTypion<TypesOf<C>>>;
24
+ //#endregion
25
+ export type { TestTypion, TestTypionOptions };
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { r as standIn, t as createFromConfig } from "../typion-Dh2BCSRz.js";
2
+ import { PgTable, getTableConfig } from "drizzle-orm/pg-core";
3
+ import { is, sql } from "drizzle-orm";
4
+ import { PGlite } from "@electric-sql/pglite";
5
+ import { drizzle } from "drizzle-orm/pglite";
6
+ import { randomBytes } from "node:crypto";
7
+ import { pushSchema } from "drizzle-kit/api-postgres";
8
+ //#region src/testing/harness.ts
9
+ async function openDatabase(relations) {
10
+ const url = process.env.TYPION_TEST_DATABASE_URL;
11
+ if (url === void 0 || url === "") {
12
+ const client = new PGlite();
13
+ return {
14
+ db: drizzle({
15
+ client,
16
+ relations
17
+ }),
18
+ close: () => client.close()
19
+ };
20
+ }
21
+ const { default: pg } = await import("pg");
22
+ const { drizzle: drizzlePg } = await import("drizzle-orm/node-postgres");
23
+ const name = `typion_test_${randomBytes(6).toString("hex")}`;
24
+ const admin = async (statement) => {
25
+ const client = new pg.Client({ connectionString: url });
26
+ await client.connect();
27
+ try {
28
+ await client.query(statement);
29
+ } finally {
30
+ await client.end();
31
+ }
32
+ };
33
+ await admin(`create database "${name}"`);
34
+ const target = new URL(url);
35
+ target.pathname = `/${name}`;
36
+ const pool = new pg.Pool({
37
+ connectionString: target.toString(),
38
+ max: 3
39
+ });
40
+ return {
41
+ db: drizzlePg({
42
+ client: pool,
43
+ relations
44
+ }),
45
+ close: async () => {
46
+ await pool.end();
47
+ await admin(`drop database "${name}" with (force)`);
48
+ }
49
+ };
50
+ }
51
+ async function harness(def, create) {
52
+ const { db: opened, close } = await openDatabase(def.relations);
53
+ const db = opened;
54
+ const { apply } = await pushSchema(def.schema, db);
55
+ await apply();
56
+ const typion = create(db);
57
+ const tables = Object.values(def.schema).filter((value) => is(value, PgTable)).map((table) => {
58
+ const info = getTableConfig(table);
59
+ return `"${info.schema ?? "public"}"."${info.name}"`;
60
+ });
61
+ return {
62
+ typion,
63
+ db,
64
+ async reset() {
65
+ if (tables.length > 0) await db.execute(sql.raw(`truncate table ${tables.join(", ")} cascade`));
66
+ },
67
+ close,
68
+ asUser: (ctx, opts) => typion.$as(ctx, opts),
69
+ asSystem: (reason = "test", opts = {}) => typion.$system({
70
+ reason,
71
+ ...opts
72
+ })
73
+ };
74
+ }
75
+ async function createTestTypion(config, options = {}) {
76
+ const t = await harness(config.definition, (db) => createFromConfig(config, {
77
+ ...options,
78
+ db
79
+ }));
80
+ const restore = standIn(config, t.typion);
81
+ return {
82
+ ...t,
83
+ close: async () => {
84
+ restore();
85
+ await t.close();
86
+ }
87
+ };
88
+ }
89
+ //#endregion
90
+ export { createTestTypion };
91
+
92
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/testing/harness.ts"],"sourcesContent":["import { randomBytes } from 'node:crypto'\nimport { PGlite } from '@electric-sql/pglite'\nimport { pushSchema } from 'drizzle-kit/api-postgres'\nimport { type AnyRelations, is, sql } from 'drizzle-orm'\nimport {\n getTableConfig,\n type PgAsyncDatabase,\n type PgQueryResultHKT,\n PgTable,\n} from 'drizzle-orm/pg-core'\nimport { drizzle } from 'drizzle-orm/pglite'\nimport { createEngine } from '../runtime/engine.js'\nimport { createFromConfig, standIn } from '../runtime/typion.js'\nimport type {\n AfterCommitErrorHandler,\n ContextFactoryInput,\n TypesOf,\n TypionConfig,\n} from '../types/collection.js'\nimport type { ContextInput, MaybePromise } from '../types/context.js'\nimport type { TypionApp, TypionEngine, TypionTypes } from '../types/engine.js'\nimport type { PluginContextOptions, PluginSystemOptions } from '../types/kinds.js'\nimport type { TypionDefinition } from '../types/meta.js'\n\nexport interface TestTypion<TE extends TypionTypes> {\n readonly typion: TypionEngine<TE>\n readonly db: PgAsyncDatabase<PgQueryResultHKT, TE['relations']>\n // Truncates every table of the definition; faster than a new database per test.\n reset(): Promise<void>\n close(): Promise<void>\n asUser(\n ctx: TE['ctx'],\n opts?: { meta?: Record<string, unknown> } & PluginContextOptions<TE['plugins']>,\n ): TypionApp<TE, 'user'>\n asSystem(\n reason?: string,\n opts?: {\n ctx?: Partial<TE['ctx']>\n meta?: Record<string, unknown>\n } & PluginContextOptions<TE['plugins']> &\n PluginSystemOptions<TE['plugins']>,\n ): TypionApp<TE, 'system'>\n}\n\nexport type TestTypionOptions<TE extends TypionTypes, TInput = ContextInput> = {\n // Replaces the context factory of the config, and gets what it gets: `system` and `db` too, for a config's instance.\n context?: (input: TInput) => MaybePromise<TE['ctx']>\n onAfterCommitError?: AfterCommitErrorHandler\n checkSchema?: boolean\n // A fixed clock for timestamps the engine writes.\n now?: () => Date\n}\n\ntype TestDatabase = {\n db: PgAsyncDatabase<PgQueryResultHKT, AnyRelations>\n close: () => Promise<void>\n}\n\n// `TYPION_TEST_DATABASE_URL` runs the instance on that Postgres server instead of PGlite, in a database of its\n// own, created here and dropped on close: suites run in parallel and must never share tables.\nasync function openDatabase(relations: AnyRelations): Promise<TestDatabase> {\n const url = process.env.TYPION_TEST_DATABASE_URL\n if (url === undefined || url === '') {\n const client = new PGlite()\n return { db: drizzle({ client, relations }), close: () => client.close() }\n }\n const { default: pg } = await import('pg')\n const { drizzle: drizzlePg } = await import('drizzle-orm/node-postgres')\n const name = `typion_test_${randomBytes(6).toString('hex')}`\n const admin = async (statement: string) => {\n const client = new pg.Client({ connectionString: url })\n await client.connect()\n try {\n await client.query(statement)\n } finally {\n await client.end()\n }\n }\n await admin(`create database \"${name}\"`)\n const target = new URL(url)\n target.pathname = `/${name}`\n // Few connections each: many instances share the server's limit while the suites run in parallel.\n const pool = new pg.Pool({ connectionString: target.toString(), max: 3 })\n return {\n db: drizzlePg({ client: pool, relations }),\n close: async () => {\n await pool.end()\n await admin(`drop database \"${name}\" with (force)`)\n },\n }\n}\n\n// pushSchema diffs every schema in the database, so the complete `schema` (user, generated and plugin tables\n// plus their pg schemas) is always passed: a partial record would drop the missing tables.\nasync function harness<TE extends TypionTypes>(\n def: TypionDefinition<TE>,\n create: (db: TestTypion<TE>['db']) => TypionEngine<TE>,\n): Promise<TestTypion<TE>> {\n const { db: opened, close } = await openDatabase(def.relations)\n const db = opened as TestTypion<TE>['db']\n const { apply } = await pushSchema(def.schema as Record<string, unknown>, db as never)\n await apply()\n const typion = create(db)\n const tables = Object.values(def.schema)\n .filter((value): value is PgTable => is(value, PgTable))\n .map((table) => {\n const info = getTableConfig(table)\n return `\"${info.schema ?? 'public'}\".\"${info.name}\"`\n })\n return {\n typion,\n db,\n async reset() {\n if (tables.length > 0)\n await db.execute(sql.raw(`truncate table ${tables.join(', ')} cascade`))\n },\n close,\n asUser: (ctx, opts) => typion.$as(ctx, opts),\n asSystem: (reason = 'test', opts = {} as never) => typion.$system({ reason, ...opts } as never),\n }\n}\n\n// The instance of a config on a fresh in-memory database; the config's context factory applies unless\n// `context` replaces it. Until `close`, it is the config's instance: `getTypion(config)` returns it, so a Hono app\n// or a route handler tested with requests reads and writes the test's database.\nexport async function createTestTypion<const C extends TypionConfig<any>>(\n config: C,\n options: TestTypionOptions<TypesOf<C>, ContextFactoryInput> = {},\n): Promise<TestTypion<TypesOf<C>>> {\n const t = await harness(config.definition, (db) =>\n createFromConfig(config, { ...options, db: db as never }),\n )\n const restore = standIn(config, t.typion)\n return {\n ...t,\n close: async () => {\n restore()\n await t.close()\n },\n } as never\n}\n\n// The same over a definition of the internal chain, for the engine's own suite.\nexport function createTestEngine<TE extends TypionTypes>(\n def: TypionDefinition<TE>,\n options: TestTypionOptions<TE> = {},\n): Promise<TestTypion<TE>> {\n return harness(def, (db) => createEngine(def, { ...options, db: db as never }))\n}\n"],"mappings":";;;;;;;;AA4DA,eAAe,aAAa,WAAgD;CAC1E,MAAM,MAAM,QAAQ,IAAI;CACxB,IAAI,QAAQ,KAAA,KAAa,QAAQ,IAAI;EACnC,MAAM,SAAS,IAAI,OAAO;EAC1B,OAAO;GAAE,IAAI,QAAQ;IAAE;IAAQ;GAAU,CAAC;GAAG,aAAa,OAAO,MAAM;EAAE;CAC3E;CACA,MAAM,EAAE,SAAS,OAAO,MAAM,OAAO;CACrC,MAAM,EAAE,SAAS,cAAc,MAAM,OAAO;CAC5C,MAAM,OAAO,eAAe,YAAY,CAAC,CAAC,CAAC,SAAS,KAAK;CACzD,MAAM,QAAQ,OAAO,cAAsB;EACzC,MAAM,SAAS,IAAI,GAAG,OAAO,EAAE,kBAAkB,IAAI,CAAC;EACtD,MAAM,OAAO,QAAQ;EACrB,IAAI;GACF,MAAM,OAAO,MAAM,SAAS;EAC9B,UAAU;GACR,MAAM,OAAO,IAAI;EACnB;CACF;CACA,MAAM,MAAM,oBAAoB,KAAK,EAAE;CACvC,MAAM,SAAS,IAAI,IAAI,GAAG;CAC1B,OAAO,WAAW,IAAI;CAEtB,MAAM,OAAO,IAAI,GAAG,KAAK;EAAE,kBAAkB,OAAO,SAAS;EAAG,KAAK;CAAE,CAAC;CACxE,OAAO;EACL,IAAI,UAAU;GAAE,QAAQ;GAAM;EAAU,CAAC;EACzC,OAAO,YAAY;GACjB,MAAM,KAAK,IAAI;GACf,MAAM,MAAM,kBAAkB,KAAK,eAAe;EACpD;CACF;AACF;AAIA,eAAe,QACb,KACA,QACyB;CACzB,MAAM,EAAE,IAAI,QAAQ,UAAU,MAAM,aAAa,IAAI,SAAS;CAC9D,MAAM,KAAK;CACX,MAAM,EAAE,UAAU,MAAM,WAAW,IAAI,QAAmC,EAAW;CACrF,MAAM,MAAM;CACZ,MAAM,SAAS,OAAO,EAAE;CACxB,MAAM,SAAS,OAAO,OAAO,IAAI,MAAM,CAAC,CACrC,QAAQ,UAA4B,GAAG,OAAO,OAAO,CAAC,CAAC,CACvD,KAAK,UAAU;EACd,MAAM,OAAO,eAAe,KAAK;EACjC,OAAO,IAAI,KAAK,UAAU,SAAS,KAAK,KAAK,KAAK;CACpD,CAAC;CACH,OAAO;EACL;EACA;EACA,MAAM,QAAQ;GACZ,IAAI,OAAO,SAAS,GAClB,MAAM,GAAG,QAAQ,IAAI,IAAI,kBAAkB,OAAO,KAAK,IAAI,EAAE,SAAS,CAAC;EAC3E;EACA;EACA,SAAS,KAAK,SAAS,OAAO,IAAI,KAAK,IAAI;EAC3C,WAAW,SAAS,QAAQ,OAAO,CAAC,MAAe,OAAO,QAAQ;GAAE;GAAQ,GAAG;EAAK,CAAU;CAChG;AACF;AAKA,eAAsB,iBACpB,QACA,UAA8D,CAAC,GAC9B;CACjC,MAAM,IAAI,MAAM,QAAQ,OAAO,aAAa,OAC1C,iBAAiB,QAAQ;EAAE,GAAG;EAAa;CAAY,CAAC,CAC1D;CACA,MAAM,UAAU,QAAQ,QAAQ,EAAE,MAAM;CACxC,OAAO;EACL,GAAG;EACH,OAAO,YAAY;GACjB,QAAQ;GACR,MAAM,EAAE,MAAM;EAChB;CACF;AACF"}