fastify-auto-sqlapi 0.1.13 → 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 +32 -5
- package/BREAKING_CHANGES.md +80 -5
- package/CHANGELOG.md +45 -0
- package/README.md +49 -7
- 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 +104 -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 +106 -749
- 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 +37 -1
- package/dist/lib/read-access.d.ts.map +1 -1
- package/dist/lib/read-access.js +64 -1
- 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 +120 -121
- package/dist/lib/sql-api.d.ts.map +1 -1
- package/dist/lib/table-helpers.d.ts +16 -1
- package/dist/lib/table-helpers.d.ts.map +1 -1
- package/dist/lib/table-helpers.js +55 -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/join.d.ts +9 -0
- package/dist/types/join.d.ts.map +1 -1
- package/dist/types/table.d.ts +1 -1
- package/docs/GLOSSARY.md +28 -0
- package/docs/adr/0010-joins-do-not-run-route-guards.md +100 -0
- package/docs/adr/0011-join-fields-allowlist.md +95 -0
- package/docs/adr/0012-anti-duplication-tooling.md +59 -0
- package/docs/adr/README.md +5 -1
- package/package.json +29 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Type } from '@sinclair/typebox';
|
|
2
|
-
import { ALLOWED_METHODS } from '../condition-methods.js';
|
|
2
|
+
import { ALLOWED_METHODS, MAX_CONDITIONS, MAX_ORDER_BY_LENGTH } from '../condition-methods.js';
|
|
3
3
|
import { readableFieldNames } from '../read-access.js';
|
|
4
4
|
import { DEFAULT_MAX_ITEMS_PER_PAGE } from '../../types.js';
|
|
5
5
|
/** Schema fields minus the ones hidden by `readExclude`. */
|
|
@@ -12,6 +12,112 @@ function readableFields(schema, tableConf) {
|
|
|
12
12
|
}
|
|
13
13
|
return out;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* A `filters` map.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately NOT `additionalProperties: false`, unlike the write bodies. Fastify runs Ajv
|
|
19
|
+
* with `removeAdditional: true`, so a closed schema would silently *strip* an unknown filter
|
|
20
|
+
* key before the handler ever sees it — reproducing the very bug it looks like it fixes, and
|
|
21
|
+
* leaving the engine's rejection unreachable over HTTP. Unknown keys are rejected by
|
|
22
|
+
* `assertKnownFilterKeys` in the engine instead, which also covers `sqlApi.search()`.
|
|
23
|
+
*/
|
|
24
|
+
function filtersObject(fields) {
|
|
25
|
+
return Type.Partial(Type.Object(fields));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* TypeBox types declared by a table's computed fields.
|
|
29
|
+
*
|
|
30
|
+
* Each computed is dry-run with a stub context purely to read its declared `type`; the
|
|
31
|
+
* functions are pure with respect to that context. Best-effort by design — a computed that
|
|
32
|
+
* throws is skipped, because this only feeds the Swagger/body schema and the engine
|
|
33
|
+
* validates for real at request time.
|
|
34
|
+
*/
|
|
35
|
+
function computedFieldTypes(tableConf) {
|
|
36
|
+
const out = {};
|
|
37
|
+
for (const [name, fn] of Object.entries(tableConf?.computedFields ?? {})) {
|
|
38
|
+
try {
|
|
39
|
+
const stub = fn({
|
|
40
|
+
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
41
|
+
qiCol: () => '""',
|
|
42
|
+
});
|
|
43
|
+
if (stub.type)
|
|
44
|
+
out[name] = stub.type;
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// ignore — schema generation is best-effort; runtime validates.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
function buildJoinBodyProps(dbTables, tableConf, conditionItemSchema) {
|
|
53
|
+
const props = {
|
|
54
|
+
joinMustExist: {},
|
|
55
|
+
joinMultiple: {},
|
|
56
|
+
joinGroup: {},
|
|
57
|
+
joinLeft: {},
|
|
58
|
+
};
|
|
59
|
+
for (const joinDef of tableConf.allowedReadJoins ?? []) {
|
|
60
|
+
const { joinSchema, alias, unique } = joinDef;
|
|
61
|
+
const joinTableConf = dbTables[joinSchema.tableName];
|
|
62
|
+
const joinSchemaFields = joinTableConf
|
|
63
|
+
? { ...readableFields(joinSchema, joinTableConf), ...computedFieldTypes(joinTableConf) }
|
|
64
|
+
: { ...joinSchema.fields };
|
|
65
|
+
// `extraFilters` are only honoured where the engine delegates to the join table's
|
|
66
|
+
// own `filters()` — buildJoinRefCondition, i.e. the unique:false paths. joinLeft
|
|
67
|
+
// builds its condition inline (buildLeftJoinClauses) and handles schema and computed
|
|
68
|
+
// fields only, so advertising extraFilters there would promise a silent no-op.
|
|
69
|
+
const joinFilterFields = { ...joinSchemaFields, ...(joinTableConf?.extraFilters ?? {}) };
|
|
70
|
+
const joinRefShape = {
|
|
71
|
+
filters: Type.Optional(filtersObject(joinFilterFields)),
|
|
72
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
|
|
73
|
+
};
|
|
74
|
+
if (unique) {
|
|
75
|
+
props.joinLeft[alias] = Type.Object({
|
|
76
|
+
filters: Type.Optional(filtersObject(joinSchemaFields)),
|
|
77
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
|
|
78
|
+
selection: Type.Optional(Type.String()),
|
|
79
|
+
});
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
props.joinMustExist[alias] = Type.Object(joinRefShape);
|
|
83
|
+
props.joinMultiple[alias] = Type.Object({
|
|
84
|
+
...joinRefShape,
|
|
85
|
+
selection: Type.Optional(Type.String()),
|
|
86
|
+
});
|
|
87
|
+
props.joinGroup[alias] = Type.Object({
|
|
88
|
+
aggregations: Type.Object({
|
|
89
|
+
by: Type.Optional(Type.String()),
|
|
90
|
+
distinctCount: Type.Optional(Type.Array(Type.String())),
|
|
91
|
+
min: Type.Optional(Type.Array(Type.String())),
|
|
92
|
+
max: Type.Optional(Type.Array(Type.String())),
|
|
93
|
+
sum: Type.Optional(Type.Array(Type.String())),
|
|
94
|
+
avg: Type.Optional(Type.Array(Type.String())),
|
|
95
|
+
count: Type.Optional(Type.Array(Type.String())),
|
|
96
|
+
}),
|
|
97
|
+
filters: Type.Optional(filtersObject(joinFilterFields)),
|
|
98
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return props;
|
|
102
|
+
}
|
|
103
|
+
/** Per-alias *response* shapes: the rows each join family contributes to the result. */
|
|
104
|
+
function buildJoinResponseProps(dbTables, tableConf) {
|
|
105
|
+
const joinMultipleProps = {};
|
|
106
|
+
const joinLeftProps = {};
|
|
107
|
+
const joinGroupProps = {};
|
|
108
|
+
for (const joinDef of tableConf.allowedReadJoins ?? []) {
|
|
109
|
+
const { joinSchema, alias, unique } = joinDef;
|
|
110
|
+
const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
|
|
111
|
+
if (unique) {
|
|
112
|
+
joinLeftProps[alias] = itemArray;
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
joinMultipleProps[alias] = itemArray;
|
|
116
|
+
joinGroupProps[alias] = JoinGroupResultItem;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { joinMultipleProps, joinLeftProps, joinGroupProps };
|
|
120
|
+
}
|
|
15
121
|
const JoinGroupResultItem = Type.Object({
|
|
16
122
|
sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
17
123
|
min: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
@@ -30,105 +136,28 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
30
136
|
method: methodEnum,
|
|
31
137
|
params: Type.Optional(Type.Array(Type.Any())),
|
|
32
138
|
});
|
|
33
|
-
|
|
34
|
-
const computedTypes = {};
|
|
35
|
-
if (tableConf.computedFields) {
|
|
36
|
-
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
37
|
-
// Cheap dry-run with a stub context to extract the declared `type`.
|
|
38
|
-
// The fn is pure-functional w.r.t. ctx — only `type` is read here.
|
|
39
|
-
try {
|
|
40
|
-
const stub = fn({
|
|
41
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
42
|
-
qiCol: () => '""',
|
|
43
|
-
});
|
|
44
|
-
if (stub.type)
|
|
45
|
-
computedTypes[name] = stub.type;
|
|
46
|
-
}
|
|
47
|
-
catch {
|
|
48
|
-
// ignore — Swagger schema generation is best-effort; runtime validates.
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
139
|
+
const computedTypes = computedFieldTypes(tableConf);
|
|
52
140
|
const filterFields = {
|
|
53
141
|
...readableFields(schema, tableConf),
|
|
54
142
|
...tableConf.extraFilters,
|
|
55
143
|
...computedTypes,
|
|
56
144
|
};
|
|
57
|
-
const filtersSchema = Type.Optional(
|
|
58
|
-
|
|
59
|
-
// - unique:false → joinMustExist / joinMultiple / joinGroup
|
|
60
|
-
// - unique:true → joinLeft
|
|
61
|
-
const joinMustExistProps = {};
|
|
62
|
-
const joinMultipleProps = {};
|
|
63
|
-
const joinGroupProps = {};
|
|
64
|
-
const joinLeftProps = {};
|
|
65
|
-
if (tableConf.allowedReadJoins) {
|
|
66
|
-
for (const joinDef of tableConf.allowedReadJoins) {
|
|
67
|
-
const { joinSchema, alias, unique } = joinDef;
|
|
68
|
-
const joinTableConf = dbTables[joinSchema.tableName];
|
|
69
|
-
const joinComputedTypes = {};
|
|
70
|
-
if (joinTableConf?.computedFields) {
|
|
71
|
-
for (const [name, fn] of Object.entries(joinTableConf.computedFields)) {
|
|
72
|
-
try {
|
|
73
|
-
const stub = fn({
|
|
74
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
75
|
-
qiCol: () => '""',
|
|
76
|
-
});
|
|
77
|
-
if (stub.type)
|
|
78
|
-
joinComputedTypes[name] = stub.type;
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// best-effort
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
const joinFilterFields = joinTableConf
|
|
86
|
-
? { ...readableFields(joinSchema, joinTableConf), ...joinTableConf.extraFilters, ...joinComputedTypes }
|
|
87
|
-
: { ...joinSchema.fields };
|
|
88
|
-
const joinRefShape = {
|
|
89
|
-
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
90
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
91
|
-
};
|
|
92
|
-
const joinFetchShape = {
|
|
93
|
-
...joinRefShape,
|
|
94
|
-
selection: Type.Optional(Type.String()),
|
|
95
|
-
};
|
|
96
|
-
if (unique) {
|
|
97
|
-
joinLeftProps[alias] = Type.Object(joinFetchShape);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
joinMustExistProps[alias] = Type.Object(joinRefShape);
|
|
101
|
-
joinMultipleProps[alias] = Type.Object(joinFetchShape);
|
|
102
|
-
joinGroupProps[alias] = Type.Object({
|
|
103
|
-
aggregations: Type.Object({
|
|
104
|
-
by: Type.Optional(Type.String()),
|
|
105
|
-
distinctCount: Type.Optional(Type.Array(Type.String())),
|
|
106
|
-
min: Type.Optional(Type.Array(Type.String())),
|
|
107
|
-
max: Type.Optional(Type.Array(Type.String())),
|
|
108
|
-
sum: Type.Optional(Type.Array(Type.String())),
|
|
109
|
-
avg: Type.Optional(Type.Array(Type.String())),
|
|
110
|
-
count: Type.Optional(Type.Array(Type.String())),
|
|
111
|
-
}),
|
|
112
|
-
filters: Type.Optional(Type.Partial(Type.Object(joinFilterFields))),
|
|
113
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
}
|
|
145
|
+
const filtersSchema = Type.Optional(filtersObject(filterFields));
|
|
146
|
+
const joins = buildJoinBodyProps(dbTables, tableConf, conditionItemSchema);
|
|
118
147
|
const bodyProperties = {
|
|
119
148
|
filters: filtersSchema,
|
|
120
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
149
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
|
|
121
150
|
};
|
|
122
151
|
if (Object.keys(computedTypes).length > 0) {
|
|
123
152
|
bodyProperties.selectComputed = Type.Optional(Type.Array(Type.String()));
|
|
124
153
|
}
|
|
125
|
-
if (Object.keys(
|
|
126
|
-
bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(
|
|
127
|
-
bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(
|
|
128
|
-
bodyProperties.joinGroup = Type.Optional(Type.Partial(Type.Object(
|
|
154
|
+
if (Object.keys(joins.joinMustExist).length > 0) {
|
|
155
|
+
bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(joins.joinMustExist)));
|
|
156
|
+
bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(joins.joinMultiple)));
|
|
157
|
+
bodyProperties.joinGroup = Type.Optional(Type.Partial(Type.Object(joins.joinGroup)));
|
|
129
158
|
}
|
|
130
|
-
if (Object.keys(
|
|
131
|
-
bodyProperties.joinLeft = Type.Optional(Type.Partial(Type.Object(
|
|
159
|
+
if (Object.keys(joins.joinLeft).length > 0) {
|
|
160
|
+
bodyProperties.joinLeft = Type.Optional(Type.Partial(Type.Object(joins.joinLeft)));
|
|
132
161
|
}
|
|
133
162
|
return Type.Object(bodyProperties);
|
|
134
163
|
}
|
|
@@ -142,7 +171,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
142
171
|
*/
|
|
143
172
|
export function SearchTableQuery(maxItemsPerPage = DEFAULT_MAX_ITEMS_PER_PAGE) {
|
|
144
173
|
return Type.Object({
|
|
145
|
-
orderBy: Type.Optional(Type.String()),
|
|
174
|
+
orderBy: Type.Optional(Type.String({ maxLength: MAX_ORDER_BY_LENGTH })),
|
|
146
175
|
page: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
147
176
|
itemsPerPage: Type.Optional(Type.Integer({
|
|
148
177
|
minimum: 1,
|
|
@@ -162,42 +191,12 @@ export function SearchTableResponse(dbTables, tableName) {
|
|
|
162
191
|
const tableConf = dbTables[tableName];
|
|
163
192
|
// Computed fields are present in main rows only when explicitly listed in
|
|
164
193
|
// request body's selectComputed, so they're optional/Partial here.
|
|
165
|
-
const computedTypes =
|
|
166
|
-
if (tableConf.computedFields) {
|
|
167
|
-
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
168
|
-
try {
|
|
169
|
-
const stub = fn({
|
|
170
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
171
|
-
qiCol: () => '""',
|
|
172
|
-
});
|
|
173
|
-
if (stub.type)
|
|
174
|
-
computedTypes[name] = stub.type;
|
|
175
|
-
}
|
|
176
|
-
catch {
|
|
177
|
-
// best-effort
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
}
|
|
194
|
+
const computedTypes = computedFieldTypes(tableConf);
|
|
181
195
|
const mainReadable = readableFields(tableConf.Schema, tableConf);
|
|
182
196
|
const mainItem = Object.keys(computedTypes).length > 0
|
|
183
197
|
? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
|
|
184
198
|
: Type.Partial(Type.Object(mainReadable));
|
|
185
|
-
const joinMultipleProps =
|
|
186
|
-
const joinLeftProps = {};
|
|
187
|
-
const joinGroupProps = {};
|
|
188
|
-
if (tableConf.allowedReadJoins) {
|
|
189
|
-
for (const joinDef of tableConf.allowedReadJoins) {
|
|
190
|
-
const { joinSchema, alias, unique } = joinDef;
|
|
191
|
-
const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
|
|
192
|
-
if (unique) {
|
|
193
|
-
joinLeftProps[alias] = itemArray;
|
|
194
|
-
}
|
|
195
|
-
else {
|
|
196
|
-
joinMultipleProps[alias] = itemArray;
|
|
197
|
-
joinGroupProps[alias] = JoinGroupResultItem;
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
199
|
+
const { joinMultipleProps, joinLeftProps, joinGroupProps } = buildJoinResponseProps(dbTables, tableConf);
|
|
201
200
|
const responseProps = {
|
|
202
201
|
table: Type.String(),
|
|
203
202
|
main: Type.Array(mainItem),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAE9C,OAAO,EAAqB,WAAW,EAAE,MAAM,SAAS,CAAC;AACzD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAU5D,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,eAAe,EACf,SAAS,EACT,gBAAgB,EAChB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,SAAS,EACT,YAAY,EACZ,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,gBAAgB,EAChB,gBAAgB,EAChB,QAAQ,
|
|
1
|
+
{"version":3,"file":"sql-api.d.ts","sourceRoot":"","sources":["../../src/lib/sql-api.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConditionBuilder, type ConditionValueOrUndefined } from 'node-condition-builder';
|
|
2
|
-
import type
|
|
2
|
+
import { type TSchema, type TObject } from '@sinclair/typebox';
|
|
3
3
|
import type { SchemaDefinition, JoinDefinition, TableFilterFn, ITable } from '../types.js';
|
|
4
4
|
type ExtraProps<EF> = EF extends TObject<infer P> ? P : EF extends Record<string, TSchema> ? EF : Record<string, never>;
|
|
5
5
|
export declare function exportTableInfo<F extends Record<string, TSchema>, EF extends TObject | Record<string, TSchema> = Record<never, TSchema>>(Schema: SchemaDefinition<F>, extraFilters?: EF, extendedCondition?: (condition: ConditionBuilder, filters: {
|
|
@@ -14,6 +14,21 @@ export interface BuildRelationOptions {
|
|
|
14
14
|
alias?: string;
|
|
15
15
|
selection?: string;
|
|
16
16
|
unique?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Allowlist of the target table's fields reachable through this relation.
|
|
19
|
+
*
|
|
20
|
+
* The relation is declared against a schema narrowed to these fields, so every read
|
|
21
|
+
* surface — `selection`, `filters`, `conditions`, `orderBy`, aggregations, and the
|
|
22
|
+
* generated request/response schemas — rejects anything outside the list with
|
|
23
|
+
* `400 Unknown field`. Use it to expose a table broadly while keeping some of its
|
|
24
|
+
* columns out of one relation, without hiding them from the table's own routes the way
|
|
25
|
+
* `readExclude` would.
|
|
26
|
+
*
|
|
27
|
+
* Fail-closed: a column added to the table later is not reachable through the relation
|
|
28
|
+
* until it is added here. Must include the join field, and is not allowed on a write
|
|
29
|
+
* join.
|
|
30
|
+
*/
|
|
31
|
+
fields?: string[];
|
|
17
32
|
}
|
|
18
33
|
export declare function buildRelation<M extends Record<string, TSchema>, J extends Record<string, TSchema>>(mainSchema: SchemaDefinition<M>, mainField: string & keyof M | (string & keyof M)[], joinSchema: SchemaDefinition<J>, joinField: string & keyof J, options?: BuildRelationOptions): JoinDefinition;
|
|
19
34
|
export declare function buildUpsertRule<F extends Record<string, TSchema>>(schema: SchemaDefinition<F>, columns: (string & keyof F)[]): [SchemaDefinition, string[]];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"table-helpers.d.ts","sourceRoot":"","sources":["../../src/lib/table-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,yBAAyB,EAAiC,MAAM,wBAAwB,CAAC;AACzH,OAAO,EAAQ,KAAK,OAAO,EAAE,KAAK,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,KAAK,EACV,gBAAgB,EAChB,cAAc,EACd,aAAa,EACb,MAAM,EACP,MAAM,aAAa,CAAC;AAarB,KAAK,UAAU,CAAC,EAAE,IAAI,EAAE,SAAS,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;AAExH,wBAAgB,eAAe,CAC7B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,EAAE,SAAS,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,EAErE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,YAAY,GAAE,EAAa,EAC3B,iBAAiB,CAAC,EAAE,CAClB,SAAS,EAAE,gBAAgB,EAC3B,OAAO,EAAE;KAAG,CAAC,IAAI,MAAM,CAAC,GAAG,MAAM,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,yBAAyB;CAAE,KAC3E,IAAI,GACR;IAAE,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,EAAE,aAAa,CAAC;IAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAsChG;AAED,wBAAgB,WAAW,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC3D,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAChB,MAAM,CAAC,CAAC,CAAC,CAOX;AAqFD,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;;;;;;;;;;OAaG;IACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAgDD,wBAAgB,aAAa,CAC3B,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACjC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjC,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,GAAG,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,EAClD,UAAU,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC/B,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC,EAC3B,OAAO,CAAC,EAAE,oBAAoB,GAC7B,cAAc,CAWhB;AAED,wBAAgB,eAAe,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/D,MAAM,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC,EAAE,GAC5B,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAE9B;AAED,wBAAgB,gBAAgB,CAC9B,GAAG,KAAK,EAAE,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,EAAE,GACvC,GAAG,CAAC,gBAAgB,EAAE,MAAM,EAAE,CAAC,CAEjC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ConditionBuilder } from 'node-condition-builder';
|
|
2
|
+
import { Type } from '@sinclair/typebox';
|
|
2
3
|
import { getDialect } from './dialect.js';
|
|
3
4
|
// Quote an identifier using the given ConditionBuilder dialect (or the global
|
|
4
5
|
// default when not provided). Needed so that DB columns with uppercase letters
|
|
@@ -46,10 +47,27 @@ export function exportTableInfo(Schema, extraFilters = {}, extendedCondition) {
|
|
|
46
47
|
export function defineTable(config) {
|
|
47
48
|
validateAliasUniqueness(config.allowedReadJoins, 'allowedReadJoins');
|
|
48
49
|
validateAliasUniqueness(config.allowedWriteJoins, 'allowedWriteJoins');
|
|
50
|
+
validateWriteJoinsUnrestricted(config.allowedWriteJoins);
|
|
49
51
|
validateComputedFields(config);
|
|
50
52
|
validateReadExclude(config);
|
|
51
53
|
return config;
|
|
52
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* `fields` narrows a relation for reading; it has no meaning on a write join and would be
|
|
57
|
+
* actively harmful there. Write paths resolve the secondary's upsert rule by looking the
|
|
58
|
+
* schema object up in `upsertMap` **by identity**, and a narrowed relation carries a copy —
|
|
59
|
+
* the lookup would miss and the upsert would silently degrade to a plain insert. The
|
|
60
|
+
* narrowed schema would also drop columns the caller legitimately sends.
|
|
61
|
+
*/
|
|
62
|
+
function validateWriteJoinsUnrestricted(joins) {
|
|
63
|
+
for (const join of joins ?? []) {
|
|
64
|
+
if (join.fields) {
|
|
65
|
+
throw new Error(`defineTable: relation '${join.alias}' in allowedWriteJoins declares 'fields'. ` +
|
|
66
|
+
`The fields allowlist restricts reading only — remove it, and use a separate ` +
|
|
67
|
+
`buildRelation for the read side if that relation is also in allowedReadJoins.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
53
71
|
function validateReadExclude(config) {
|
|
54
72
|
const excluded = config.readExclude;
|
|
55
73
|
if (!excluded?.length)
|
|
@@ -97,14 +115,50 @@ function validateAliasUniqueness(joins, label) {
|
|
|
97
115
|
seen.add(j.alias);
|
|
98
116
|
}
|
|
99
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* Copy of `schema` exposing only `fields`, in schema declaration order.
|
|
120
|
+
*
|
|
121
|
+
* `tableName`, `col` and `colMap` are carried over untouched: the engine resolves the
|
|
122
|
+
* target's configuration through `dbTables[joinSchema.tableName]`, so a narrowed relation
|
|
123
|
+
* must still answer to its real table name, and column mapping must keep working for the
|
|
124
|
+
* fields that survive.
|
|
125
|
+
*/
|
|
126
|
+
function narrowSchemaToFields(schema, fields, joinField) {
|
|
127
|
+
const declared = Object.keys(schema.fields);
|
|
128
|
+
for (const field of fields) {
|
|
129
|
+
if (!declared.includes(field)) {
|
|
130
|
+
throw new Error(`buildRelation: 'fields' entry '${field}' is not a field of schema ` +
|
|
131
|
+
`'${schema.tableName}'. Available: ${declared.join(', ')}.`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
// The join field is what ties the fetched rows back to the main result set. Dropping it
|
|
135
|
+
// from the projection would hand the caller rows it cannot match to anything.
|
|
136
|
+
if (!fields.includes(joinField)) {
|
|
137
|
+
throw new Error(`buildRelation: 'fields' must include the join field '${joinField}' on ` +
|
|
138
|
+
`'${schema.tableName}' — it is what correlates the joined rows with the main ones.`);
|
|
139
|
+
}
|
|
140
|
+
const picked = {};
|
|
141
|
+
for (const field of declared) {
|
|
142
|
+
if (fields.includes(field))
|
|
143
|
+
picked[field] = schema.fields[field];
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
...schema,
|
|
147
|
+
fields: picked,
|
|
148
|
+
validation: Type.Object(picked),
|
|
149
|
+
partialValidation: Type.Partial(Type.Object(picked)),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
100
152
|
export function buildRelation(mainSchema, mainField, joinSchema, joinField, options) {
|
|
153
|
+
const fields = options?.fields;
|
|
101
154
|
return {
|
|
102
|
-
joinSchema,
|
|
155
|
+
joinSchema: fields ? narrowSchemaToFields(joinSchema, fields, joinField) : joinSchema,
|
|
103
156
|
joinField,
|
|
104
157
|
mainField,
|
|
105
158
|
alias: options?.alias ?? joinSchema.tableName,
|
|
106
159
|
selection: options?.selection ?? '*',
|
|
107
160
|
unique: options?.unique ?? false,
|
|
161
|
+
...(fields ? { fields: [...fields] } : {}),
|
|
108
162
|
};
|
|
109
163
|
}
|
|
110
164
|
export function buildUpsertRule(schema, columns) {
|
package/dist/lib/tenant.d.ts
CHANGED
|
@@ -4,8 +4,34 @@ import { ConditionBuilder, type ConditionValue, type ConditionValueOrUndefined }
|
|
|
4
4
|
import type { ITable, SqlApiPluginOptions, TenantId, TenantScope, TenantScopeIndirect, TenantContext } from '../types.js';
|
|
5
5
|
export declare function resolveTenant(options: SqlApiPluginOptions, tableConf: ITable, request: FastifyRequest): Promise<TenantContext | undefined>;
|
|
6
6
|
export declare function buildTenantCondition(db: QueryClient, scope: TenantScope, tenantIds: TenantId[], ownTable: string): ConditionBuilder;
|
|
7
|
+
/**
|
|
8
|
+
* Re-target a tenant context onto a related table: the caller's tenant ids paired with that
|
|
9
|
+
* table's OWN scope. A child table declares its own `tenantScope`, which may differ in shape
|
|
10
|
+
* from the main table's, so a write to it must be checked against its own column/through-FK.
|
|
11
|
+
*
|
|
12
|
+
* Returns undefined for the two cases that must stay unscoped — no tenant context (admin) and
|
|
13
|
+
* a table that declares no scope — mirroring `appendJoinTenantScope` on the read side.
|
|
14
|
+
*/
|
|
15
|
+
export declare function tenantForTable(tenant: TenantContext | undefined, tableConf: ITable | undefined): TenantContext | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Tenant guard as a self-contained boolean fragment, for the positions where the scope cannot
|
|
18
|
+
* be expressed as an extra INNER JOIN: a child DELETE, a LEFT JOIN's ON clause, and the
|
|
19
|
+
* correlated subquery of an aggregation. A direct scope compares the table's own column; an
|
|
20
|
+
* indirect one tests the through table with a correlated EXISTS rather than a join.
|
|
21
|
+
*
|
|
22
|
+
* `tableRef` is the name the scoped rows go by in that statement — the table name in a DELETE,
|
|
23
|
+
* the join alias in an ON clause, the subquery alias inside a correlated SELECT. Returns
|
|
24
|
+
* undefined without a tenant context (admin).
|
|
25
|
+
*/
|
|
26
|
+
export declare function buildTenantRowGuard(db: QueryClient, tenant: TenantContext | undefined, tableRef: string): ConditionBuilder | undefined;
|
|
7
27
|
export declare function buildTenantJoin(db: QueryClient, scope: TenantScopeIndirect, mainTableName: string): string;
|
|
28
|
+
/** Writes the tenant value into a record according to its scope.
|
|
29
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
30
|
+
*/
|
|
8
31
|
export declare function injectTenantValue(record: Record<string, unknown>, scope: TenantScope, tenantIds: TenantId[]): void;
|
|
32
|
+
/** Checks that an indirect-scope FK really belongs to the tenant.
|
|
33
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
34
|
+
*/
|
|
9
35
|
export declare function validateTenantFK(db: QueryClient, scope: TenantScopeIndirect, tenantIds: TenantId[], fkValues: unknown[]): Promise<void>;
|
|
10
36
|
/**
|
|
11
37
|
* Build WHERE clause for DELETE with tenant filtering.
|
package/dist/lib/tenant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tenant.d.ts","sourceRoot":"","sources":["../../src/lib/tenant.ts"],"names":[],"mappings":"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"}
|
package/dist/lib/tenant.js
CHANGED
|
@@ -30,12 +30,55 @@ export function buildTenantCondition(db, scope, tenantIds, ownTable) {
|
|
|
30
30
|
cb.isIn(qualifier, tenantIds);
|
|
31
31
|
return cb;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Re-target a tenant context onto a related table: the caller's tenant ids paired with that
|
|
35
|
+
* table's OWN scope. A child table declares its own `tenantScope`, which may differ in shape
|
|
36
|
+
* from the main table's, so a write to it must be checked against its own column/through-FK.
|
|
37
|
+
*
|
|
38
|
+
* Returns undefined for the two cases that must stay unscoped — no tenant context (admin) and
|
|
39
|
+
* a table that declares no scope — mirroring `appendJoinTenantScope` on the read side.
|
|
40
|
+
*/
|
|
41
|
+
export function tenantForTable(tenant, tableConf) {
|
|
42
|
+
const scope = tableConf?.tenantScope;
|
|
43
|
+
if (!tenant || !scope)
|
|
44
|
+
return undefined;
|
|
45
|
+
return { ids: tenant.ids, scope };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Tenant guard as a self-contained boolean fragment, for the positions where the scope cannot
|
|
49
|
+
* be expressed as an extra INNER JOIN: a child DELETE, a LEFT JOIN's ON clause, and the
|
|
50
|
+
* correlated subquery of an aggregation. A direct scope compares the table's own column; an
|
|
51
|
+
* indirect one tests the through table with a correlated EXISTS rather than a join.
|
|
52
|
+
*
|
|
53
|
+
* `tableRef` is the name the scoped rows go by in that statement — the table name in a DELETE,
|
|
54
|
+
* the join alias in an ON clause, the subquery alias inside a correlated SELECT. Returns
|
|
55
|
+
* undefined without a tenant context (admin).
|
|
56
|
+
*/
|
|
57
|
+
export function buildTenantRowGuard(db, tenant, tableRef) {
|
|
58
|
+
if (!tenant)
|
|
59
|
+
return undefined;
|
|
60
|
+
const cb = new ConditionBuilder('AND', db.cbDialect);
|
|
61
|
+
if (!isIndirect(tenant.scope)) {
|
|
62
|
+
cb.append(buildTenantCondition(db, tenant.scope, tenant.ids, tableRef));
|
|
63
|
+
return cb;
|
|
64
|
+
}
|
|
65
|
+
const { through, column } = tenant.scope;
|
|
66
|
+
const throughRef = db.qi(through.schema.tableName);
|
|
67
|
+
const markers = tenant.ids.map(() => '?').join(', ');
|
|
68
|
+
cb.raw(`EXISTS (SELECT 1 FROM ${throughRef} WHERE ` +
|
|
69
|
+
`${throughRef}.${db.qi(through.foreignField)} = ${db.qi(tableRef)}.${db.qi(through.localField)} AND ` +
|
|
70
|
+
`${throughRef}.${db.qi(column)} IN (${markers}))`, tenant.ids);
|
|
71
|
+
return cb;
|
|
72
|
+
}
|
|
33
73
|
export function buildTenantJoin(db, scope, mainTableName) {
|
|
34
74
|
const throughTable = scope.through.schema.tableName;
|
|
35
75
|
const localField = scope.through.localField;
|
|
36
76
|
const foreignField = scope.through.foreignField;
|
|
37
77
|
return `INNER JOIN ${db.qi(throughTable)} ON ${db.qi(mainTableName)}.${db.qi(localField)} = ${db.qi(throughTable)}.${db.qi(foreignField)}`;
|
|
38
78
|
}
|
|
79
|
+
/** Writes the tenant value into a record according to its scope.
|
|
80
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
81
|
+
*/
|
|
39
82
|
export function injectTenantValue(record, scope, tenantIds) {
|
|
40
83
|
if (isIndirect(scope))
|
|
41
84
|
return;
|
|
@@ -52,6 +95,9 @@ export function injectTenantValue(record, scope, tenantIds) {
|
|
|
52
95
|
}
|
|
53
96
|
throw httpError(400, 'Ambiguous tenant: specify the tenant value');
|
|
54
97
|
}
|
|
98
|
+
/** Checks that an indirect-scope FK really belongs to the tenant.
|
|
99
|
+
* @testonly Exported only so unit tests can exercise it directly.
|
|
100
|
+
*/
|
|
55
101
|
export async function validateTenantFK(db, scope, tenantIds, fkValues) {
|
|
56
102
|
if (!fkValues.length)
|
|
57
103
|
return;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
2
2
|
import type { ITable, SqlApiPluginOptions, DbTables, TableOperation } from '../../types.js';
|
|
3
|
-
type RequestHook = (request: FastifyRequest, reply: FastifyReply) => Promise<void | FastifyReply>;
|
|
4
|
-
export declare function mergeOnRequests(options: SqlApiPluginOptions, tableConf: ITable): RequestHook[];
|
|
5
3
|
export declare function buildWriteDescription(action: string, tableName: string, tableConf: ITable): string;
|
|
6
4
|
interface RouteSchema {
|
|
7
5
|
body?: any;
|
|
@@ -31,11 +29,6 @@ export interface AutoRouteSpec {
|
|
|
31
29
|
* the registrar will reply.status(successStatus).send(returnValue). */
|
|
32
30
|
handle: (fastify: FastifyInstance, tableName: string, tableConf: ITable, request: FastifyRequest, reply: FastifyReply) => Promise<unknown>;
|
|
33
31
|
}
|
|
34
|
-
/**
|
|
35
|
-
* Iterates over every table in `options.DbTables` and registers one Fastify route for it
|
|
36
|
-
* following `spec`. Centralizes onRequest hook merging, tags, schema response wrapping,
|
|
37
|
-
* default reply when handler returns a value.
|
|
38
|
-
*/
|
|
39
32
|
export declare function registerForAllTables(fastify: FastifyInstance, options: SqlApiPluginOptions, spec: AutoRouteSpec): Promise<void>;
|
|
40
33
|
export {};
|
|
41
34
|
//# sourceMappingURL=route-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"route-helpers.d.ts","sourceRoot":"","sources":["../../../src/routes/auto/route-helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAG7E,OAAO,KAAK,EAAE,MAAM,EAAE,mBAAmB,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAQ5F,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAQlG;AAED,UAAU,WAAW;IAEnB,IAAI,CAAC,EAAE,GAAG,CAAC;IAEX,MAAM,CAAC,EAAE,GAAG,CAAC;IAEb,WAAW,CAAC,EAAE,GAAG,CAAC;IAElB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,iGAAiG;IACjG,SAAS,EAAE,cAAc,CAAC;IAC1B;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC;IAC1C,gFAAgF;IAChF,GAAG,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,CAAC;IACnC,aAAa,EAAE,GAAG,GAAG,GAAG,CAAC;IACzB,wIAAwI;IACxI,OAAO,EAAE,CAAC,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,WAAW,CAAC;IACnF,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,KAAK,MAAM,CAAC;IACpF;2EACuE;IACvE,MAAM,EAAE,CACN,OAAO,EAAE,eAAe,EACxB,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,cAAc,EACvB,KAAK,EAAE,YAAY,KAChB,OAAO,CAAC,OAAO,CAAC,CAAC;CACvB;AA6DD,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE,mBAAmB,EAC5B,IAAI,EAAE,aAAa,GAClB,OAAO,CAAC,IAAI,CAAC,CAuBf"}
|