fastify-auto-sqlapi 0.1.14 → 0.2.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/AGENTS_BACKEND.md +4 -4
- package/BREAKING_CHANGES.md +5 -5
- package/README.md +1 -1
- package/dist/bin/generate-schema.js +89 -77
- package/dist/bin/generate-tables.js +72 -62
- package/dist/bin/utils.d.ts.map +1 -1
- package/dist/bin/utils.js +47 -33
- package/dist/lib/agent/manifest.d.ts +3 -2
- package/dist/lib/agent/manifest.d.ts.map +1 -1
- package/dist/lib/agent/manifest.js +102 -80
- package/dist/lib/cli/mysql-introspect.d.ts +4 -9
- package/dist/lib/cli/mysql-introspect.d.ts.map +1 -1
- package/dist/lib/cli/mysql-introspect.js +8 -7
- package/dist/lib/cli/pg-introspect.d.ts.map +1 -1
- package/dist/lib/cli/pg-introspect.js +0 -1
- package/dist/lib/cli/schema-codegen.d.ts +3 -0
- package/dist/lib/cli/schema-codegen.d.ts.map +1 -1
- package/dist/lib/cli/schema-codegen.js +3 -0
- package/dist/lib/cli/tables-codegen.d.ts +8 -1
- package/dist/lib/cli/tables-codegen.d.ts.map +1 -1
- package/dist/lib/cli/tables-codegen.js +100 -118
- package/dist/lib/condition-methods.d.ts +14 -4
- package/dist/lib/condition-methods.d.ts.map +1 -1
- package/dist/lib/condition-methods.js +18 -4
- package/dist/lib/db.d.ts +1 -1
- package/dist/lib/db.d.ts.map +1 -1
- package/dist/lib/db.js +16 -4
- package/dist/lib/dialect.d.ts +2 -1
- package/dist/lib/dialect.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.d.ts.map +1 -1
- package/dist/lib/engine/bulk/bulk-upsert.js +42 -35
- package/dist/lib/engine/rest/delete.d.ts.map +1 -1
- package/dist/lib/engine/rest/get.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.d.ts.map +1 -1
- package/dist/lib/engine/rest/insert.js +1 -1
- package/dist/lib/engine/rest/update.d.ts.map +1 -1
- package/dist/lib/engine/rest/update.js +25 -18
- package/dist/lib/engine/search/aggregations.d.ts +33 -0
- package/dist/lib/engine/search/aggregations.d.ts.map +1 -0
- package/dist/lib/engine/search/aggregations.js +109 -0
- package/dist/lib/engine/search/conditions.d.ts +31 -0
- package/dist/lib/engine/search/conditions.d.ts.map +1 -0
- package/dist/lib/engine/search/conditions.js +131 -0
- package/dist/lib/engine/search/fields.d.ts +49 -0
- package/dist/lib/engine/search/fields.d.ts.map +1 -0
- package/dist/lib/engine/search/fields.js +142 -0
- package/dist/lib/engine/search/join-families.d.ts +35 -0
- package/dist/lib/engine/search/join-families.d.ts.map +1 -0
- package/dist/lib/engine/search/join-families.js +345 -0
- package/dist/lib/engine/search/joins.d.ts +55 -0
- package/dist/lib/engine/search/joins.d.ts.map +1 -0
- package/dist/lib/engine/search/joins.js +81 -0
- package/dist/lib/engine/search/order-by.d.ts +35 -0
- package/dist/lib/engine/search/order-by.d.ts.map +1 -0
- package/dist/lib/engine/search/order-by.js +104 -0
- package/dist/lib/engine/search/search.d.ts.map +1 -1
- package/dist/lib/engine/search/search.js +101 -787
- package/dist/lib/engine/validate.d.ts +0 -5
- package/dist/lib/engine/validate.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.d.ts +25 -17
- package/dist/lib/engine/write-helpers.d.ts.map +1 -1
- package/dist/lib/engine/write-helpers.js +86 -44
- package/dist/lib/read-access.d.ts +12 -1
- package/dist/lib/read-access.d.ts.map +1 -1
- package/dist/lib/read-access.js +20 -4
- package/dist/lib/schema/helpers.d.ts +0 -15
- package/dist/lib/schema/helpers.d.ts.map +1 -1
- package/dist/lib/schema/helpers.js +7 -4
- package/dist/lib/schema/search.d.ts.map +1 -1
- package/dist/lib/schema/search.js +107 -128
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/tenant.d.ts +26 -0
- package/dist/lib/tenant.d.ts.map +1 -1
- package/dist/lib/tenant.js +46 -0
- package/dist/routes/auto/route-helpers.d.ts +0 -7
- package/dist/routes/auto/route-helpers.d.ts.map +1 -1
- package/dist/routes/auto/route-helpers.js +46 -30
- package/dist/routes/auto/search.routes.js +0 -1
- package/dist/types/table.d.ts +1 -1
- package/docs/GLOSSARY.md +28 -0
- package/docs/adr/0012-anti-duplication-tooling.md +59 -0
- package/docs/adr/README.md +3 -1
- package/package.json +29 -2
package/AGENTS_BACKEND.md
CHANGED
|
@@ -462,7 +462,7 @@ export const TableCustomer = defineTable({
|
|
|
462
462
|
|
|
463
463
|
## Computed Fields (extension system)
|
|
464
464
|
|
|
465
|
-
`computedFields` lets you declare **virtual fields** as SQL expressions on a per-table basis. Each computed becomes usable like a regular schema field across the search API: `filters` (equality), `conditions` (operators), `orderBy` (1-
|
|
465
|
+
`computedFields` lets you declare **virtual fields** as SQL expressions on a per-table basis. Each computed becomes usable like a regular schema field across the search API: `filters` (equality), `conditions` (operators), `orderBy` (1-part), `computeMin/Max/Sum/Avg`, and (opt-in) in `selectComputed` for the main response. Same machinery serves JSON column extraction, derived strings, dialect-aware date/calendar bucketing — without growing the library case-by-case.
|
|
466
466
|
|
|
467
467
|
```typescript
|
|
468
468
|
import { defineTable, exportTableInfo, Type } from 'fastify-auto-sqlapi';
|
|
@@ -546,7 +546,7 @@ For `joinLeft` specifically, the computed expr is automatically alias-prefixed b
|
|
|
546
546
|
|
|
547
547
|
- **Bound `values` in computed expressions** work anywhere the expression lands in the `WHERE` clause or in `ORDER BY`: `filters` and `conditions` (main, `joinMustExist`, `joinMultiple`, `joinGroup`, `joinLeft`) and `orderBy`. The ConditionBuilder assigns the placeholder positions there, so no coordination is needed from the caller.
|
|
548
548
|
- They are still rejected with 400 on `selectComputed`, `computeMin/Max/Sum/Avg`, `joinGroup.aggregations.by` and `defaultOrder`. Those expressions are emitted in the `SELECT` / `GROUP BY` list, which precedes the `WHERE` values in the parameter order — there is no correct position for them there. Most use cases (JSON extraction, concat, dateTrunc, simple ops) need no bound values at all.
|
|
549
|
-
- **Computed CAN be used in `joinGroup.aggregations.by`** (just pass the computed name as a string). Bound `values` are not supported in this position (rejected with 400 — it is emitted in the `SELECT`/`GROUP BY` list). The existing FK-correlation rule for `orderBy <alias>.<fn>.<field>` still applies: 3-
|
|
549
|
+
- **Computed CAN be used in `joinGroup.aggregations.by`** (just pass the computed name as a string). Bound `values` are not supported in this position (rejected with 400 — it is emitted in the `SELECT`/`GROUP BY` list). The existing FK-correlation rule for `orderBy <alias>.<fn>.<field>` still applies: 3-part aggregation orderBy on a `by` that isn't the correlation FK is rejected — by definition a computed-by produces multiple groups per main row, so this is never valid.
|
|
550
550
|
- **Computed cannot be used as aggregation function** (`sum`/`min`/`max`/...). The values inside `aggregations.sum: ['<name>']` must be schema field names. To aggregate on a derived expression, declare the derivation as a computed and pass the computed name as the field, BUT only via `computeMin/Max/Sum/Avg` (top-level main aggregates), not the joinGroup ones.
|
|
551
551
|
- **No chained computed** (a computed referencing another computed). Flat-only.
|
|
552
552
|
- **Read-only**. Computed fields are not usable in insert/update bodies — the consumer's `expr` is for SELECT/WHERE/ORDER BY, never for writes.
|
|
@@ -635,8 +635,8 @@ const sqlApi = createSqlApi(db, dbTables, { dialect: 'mysql' });
|
|
|
635
635
|
```typescript
|
|
636
636
|
// Search — full filter, join, pagination, aggregation support
|
|
637
637
|
// orderBy supports two dotted notations:
|
|
638
|
-
// - 3-
|
|
639
|
-
// - 2-
|
|
638
|
+
// - 3-part `<alias>.<fn>.<field>` for joinGroup aggregations (must be declared in the request body)
|
|
639
|
+
// - 2-part `<alias>.<field>` for joinLeft parent fields
|
|
640
640
|
// See AGENTS_FRONTEND.md and BREAKING_CHANGES.md for the full reference.
|
|
641
641
|
sqlApi.search(tableName, {
|
|
642
642
|
filters?, conditions?,
|
package/BREAKING_CHANGES.md
CHANGED
|
@@ -144,7 +144,7 @@ Real `LEFT JOIN` for N:1 (parent) relationships. The parent rows are returned in
|
|
|
144
144
|
|
|
145
145
|
The engine attaches a real `LEFT JOIN <parentTable> AS <alias>` to the main query **only when needed**:
|
|
146
146
|
- when the request includes `filters` or `conditions` on the parent (effectively turns into INNER JOIN behavior on those aliases — main rows whose parent doesn't match are excluded);
|
|
147
|
-
- when `orderBy` uses 2-
|
|
147
|
+
- when `orderBy` uses 2-part dotted notation referring to a parent field (`alias.field`).
|
|
148
148
|
|
|
149
149
|
Otherwise only a side query `WHERE pk IN (distinct fk values)` is issued — no LEFT JOIN, no row duplication. Either way, the response shape is the same.
|
|
150
150
|
|
|
@@ -158,7 +158,7 @@ Otherwise only a side query `WHERE pk IN (distinct fk values)` is issued — no
|
|
|
158
158
|
| `<alias>.<field>` | `joinLeft` (`unique: true` aliases) | orders main query by a parent field via LEFT JOIN |
|
|
159
159
|
| `<alias>.<fn>.<field>` | `joinGroup` (declared in the same body) | orders main query by an aggregation via correlated scalar subquery |
|
|
160
160
|
|
|
161
|
-
Ambiguity: 2-
|
|
161
|
+
Ambiguity: 2-part and 3-part are disambiguated by counting dots. The 2-part is rejected unless the alias is in `joinLeft` allowlist; 3-part is rejected unless the alias is in the request `joinGroup` declaration.
|
|
162
162
|
|
|
163
163
|
## Response shape
|
|
164
164
|
|
|
@@ -248,7 +248,7 @@ If you used the join table's name as the secondaries key, change it to the new a
|
|
|
248
248
|
|
|
249
249
|
`<table>.<fn>.<field>` still works for joinGroup, but `<table>` must now be the alias declared in `buildRelation`. If the alias differs from the old `tableName`, update the string.
|
|
250
250
|
|
|
251
|
-
If you need to order by a parent field, **add a `joinLeft`-eligible relation** (with `unique: true`) and use 2-
|
|
251
|
+
If you need to order by a parent field, **add a `joinLeft`-eligible relation** (with `unique: true`) and use 2-part notation `alias.field` (NEW capability — not available before).
|
|
252
252
|
|
|
253
253
|
## Validation summary (400 responses)
|
|
254
254
|
|
|
@@ -257,8 +257,8 @@ The engine rejects, with `statusCode: 400`, any of:
|
|
|
257
257
|
- `joinLeft` referencing an alias declared with `unique: false`
|
|
258
258
|
- `joinMultiple`/`joinMustExist`/`joinGroup` referencing an alias declared with `unique: true`
|
|
259
259
|
- request keys referencing an alias not declared in `allowedReadJoins`
|
|
260
|
-
- `orderBy` 2-
|
|
261
|
-
- `orderBy` 3-
|
|
260
|
+
- `orderBy` 2-part `<alias>.<field>` whose alias is not in `joinLeft`-eligible (i.e. `unique:true`) declarations
|
|
261
|
+
- `orderBy` 3-part `<alias>.<fn>.<field>` whose alias or `(fn, field)` pair is not declared in `joinGroup` for the same request
|
|
262
262
|
- `defineTable` with two `allowedReadJoins`/`allowedWriteJoins` entries resolving to the same alias (thrown at startup, not at request time)
|
|
263
263
|
|
|
264
264
|
---
|
package/README.md
CHANGED
|
@@ -590,7 +590,7 @@ Search with filters, advanced conditions, pagination, ordering, joins, and aggre
|
|
|
590
590
|
- **`joinMustExist`** — EXISTS-based filter: "main rows where at least one related row matches". Accepts `{ filters, conditions }` (both optional). Aliases must come from `allowedReadJoins` declarations with `unique: false`.
|
|
591
591
|
- **`joinMultiple`** — fetches related child rows in a side query. Accepts `{ filters, conditions, selection }`. Same `unique: false` aliases.
|
|
592
592
|
- **`joinGroup`** — aggregations on the related table. Supports `sum`, `min`, `max`, `avg`, `count`, `distinctCount`, and optional `by` for GROUP BY (a schema field name or a computed-field name declared on the join table — e.g. for date bucketing declare a computed using `db.dateTrunc('month', qiCol('orderDate'))`). Accepts `{ filters, conditions }`. Same `unique: false` aliases.
|
|
593
|
-
- **`joinLeft`** — embeds an N:1 parent. Real `LEFT JOIN` is added on demand (only when the request has `filters`/`conditions` on the parent or uses 2-
|
|
593
|
+
- **`joinLeft`** — embeds an N:1 parent. Real `LEFT JOIN` is added on demand (only when the request has `filters`/`conditions` on the parent or uses 2-part `orderBy` on this alias). Aliases must be declared with `unique: true`. Accepts `{ filters, conditions, selection }`. Its `filters` accept schema and computed fields only: the parent's `extraFilters` need an alias-aware `extendedCondition`, so they are rejected with a `400` here instead of being silently ignored — use `joinMustExist` on the same relation when you need one.
|
|
594
594
|
|
|
595
595
|
**Dot-notation in `orderBy` and `conditions`**:
|
|
596
596
|
|
|
@@ -6,6 +6,83 @@ import { buildConnectionString, introspectTables } from '../lib/cli/pg-introspec
|
|
|
6
6
|
import { buildMysqlConnectionConfig, introspectMysqlTables } from '../lib/cli/mysql-introspect.js';
|
|
7
7
|
import { applyExcludeTables, buildTableMap, generateSchemaFile } from '../lib/cli/schema-codegen.js';
|
|
8
8
|
import { loadEnvFile, CONSOLE_COLORS, display, displayAsTableRow, parseArgs, runCli } from './utils.js';
|
|
9
|
+
const STATUS_COLOR = {
|
|
10
|
+
created: CONSOLE_COLORS.green,
|
|
11
|
+
updated: CONSOLE_COLORS.cyan,
|
|
12
|
+
untouched: CONSOLE_COLORS.gray,
|
|
13
|
+
};
|
|
14
|
+
function introspect(dialect, schema) {
|
|
15
|
+
if (dialect === 'mysql' || dialect === 'mariadb') {
|
|
16
|
+
const connConfig = buildMysqlConnectionConfig();
|
|
17
|
+
return introspectMysqlTables(connConfig, connConfig.database);
|
|
18
|
+
}
|
|
19
|
+
return introspectTables(buildConnectionString(), schema);
|
|
20
|
+
}
|
|
21
|
+
/** Narrow the map to the `--tables` selection. Returns false when nothing matched. */
|
|
22
|
+
function keepOnlyRequestedTables(tableMap, tableNames) {
|
|
23
|
+
if (!tableNames.length)
|
|
24
|
+
return true;
|
|
25
|
+
for (const schemaName of Object.keys(tableMap)) {
|
|
26
|
+
if (!tableNames.includes(tableMap[schemaName].name)) {
|
|
27
|
+
delete tableMap[schemaName];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return Object.keys(tableMap).length > 0;
|
|
31
|
+
}
|
|
32
|
+
function fileStatus(filename, content) {
|
|
33
|
+
if (!fs.existsSync(filename))
|
|
34
|
+
return 'created';
|
|
35
|
+
return fs.readFileSync(filename, 'utf-8') === content ? 'untouched' : 'updated';
|
|
36
|
+
}
|
|
37
|
+
function writeSchemaFiles(tableMap, schemasDir) {
|
|
38
|
+
const report = { created: false, touched: false, untouched: 0, files: new Set() };
|
|
39
|
+
for (const schemaName of Object.keys(tableMap)) {
|
|
40
|
+
const { name: tableName, fields, colMap, primary } = tableMap[schemaName];
|
|
41
|
+
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
42
|
+
report.files.add(`${schemaName}.ts`);
|
|
43
|
+
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary);
|
|
44
|
+
const status = fileStatus(filename, content);
|
|
45
|
+
if (status === 'created')
|
|
46
|
+
report.created = true;
|
|
47
|
+
if (status === 'untouched') {
|
|
48
|
+
report.untouched++;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(filename, content);
|
|
52
|
+
displayAsTableRow(filename, status, 90, STATUS_COLOR[status]);
|
|
53
|
+
report.touched = true;
|
|
54
|
+
}
|
|
55
|
+
return report;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Delete `Schema*.ts` files no longer backed by a table — including the ones whose table
|
|
59
|
+
* was just excluded by config. Only safe when generating every table: with `--tables` the
|
|
60
|
+
* unlisted schemas are legitimately absent from `generated`.
|
|
61
|
+
*/
|
|
62
|
+
function removeOrphanSchemas(schemasDir, generated) {
|
|
63
|
+
let removed = false;
|
|
64
|
+
for (const file of fs.readdirSync(schemasDir)) {
|
|
65
|
+
if (file.startsWith('Schema') && file.endsWith('.ts') && !generated.has(file)) {
|
|
66
|
+
const filePath = path.join(schemasDir, file);
|
|
67
|
+
fs.unlinkSync(filePath);
|
|
68
|
+
displayAsTableRow(filePath, 'removed', 90, CONSOLE_COLORS.red);
|
|
69
|
+
removed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
function reportOutcome(report) {
|
|
75
|
+
console.log('');
|
|
76
|
+
if (report.untouched > 0) {
|
|
77
|
+
display(`${report.untouched} schema(s) untouched`, CONSOLE_COLORS.gray);
|
|
78
|
+
}
|
|
79
|
+
if (report.created) {
|
|
80
|
+
display("Don't forget to git add your new Schema files.", CONSOLE_COLORS.magenta);
|
|
81
|
+
}
|
|
82
|
+
if (!report.touched) {
|
|
83
|
+
display('All schemas are already up to date.', CONSOLE_COLORS.magenta);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
9
86
|
await runCli('fastify-auto-sqlapi: generating schemas', async () => {
|
|
10
87
|
const cliArgs = parseArgs({
|
|
11
88
|
output: { type: 'value' },
|
|
@@ -15,99 +92,34 @@ await runCli('fastify-auto-sqlapi: generating schemas', async () => {
|
|
|
15
92
|
const config = await loadConfig();
|
|
16
93
|
loadEnvFile(config.envFile);
|
|
17
94
|
const dialect = (cliArgs.dialect || config.dialect || 'postgres');
|
|
18
|
-
const schema = config.schema ||
|
|
95
|
+
const schema = config.schema || 'public';
|
|
19
96
|
const outputDir = path.resolve(process.cwd(), cliArgs.output || config.outputDir);
|
|
20
97
|
const schemasDir = path.join(outputDir, 'schemas');
|
|
21
98
|
if (!fs.existsSync(schemasDir)) {
|
|
22
99
|
fs.mkdirSync(schemasDir, { recursive: true });
|
|
23
100
|
display(`Created directory: ${schemasDir}`, CONSOLE_COLORS.green);
|
|
24
101
|
}
|
|
25
|
-
|
|
26
|
-
if (dialect === 'mysql' || dialect === 'mariadb') {
|
|
27
|
-
const connConfig = buildMysqlConnectionConfig();
|
|
28
|
-
rows = await introspectMysqlTables(connConfig, connConfig.database);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
const connectionString = buildConnectionString();
|
|
32
|
-
rows = await introspectTables(connectionString, schema);
|
|
33
|
-
}
|
|
102
|
+
const rows = await introspect(dialect, schema);
|
|
34
103
|
if (rows.length === 0) {
|
|
35
104
|
display(`No tables found in schema "${schema}".`, CONSOLE_COLORS.magenta);
|
|
36
105
|
return;
|
|
37
106
|
}
|
|
38
107
|
const tableMap = buildTableMap(rows);
|
|
39
|
-
// Config blacklist: excluded tables are invisible to the generator, so their
|
|
40
|
-
//
|
|
108
|
+
// Config blacklist: excluded tables are invisible to the generator, so their schema files
|
|
109
|
+
// (if any) fall through to the orphan cleanup below. Deliberately no early return when
|
|
110
|
+
// everything is excluded — that cleanup still has to run.
|
|
41
111
|
const excluded = applyExcludeTables(tableMap, config.excludeTables);
|
|
42
112
|
if (excluded.length) {
|
|
43
113
|
display(`Excluded by config (excludeTables): ${excluded.join(', ')}`, CONSOLE_COLORS.gray);
|
|
44
114
|
}
|
|
45
|
-
// Note: no early return when everything is excluded — the orphan cleanup below
|
|
46
|
-
// must still run to remove the schema files of freshly excluded tables.
|
|
47
|
-
// Filter tables if --tables flag is provided
|
|
48
115
|
const tableNames = cliArgs.tables;
|
|
49
|
-
if (tableNames
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
delete tableMap[schemaName];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (Object.keys(tableMap).length === 0) {
|
|
56
|
-
display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
|
|
57
|
-
return;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
let somethingCreated = false;
|
|
61
|
-
let somethingTouched = false;
|
|
62
|
-
let untouched = 0;
|
|
63
|
-
const generatedFiles = new Set();
|
|
64
|
-
for (const schemaName of Object.keys(tableMap)) {
|
|
65
|
-
const { name: tableName, fields, colMap, primary } = tableMap[schemaName];
|
|
66
|
-
const filename = path.join(schemasDir, `${schemaName}.ts`);
|
|
67
|
-
generatedFiles.add(`${schemaName}.ts`);
|
|
68
|
-
const content = generateSchemaFile(schemaName, tableName, fields, colMap, primary);
|
|
69
|
-
let status = 'created';
|
|
70
|
-
let color = CONSOLE_COLORS.green;
|
|
71
|
-
if (fs.existsSync(filename)) {
|
|
72
|
-
status = 'untouched';
|
|
73
|
-
color = CONSOLE_COLORS.gray;
|
|
74
|
-
if (fs.readFileSync(filename, 'utf-8') !== content) {
|
|
75
|
-
status = 'updated';
|
|
76
|
-
color = CONSOLE_COLORS.cyan;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
else {
|
|
80
|
-
somethingCreated = true;
|
|
81
|
-
}
|
|
82
|
-
if (status !== 'untouched') {
|
|
83
|
-
fs.writeFileSync(filename, content);
|
|
84
|
-
displayAsTableRow(filename, status, 90, color);
|
|
85
|
-
somethingTouched = true;
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
untouched++;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
// Remove orphan Schema*.ts files (only when generating all tables)
|
|
92
|
-
if (!tableNames.length) {
|
|
93
|
-
const existingFiles = fs.readdirSync(schemasDir);
|
|
94
|
-
for (const file of existingFiles) {
|
|
95
|
-
if (file.startsWith('Schema') && file.endsWith('.ts') && !generatedFiles.has(file)) {
|
|
96
|
-
const filePath = path.join(schemasDir, file);
|
|
97
|
-
fs.unlinkSync(filePath);
|
|
98
|
-
displayAsTableRow(filePath, 'removed', 90, CONSOLE_COLORS.red);
|
|
99
|
-
somethingTouched = true;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
console.log('');
|
|
104
|
-
if (untouched > 0) {
|
|
105
|
-
display(`${untouched} schema(s) untouched`, CONSOLE_COLORS.gray);
|
|
106
|
-
}
|
|
107
|
-
if (somethingCreated) {
|
|
108
|
-
display("Don't forget to git add your new Schema files.", CONSOLE_COLORS.magenta);
|
|
116
|
+
if (!keepOnlyRequestedTables(tableMap, tableNames)) {
|
|
117
|
+
display(`No matching tables found for: ${tableNames.join(', ')}`, CONSOLE_COLORS.magenta);
|
|
118
|
+
return;
|
|
109
119
|
}
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
const report = writeSchemaFiles(tableMap, schemasDir);
|
|
121
|
+
if (!tableNames.length && removeOrphanSchemas(schemasDir, report.files)) {
|
|
122
|
+
report.touched = true;
|
|
112
123
|
}
|
|
124
|
+
reportOutcome(report);
|
|
113
125
|
});
|
|
@@ -22,6 +22,74 @@ function printHelp() {
|
|
|
22
22
|
display('Table*.ts files are only created if they do not already exist.', CONSOLE_COLORS.magenta);
|
|
23
23
|
display('dbTables.ts is created if missing, indexing the Table*.ts files present on disk.', CONSOLE_COLORS.magenta);
|
|
24
24
|
}
|
|
25
|
+
const tableVarOf = (schema) => 'Table' + schema.schemaName.replace(/^Schema/, '');
|
|
26
|
+
/** Parse every Schema*.ts on disk — relation detection needs them all, even for a subset run. */
|
|
27
|
+
function parseAllSchemas(schemasDir, schemaFiles) {
|
|
28
|
+
const parsed = [];
|
|
29
|
+
for (const file of schemaFiles) {
|
|
30
|
+
const content = fs.readFileSync(path.join(schemasDir, file), 'utf-8');
|
|
31
|
+
const schema = parseSchemaFile(content);
|
|
32
|
+
if (schema)
|
|
33
|
+
parsed.push(schema);
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
/** The schemas to emit Table files for. Exits when a requested name has no schema. */
|
|
38
|
+
function selectTargetSchemas(allSchemas, all, requested) {
|
|
39
|
+
if (all)
|
|
40
|
+
return allSchemas;
|
|
41
|
+
const requestedSet = new Set(requested);
|
|
42
|
+
const targets = allSchemas.filter((s) => requestedSet.has(s.tableName));
|
|
43
|
+
const foundNames = new Set(targets.map((s) => s.tableName));
|
|
44
|
+
for (const name of requestedSet) {
|
|
45
|
+
if (!foundNames.has(name)) {
|
|
46
|
+
error(`Table "${name}" not found. Available: ${allSchemas.map((s) => s.tableName).join(', ')}`);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return targets;
|
|
51
|
+
}
|
|
52
|
+
/** Write the missing Table*.ts files. Existing ones are never overwritten. */
|
|
53
|
+
function writeTableFiles(targetSchemas, allSchemas, tablesDir) {
|
|
54
|
+
const created = [];
|
|
55
|
+
for (const schema of targetSchemas) {
|
|
56
|
+
const tableVarName = tableVarOf(schema);
|
|
57
|
+
const tableFile = path.join(tablesDir, `${tableVarName}.ts`);
|
|
58
|
+
if (fs.existsSync(tableFile)) {
|
|
59
|
+
displayAsTableRow(`${tableVarName}.ts`, 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
fs.writeFileSync(tableFile, generateSingleTableFile(schema, allSchemas));
|
|
63
|
+
created.push(tableVarName);
|
|
64
|
+
displayAsTableRow(`${tableVarName}.ts`, 'created', 60, CONSOLE_COLORS.green);
|
|
65
|
+
}
|
|
66
|
+
return created;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Write `dbTables.ts` only when missing. The index must reference only the Table*.ts files
|
|
70
|
+
* actually on disk: a subset run would otherwise emit imports for files never generated,
|
|
71
|
+
* and the consumer's project would not compile. When it already exists, the newly created
|
|
72
|
+
* tables are reported instead of being silently left out.
|
|
73
|
+
*/
|
|
74
|
+
function writeDbTablesIndex(tablesDir, allSchemas, createdTableVars) {
|
|
75
|
+
const dbTablesFile = path.join(tablesDir, 'dbTables.ts');
|
|
76
|
+
if (!fs.existsSync(dbTablesFile)) {
|
|
77
|
+
const existingTableVars = new Set(fs
|
|
78
|
+
.readdirSync(tablesDir)
|
|
79
|
+
.filter((f) => f.startsWith('Table') && f.endsWith('.ts'))
|
|
80
|
+
.map((f) => f.slice(0, -'.ts'.length)));
|
|
81
|
+
const indexSchemas = allSchemas.filter((s) => existingTableVars.has(tableVarOf(s)));
|
|
82
|
+
fs.writeFileSync(dbTablesFile, generateDbTablesIndex(indexSchemas));
|
|
83
|
+
displayAsTableRow('dbTables.ts', 'created', 60, CONSOLE_COLORS.green);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
displayAsTableRow('dbTables.ts', 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
|
|
87
|
+
const indexContent = fs.readFileSync(dbTablesFile, 'utf-8');
|
|
88
|
+
const missing = createdTableVars.filter((v) => !indexContent.includes(v));
|
|
89
|
+
if (missing.length > 0) {
|
|
90
|
+
display(`Remember to add the new tables to dbTables.ts: ${missing.join(', ')}`, CONSOLE_COLORS.yellow);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
25
93
|
await runCli('fastify-auto-sqlapi: generating tables template', async () => {
|
|
26
94
|
const cliArgs = parseArgs({
|
|
27
95
|
output: { type: 'value' },
|
|
@@ -55,72 +123,14 @@ await runCli('fastify-auto-sqlapi: generating tables template', async () => {
|
|
|
55
123
|
display('Run sqlapi-generate-schema first.', CONSOLE_COLORS.magenta);
|
|
56
124
|
process.exit(1);
|
|
57
125
|
}
|
|
58
|
-
|
|
59
|
-
const allSchemas = [];
|
|
60
|
-
for (const file of schemaFiles) {
|
|
61
|
-
const content = fs.readFileSync(path.join(schemasDir, file), 'utf-8');
|
|
62
|
-
const parsed = parseSchemaFile(content);
|
|
63
|
-
if (parsed) {
|
|
64
|
-
allSchemas.push(parsed);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
126
|
+
const allSchemas = parseAllSchemas(schemasDir, schemaFiles);
|
|
67
127
|
if (allSchemas.length === 0) {
|
|
68
128
|
error('No valid schemas parsed.');
|
|
69
129
|
process.exit(1);
|
|
70
130
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
targetSchemas = allSchemas;
|
|
75
|
-
}
|
|
76
|
-
else {
|
|
77
|
-
const requestedSet = new Set(cliArgs.tables);
|
|
78
|
-
targetSchemas = allSchemas.filter((s) => requestedSet.has(s.tableName));
|
|
79
|
-
const foundNames = new Set(targetSchemas.map((s) => s.tableName));
|
|
80
|
-
for (const name of requestedSet) {
|
|
81
|
-
if (!foundNames.has(name)) {
|
|
82
|
-
error(`Table "${name}" not found. Available: ${allSchemas.map((s) => s.tableName).join(', ')}`);
|
|
83
|
-
process.exit(1);
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
const tableVarOf = (schema) => 'Table' + schema.schemaName.replace(/^Schema/, '');
|
|
88
|
-
// Generate individual Table*.ts files (skip if already exists)
|
|
89
|
-
const createdTableVars = [];
|
|
90
|
-
for (const schema of targetSchemas) {
|
|
91
|
-
const tableVarName = tableVarOf(schema);
|
|
92
|
-
const tableFile = path.join(tablesDir, `${tableVarName}.ts`);
|
|
93
|
-
if (fs.existsSync(tableFile)) {
|
|
94
|
-
displayAsTableRow(`${tableVarName}.ts`, 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
|
|
95
|
-
}
|
|
96
|
-
else {
|
|
97
|
-
const content = generateSingleTableFile(schema, allSchemas);
|
|
98
|
-
fs.writeFileSync(tableFile, content);
|
|
99
|
-
createdTableVars.push(tableVarName);
|
|
100
|
-
displayAsTableRow(`${tableVarName}.ts`, 'created', 60, CONSOLE_COLORS.green);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// Generate dbTables.ts only if it does not exist. The index must reference only
|
|
104
|
-
// Table*.ts files actually present on disk, otherwise a subset run would emit
|
|
105
|
-
// imports for files that were never generated and the project would not compile.
|
|
106
|
-
const dbTablesFile = path.join(tablesDir, 'dbTables.ts');
|
|
107
|
-
if (fs.existsSync(dbTablesFile)) {
|
|
108
|
-
displayAsTableRow('dbTables.ts', 'skipped (already exists)', 60, CONSOLE_COLORS.gray);
|
|
109
|
-
const indexContent = fs.readFileSync(dbTablesFile, 'utf-8');
|
|
110
|
-
const missing = createdTableVars.filter((v) => !indexContent.includes(v));
|
|
111
|
-
if (missing.length > 0) {
|
|
112
|
-
display(`Remember to add the new tables to dbTables.ts: ${missing.join(', ')}`, CONSOLE_COLORS.yellow);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
else {
|
|
116
|
-
const existingTableVars = new Set(fs
|
|
117
|
-
.readdirSync(tablesDir)
|
|
118
|
-
.filter((f) => f.startsWith('Table') && f.endsWith('.ts'))
|
|
119
|
-
.map((f) => f.slice(0, -'.ts'.length)));
|
|
120
|
-
const indexSchemas = allSchemas.filter((s) => existingTableVars.has(tableVarOf(s)));
|
|
121
|
-
fs.writeFileSync(dbTablesFile, generateDbTablesIndex(indexSchemas));
|
|
122
|
-
displayAsTableRow('dbTables.ts', 'created', 60, CONSOLE_COLORS.green);
|
|
123
|
-
}
|
|
131
|
+
const targetSchemas = selectTargetSchemas(allSchemas, cliArgs.all, cliArgs.tables);
|
|
132
|
+
const createdTableVars = writeTableFiles(targetSchemas, allSchemas, tablesDir);
|
|
133
|
+
writeDbTablesIndex(tablesDir, allSchemas, createdTableVars);
|
|
124
134
|
console.log('');
|
|
125
135
|
display('Edit the generated Table*.ts files to customize your table configuration.', CONSOLE_COLORS.magenta);
|
|
126
136
|
});
|
package/dist/bin/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/bin/utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAqBlD;AAED,eAAO,MAAM,cAAc;;;;;;;;CAQjB,CAAC;AAEX,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAAU,GAChB,IAAI,CAQN;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,IAAI,CAE7D;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExC;AAED,KAAK,OAAO,GACR;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,KAAK,QAAQ,CAAC,CAAC,SAAS,OAAO,IAC7B,CAAC,CAAC,MAAM,CAAC,SAAS,MAAM,GAAG,OAAO,GAClC,CAAC,CAAC,MAAM,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,GACnC,MAAM,GAAG,SAAS,CAAC;AAErB;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzD,IAAI,EAAE,CAAC,EACP,IAAI,WAAwB,GAC3B;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/bin/utils.ts"],"names":[],"mappings":"AAGA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAqBlD;AAED,eAAO,MAAM,cAAc;;;;;;;;CAQjB,CAAC;AAEX,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,KAAK,GAAE,MAAU,GAChB,IAAI,CAQN;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,IAAI,CAE7D;AAED,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAExC;AAED,KAAK,OAAO,GACR;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACjB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,GAChB;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,KAAK,QAAQ,CAAC,CAAC,SAAS,OAAO,IAC7B,CAAC,CAAC,MAAM,CAAC,SAAS,MAAM,GAAG,OAAO,GAClC,CAAC,CAAC,MAAM,CAAC,SAAS,MAAM,GAAG,MAAM,EAAE,GACnC,MAAM,GAAG,SAAS,CAAC;AAErB;;;;;;;;GAQG;AACH,wBAAgB,SAAS,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACzD,IAAI,EAAE,CAAC,EACP,IAAI,WAAwB,GAC3B;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,GAAG;IAAE,UAAU,EAAE,MAAM,EAAE,CAAA;CAAE,CAmB/D;AAqDD;;;GAGG;AACH,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAQjF"}
|
package/dist/bin/utils.js
CHANGED
|
@@ -62,51 +62,65 @@ export function error(text) {
|
|
|
62
62
|
* (split on ',') into the list as well, supporting both "a b" and "a,b" CLI shapes.
|
|
63
63
|
*/
|
|
64
64
|
export function parseArgs(spec, argv = process.argv.slice(2)) {
|
|
65
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
66
65
|
const out = { positional: [] };
|
|
67
66
|
for (const [name, def] of Object.entries(spec)) {
|
|
68
67
|
out[name] = def.type === 'flag' ? false : def.type === 'list' ? [] : undefined;
|
|
69
68
|
}
|
|
70
|
-
|
|
69
|
+
let i = 0;
|
|
70
|
+
while (i < argv.length) {
|
|
71
71
|
const token = argv[i];
|
|
72
72
|
if (token.startsWith('--')) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (!def)
|
|
76
|
-
continue;
|
|
77
|
-
if (def.type === 'flag') {
|
|
78
|
-
out[name] = true;
|
|
79
|
-
}
|
|
80
|
-
else if (def.type === 'value' && i + 1 < argv.length) {
|
|
81
|
-
out[name] = argv[++i];
|
|
82
|
-
}
|
|
83
|
-
else if (def.type === 'list' && i + 1 < argv.length) {
|
|
84
|
-
const next = argv[++i];
|
|
85
|
-
for (const part of next.split(',')) {
|
|
86
|
-
const t = part.trim();
|
|
87
|
-
if (t)
|
|
88
|
-
out[name].push(t);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
73
|
+
// An option may consume the following entry as its value.
|
|
74
|
+
i += 1 + readOption(out, spec, argv, i);
|
|
91
75
|
}
|
|
92
76
|
else {
|
|
93
|
-
out
|
|
94
|
-
|
|
95
|
-
for (const [n, def] of Object.entries(spec)) {
|
|
96
|
-
if (def.type === 'list') {
|
|
97
|
-
for (const part of token.split(',')) {
|
|
98
|
-
const t = part.trim();
|
|
99
|
-
if (t)
|
|
100
|
-
out[n].push(t);
|
|
101
|
-
}
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
77
|
+
readPositional(out, spec, token);
|
|
78
|
+
i += 1;
|
|
105
79
|
}
|
|
106
80
|
}
|
|
107
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
108
81
|
return out;
|
|
109
82
|
}
|
|
83
|
+
/** Split on ',' and append the non-empty tokens. */
|
|
84
|
+
function pushCsv(target, raw) {
|
|
85
|
+
for (const part of raw.split(',')) {
|
|
86
|
+
const t = part.trim();
|
|
87
|
+
if (t)
|
|
88
|
+
target.push(t);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Read one `--name [value]` option at `i`. Returns how many *extra* argv entries it
|
|
93
|
+
* consumed (1 when it took the next entry as its value, 0 otherwise).
|
|
94
|
+
*/
|
|
95
|
+
function readOption(out, spec, argv, i) {
|
|
96
|
+
const name = argv[i].slice(2);
|
|
97
|
+
const def = spec[name];
|
|
98
|
+
if (!def)
|
|
99
|
+
return 0;
|
|
100
|
+
const hasNext = i + 1 < argv.length;
|
|
101
|
+
if (def.type === 'flag') {
|
|
102
|
+
out[name] = true;
|
|
103
|
+
}
|
|
104
|
+
else if (def.type === 'value' && hasNext) {
|
|
105
|
+
out[name] = argv[i + 1];
|
|
106
|
+
return 1;
|
|
107
|
+
}
|
|
108
|
+
else if (def.type === 'list' && hasNext) {
|
|
109
|
+
pushCsv(out[name], argv[i + 1]);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
}
|
|
114
|
+
/** A positional token also feeds the (single) list spec, so `a b` and `a,b` both work. */
|
|
115
|
+
function readPositional(out, spec, token) {
|
|
116
|
+
out.positional.push(token);
|
|
117
|
+
for (const [name, def] of Object.entries(spec)) {
|
|
118
|
+
if (def.type === 'list') {
|
|
119
|
+
pushCsv(out[name], token);
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
110
124
|
/**
|
|
111
125
|
* CLI entry-point wrapper: prints a header, awaits `fn`, and turns thrown errors into a red message
|
|
112
126
|
* with a non-zero exit code (without aborting the process synchronously, so unfinished stdout flushes).
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { DbTables, ITable, TableOperation } from '../../types.js';
|
|
2
|
-
|
|
2
|
+
interface ManifestField {
|
|
3
3
|
type: string;
|
|
4
4
|
/** Present (true) when the field must be sent on insert. */
|
|
5
5
|
required?: boolean;
|
|
@@ -8,7 +8,7 @@ export interface ManifestField {
|
|
|
8
8
|
/** Present (true) when the field is writable but never returned by reads (readExclude). */
|
|
9
9
|
writeOnly?: boolean;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
interface ManifestJoin {
|
|
12
12
|
alias: string;
|
|
13
13
|
table: string;
|
|
14
14
|
/** '1:N' → usable in joinMustExist/joinMultiple/joinGroup; 'N:1' → usable in joinLeft. */
|
|
@@ -50,4 +50,5 @@ export declare function buildAgentManifest(dbTables: DbTables): AgentManifest;
|
|
|
50
50
|
* Notation: `!` required on insert, `?` nullable, `(writeOnly)` never returned by reads.
|
|
51
51
|
*/
|
|
52
52
|
export declare function renderAgentManifestMd(manifest: AgentManifest): string;
|
|
53
|
+
export {};
|
|
53
54
|
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../../src/lib/agent/manifest.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAkB,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAOvF,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,mDAAmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,2FAA2F;IAC3F,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,0FAA0F;IAC1F,IAAI,EAAE,KAAK,GAAG,KAAK,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,2CAA2C;IAC3C,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAC3B,UAAU,EAAE,cAAc,EAAE,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACtC,uFAAuF;IACvF,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,sFAAsF;IACtF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,qEAAqE;IACrE,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,SAAS,CAAC,EAAE,YAAY,EAAE,CAAC;IAC3B,oEAAoE;IACpE,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,gFAAgF;IAChF,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;CACvC;AAgCD,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,EAAE,CAInE;AA0DD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,QAAQ,GAAG,aAAa,CAMpE;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,CAkBrE"}
|