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
|
@@ -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`. */
|
|
@@ -24,6 +24,100 @@ function readableFields(schema, tableConf) {
|
|
|
24
24
|
function filtersObject(fields) {
|
|
25
25
|
return Type.Partial(Type.Object(fields));
|
|
26
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
|
+
}
|
|
27
121
|
const JoinGroupResultItem = Type.Object({
|
|
28
122
|
sum: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
29
123
|
min: Type.Optional(Type.Record(Type.String(), Type.Any())),
|
|
@@ -42,113 +136,28 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
42
136
|
method: methodEnum,
|
|
43
137
|
params: Type.Optional(Type.Array(Type.Any())),
|
|
44
138
|
});
|
|
45
|
-
|
|
46
|
-
const computedTypes = {};
|
|
47
|
-
if (tableConf.computedFields) {
|
|
48
|
-
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
49
|
-
// Cheap dry-run with a stub context to extract the declared `type`.
|
|
50
|
-
// The fn is pure-functional w.r.t. ctx — only `type` is read here.
|
|
51
|
-
try {
|
|
52
|
-
const stub = fn({
|
|
53
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
54
|
-
qiCol: () => '""',
|
|
55
|
-
});
|
|
56
|
-
if (stub.type)
|
|
57
|
-
computedTypes[name] = stub.type;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// ignore — Swagger schema generation is best-effort; runtime validates.
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
}
|
|
139
|
+
const computedTypes = computedFieldTypes(tableConf);
|
|
64
140
|
const filterFields = {
|
|
65
141
|
...readableFields(schema, tableConf),
|
|
66
142
|
...tableConf.extraFilters,
|
|
67
143
|
...computedTypes,
|
|
68
144
|
};
|
|
69
145
|
const filtersSchema = Type.Optional(filtersObject(filterFields));
|
|
70
|
-
|
|
71
|
-
// - unique:false → joinMustExist / joinMultiple / joinGroup
|
|
72
|
-
// - unique:true → joinLeft
|
|
73
|
-
const joinMustExistProps = {};
|
|
74
|
-
const joinMultipleProps = {};
|
|
75
|
-
const joinGroupProps = {};
|
|
76
|
-
const joinLeftProps = {};
|
|
77
|
-
if (tableConf.allowedReadJoins) {
|
|
78
|
-
for (const joinDef of tableConf.allowedReadJoins) {
|
|
79
|
-
const { joinSchema, alias, unique } = joinDef;
|
|
80
|
-
const joinTableConf = dbTables[joinSchema.tableName];
|
|
81
|
-
const joinComputedTypes = {};
|
|
82
|
-
if (joinTableConf?.computedFields) {
|
|
83
|
-
for (const [name, fn] of Object.entries(joinTableConf.computedFields)) {
|
|
84
|
-
try {
|
|
85
|
-
const stub = fn({
|
|
86
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
87
|
-
qiCol: () => '""',
|
|
88
|
-
});
|
|
89
|
-
if (stub.type)
|
|
90
|
-
joinComputedTypes[name] = stub.type;
|
|
91
|
-
}
|
|
92
|
-
catch {
|
|
93
|
-
// best-effort
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
const joinSchemaFields = joinTableConf
|
|
98
|
-
? { ...readableFields(joinSchema, joinTableConf), ...joinComputedTypes }
|
|
99
|
-
: { ...joinSchema.fields };
|
|
100
|
-
// `extraFilters` are only honoured where the engine delegates to the join table's
|
|
101
|
-
// own `filters()` — buildJoinRefCondition, i.e. the unique:false paths. joinLeft
|
|
102
|
-
// builds its condition inline (buildLeftJoinClauses) and handles schema and computed
|
|
103
|
-
// fields only, so advertising extraFilters there would promise a silent no-op.
|
|
104
|
-
const joinFilterFields = { ...joinSchemaFields, ...(joinTableConf?.extraFilters ?? {}) };
|
|
105
|
-
const joinRefShape = {
|
|
106
|
-
filters: Type.Optional(filtersObject(joinFilterFields)),
|
|
107
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
108
|
-
};
|
|
109
|
-
if (unique) {
|
|
110
|
-
joinLeftProps[alias] = Type.Object({
|
|
111
|
-
filters: Type.Optional(filtersObject(joinSchemaFields)),
|
|
112
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
113
|
-
selection: Type.Optional(Type.String()),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
else {
|
|
117
|
-
joinMustExistProps[alias] = Type.Object(joinRefShape);
|
|
118
|
-
joinMultipleProps[alias] = Type.Object({
|
|
119
|
-
...joinRefShape,
|
|
120
|
-
selection: Type.Optional(Type.String()),
|
|
121
|
-
});
|
|
122
|
-
joinGroupProps[alias] = Type.Object({
|
|
123
|
-
aggregations: Type.Object({
|
|
124
|
-
by: Type.Optional(Type.String()),
|
|
125
|
-
distinctCount: Type.Optional(Type.Array(Type.String())),
|
|
126
|
-
min: Type.Optional(Type.Array(Type.String())),
|
|
127
|
-
max: Type.Optional(Type.Array(Type.String())),
|
|
128
|
-
sum: Type.Optional(Type.Array(Type.String())),
|
|
129
|
-
avg: Type.Optional(Type.Array(Type.String())),
|
|
130
|
-
count: Type.Optional(Type.Array(Type.String())),
|
|
131
|
-
}),
|
|
132
|
-
filters: Type.Optional(filtersObject(joinFilterFields)),
|
|
133
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
146
|
+
const joins = buildJoinBodyProps(dbTables, tableConf, conditionItemSchema);
|
|
138
147
|
const bodyProperties = {
|
|
139
148
|
filters: filtersSchema,
|
|
140
|
-
conditions: Type.Optional(Type.Array(conditionItemSchema)),
|
|
149
|
+
conditions: Type.Optional(Type.Array(conditionItemSchema, { maxItems: MAX_CONDITIONS })),
|
|
141
150
|
};
|
|
142
151
|
if (Object.keys(computedTypes).length > 0) {
|
|
143
152
|
bodyProperties.selectComputed = Type.Optional(Type.Array(Type.String()));
|
|
144
153
|
}
|
|
145
|
-
if (Object.keys(
|
|
146
|
-
bodyProperties.joinMustExist = Type.Optional(Type.Partial(Type.Object(
|
|
147
|
-
bodyProperties.joinMultiple = Type.Optional(Type.Partial(Type.Object(
|
|
148
|
-
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)));
|
|
149
158
|
}
|
|
150
|
-
if (Object.keys(
|
|
151
|
-
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)));
|
|
152
161
|
}
|
|
153
162
|
return Type.Object(bodyProperties);
|
|
154
163
|
}
|
|
@@ -162,7 +171,7 @@ export function SearchTableBodyPost(dbTables, tableName) {
|
|
|
162
171
|
*/
|
|
163
172
|
export function SearchTableQuery(maxItemsPerPage = DEFAULT_MAX_ITEMS_PER_PAGE) {
|
|
164
173
|
return Type.Object({
|
|
165
|
-
orderBy: Type.Optional(Type.String()),
|
|
174
|
+
orderBy: Type.Optional(Type.String({ maxLength: MAX_ORDER_BY_LENGTH })),
|
|
166
175
|
page: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
167
176
|
itemsPerPage: Type.Optional(Type.Integer({
|
|
168
177
|
minimum: 1,
|
|
@@ -182,42 +191,12 @@ export function SearchTableResponse(dbTables, tableName) {
|
|
|
182
191
|
const tableConf = dbTables[tableName];
|
|
183
192
|
// Computed fields are present in main rows only when explicitly listed in
|
|
184
193
|
// request body's selectComputed, so they're optional/Partial here.
|
|
185
|
-
const computedTypes =
|
|
186
|
-
if (tableConf.computedFields) {
|
|
187
|
-
for (const [name, fn] of Object.entries(tableConf.computedFields)) {
|
|
188
|
-
try {
|
|
189
|
-
const stub = fn({
|
|
190
|
-
db: { qi: (s) => s, dialectName: 'postgres' },
|
|
191
|
-
qiCol: () => '""',
|
|
192
|
-
});
|
|
193
|
-
if (stub.type)
|
|
194
|
-
computedTypes[name] = stub.type;
|
|
195
|
-
}
|
|
196
|
-
catch {
|
|
197
|
-
// best-effort
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
}
|
|
194
|
+
const computedTypes = computedFieldTypes(tableConf);
|
|
201
195
|
const mainReadable = readableFields(tableConf.Schema, tableConf);
|
|
202
196
|
const mainItem = Object.keys(computedTypes).length > 0
|
|
203
197
|
? Type.Partial(Type.Object({ ...mainReadable, ...computedTypes }))
|
|
204
198
|
: Type.Partial(Type.Object(mainReadable));
|
|
205
|
-
const joinMultipleProps =
|
|
206
|
-
const joinLeftProps = {};
|
|
207
|
-
const joinGroupProps = {};
|
|
208
|
-
if (tableConf.allowedReadJoins) {
|
|
209
|
-
for (const joinDef of tableConf.allowedReadJoins) {
|
|
210
|
-
const { joinSchema, alias, unique } = joinDef;
|
|
211
|
-
const itemArray = Type.Array(Type.Partial(Type.Object(readableFields(joinSchema, dbTables[joinSchema.tableName]))));
|
|
212
|
-
if (unique) {
|
|
213
|
-
joinLeftProps[alias] = itemArray;
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
joinMultipleProps[alias] = itemArray;
|
|
217
|
-
joinGroupProps[alias] = JoinGroupResultItem;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
199
|
+
const { joinMultipleProps, joinLeftProps, joinGroupProps } = buildJoinResponseProps(dbTables, tableConf);
|
|
221
200
|
const responseProps = {
|
|
222
201
|
table: Type.String(),
|
|
223
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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"}
|
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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ensureSqlApiDecorator } from '../../lib/sql-api-decorator.js';
|
|
2
2
|
import { isCompositePrimary } from '../../types.js';
|
|
3
|
-
|
|
3
|
+
function mergeOnRequests(options, tableConf) {
|
|
4
4
|
return [...(options.onRequests || []), ...(tableConf.onRequests || [])];
|
|
5
5
|
}
|
|
6
6
|
export function buildWriteDescription(action, tableName, tableConf) {
|
|
@@ -17,46 +17,62 @@ export function buildWriteDescription(action, tableName, tableConf) {
|
|
|
17
17
|
* following `spec`. Centralizes onRequest hook merging, tags, schema response wrapping,
|
|
18
18
|
* default reply when handler returns a value.
|
|
19
19
|
*/
|
|
20
|
+
/**
|
|
21
|
+
* Whether this table is addressable by the spec's route shape.
|
|
22
|
+
*
|
|
23
|
+
* A single-PK route cannot address a composite-PK table. Skipping is the default, but when
|
|
24
|
+
* the consumer explicitly listed the operation it is a configuration error: failing at
|
|
25
|
+
* startup beats a route that silently never exists.
|
|
26
|
+
*/
|
|
27
|
+
function isTableRoutable(tableName, tableConf, spec) {
|
|
28
|
+
// operations acts as a whitelist; when omitted every operation is exposed.
|
|
29
|
+
if (tableConf.operations && !tableConf.operations.includes(spec.operation))
|
|
30
|
+
return false;
|
|
31
|
+
if (!spec.singlePkOnly || !isCompositePrimary(tableConf.primary))
|
|
32
|
+
return true;
|
|
33
|
+
if (tableConf.operations?.includes(spec.operation)) {
|
|
34
|
+
throw new Error(`Table "${tableName}" has a composite primary key (${tableConf.primary.join(', ')}): ` +
|
|
35
|
+
`operation "${spec.operation}" addresses records by a single PK value and cannot be exposed. ` +
|
|
36
|
+
`Remove it from "operations", or handle the table via search/update or a custom route.`);
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Fastify route schema for one table.
|
|
42
|
+
*
|
|
43
|
+
* Only keys that are actually defined are included: Fastify checks for key *presence*
|
|
44
|
+
* (not an undefined value) to decide whether to emit FSTWRN001.
|
|
45
|
+
*/
|
|
46
|
+
function buildRouteSchema(tableName, tableConf, spec, schemas) {
|
|
47
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
48
|
+
const schema = {
|
|
49
|
+
tags: [`SqlAPI-${tableName}`],
|
|
50
|
+
summary: `${spec.summary} ${tableName}`,
|
|
51
|
+
description: spec.description(tableName, tableConf, schemas),
|
|
52
|
+
};
|
|
53
|
+
if (schemas.body !== undefined)
|
|
54
|
+
schema.body = schemas.body;
|
|
55
|
+
if (schemas.params !== undefined)
|
|
56
|
+
schema.params = schemas.params;
|
|
57
|
+
if (schemas.querystring !== undefined)
|
|
58
|
+
schema.querystring = schemas.querystring;
|
|
59
|
+
if (schemas.response !== undefined)
|
|
60
|
+
schema.response = { [spec.successStatus]: schemas.response };
|
|
61
|
+
return schema;
|
|
62
|
+
}
|
|
20
63
|
export async function registerForAllTables(fastify, options, spec) {
|
|
21
64
|
// Granular composition support: when a route plugin is registered without the
|
|
22
65
|
// main plugin, no ancestor has decorated `sqlApi` — create it in this scope.
|
|
23
66
|
ensureSqlApiDecorator(fastify, options);
|
|
24
67
|
const { DbTables } = options;
|
|
25
68
|
for (const [tableName, tableConf] of Object.entries(DbTables)) {
|
|
26
|
-
|
|
27
|
-
if (tableConf.operations && !tableConf.operations.includes(spec.operation))
|
|
28
|
-
continue;
|
|
29
|
-
// Single-PK routes cannot address composite-PK tables — skip them (default) or,
|
|
30
|
-
// when the consumer explicitly asked for the operation, fail loudly at startup.
|
|
31
|
-
if (spec.singlePkOnly && isCompositePrimary(tableConf.primary)) {
|
|
32
|
-
if (tableConf.operations?.includes(spec.operation)) {
|
|
33
|
-
throw new Error(`Table "${tableName}" has a composite primary key (${tableConf.primary.join(', ')}): ` +
|
|
34
|
-
`operation "${spec.operation}" addresses records by a single PK value and cannot be exposed. ` +
|
|
35
|
-
`Remove it from "operations", or handle the table via search/update or a custom route.`);
|
|
36
|
-
}
|
|
69
|
+
if (!isTableRoutable(tableName, tableConf, spec))
|
|
37
70
|
continue;
|
|
38
|
-
}
|
|
39
71
|
const schemas = spec.schemas(DbTables, tableName, tableConf);
|
|
40
|
-
// Only include schema keys that are actually defined: Fastify checks for key
|
|
41
|
-
// *presence* (not undefined value) to decide whether to emit FSTWRN001.
|
|
42
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
43
|
-
const schema = {
|
|
44
|
-
tags: [`SqlAPI-${tableName}`],
|
|
45
|
-
summary: `${spec.summary} ${tableName}`,
|
|
46
|
-
description: spec.description(tableName, tableConf, schemas),
|
|
47
|
-
};
|
|
48
|
-
if (schemas.body !== undefined)
|
|
49
|
-
schema.body = schemas.body;
|
|
50
|
-
if (schemas.params !== undefined)
|
|
51
|
-
schema.params = schemas.params;
|
|
52
|
-
if (schemas.querystring !== undefined)
|
|
53
|
-
schema.querystring = schemas.querystring;
|
|
54
|
-
if (schemas.response !== undefined)
|
|
55
|
-
schema.response = { [spec.successStatus]: schemas.response };
|
|
56
72
|
fastify.route({
|
|
57
73
|
method: spec.method,
|
|
58
74
|
url: spec.url(tableConf),
|
|
59
|
-
schema,
|
|
75
|
+
schema: buildRouteSchema(tableName, tableConf, spec, schemas),
|
|
60
76
|
onRequest: mergeOnRequests(options, tableConf),
|
|
61
77
|
handler: async (request, reply) => {
|
|
62
78
|
const result = await spec.handle(fastify, tableName, tableConf, request, reply);
|
|
@@ -15,7 +15,6 @@ export default async function searchRoutes(fastify, options) {
|
|
|
15
15
|
}),
|
|
16
16
|
summary: 'Search',
|
|
17
17
|
description: (name, _tc, schemas) => {
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
18
|
const body = schemas.body;
|
|
20
19
|
const multiAliases = Object.keys(body?.properties?.joinMultiple?.properties || {});
|
|
21
20
|
const leftAliases = Object.keys(body?.properties?.joinLeft?.properties || {});
|
package/dist/types/table.d.ts
CHANGED
|
@@ -30,7 +30,7 @@ export interface ITable<F extends Record<string, TSchema> = Record<string, TSche
|
|
|
30
30
|
extraFilters: Record<string, TSchema>;
|
|
31
31
|
/**
|
|
32
32
|
* Virtual fields produced by a SQL expression. Usable like schema fields in
|
|
33
|
-
* `filters`, `conditions` (non-dotted), `orderBy` (1-
|
|
33
|
+
* `filters`, `conditions` (non-dotted), `orderBy` (1-part),
|
|
34
34
|
* `computeMin/Max/Sum/Avg`, and (opt-in) in `selectComputed` for the main
|
|
35
35
|
* response. Each entry MUST declare `type` for Swagger and validation.
|
|
36
36
|
*
|
package/docs/GLOSSARY.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Glossary
|
|
2
|
+
|
|
3
|
+
One name per concept. Use these words in code, tests and docs — a second name for an
|
|
4
|
+
existing concept makes search fail and the concept get built twice.
|
|
5
|
+
|
|
6
|
+
| Term | Meaning |
|
|
7
|
+
|---|---|
|
|
8
|
+
| Table config | `defineTable()` result describing one table's routes, guards and joins. (not: model, entity) |
|
|
9
|
+
| Schema | TypeBox definition of a table's columns, generated from the DB. (not: model) |
|
|
10
|
+
| Relation | `buildRelation()` link between two schemas, identified by an alias. (not: association) |
|
|
11
|
+
| Alias | Request/response key naming one relation; defaults to the joined table name. |
|
|
12
|
+
| `joinMustExist` | Child→main 1:N filter via EXISTS; never returns child rows. |
|
|
13
|
+
| `joinMultiple` | Child→main 1:N side query; child rows land in `result.joinMultiple.<alias>`. |
|
|
14
|
+
| `joinGroup` | Child→main 1:N aggregations in `result.joinGroup.<alias>`. |
|
|
15
|
+
| `joinLeft` | Parent→main N:1 real LEFT JOIN; needs `unique: true` on the relation. |
|
|
16
|
+
| Secondaries | Child rows written in the same request as the main row. (not: nested, children) |
|
|
17
|
+
| Deletions | Child rows removed in the same request as the main row update. |
|
|
18
|
+
| Computed field | Server-side derived value resolved during search. (not: virtual, derived) |
|
|
19
|
+
| Extra filter | Non-column search filter declared per table. (not: custom filter) |
|
|
20
|
+
| Extended condition | Consumer-supplied SQL fragment merged into a search WHERE. |
|
|
21
|
+
| Dotted notation | `orderBy`/condition key counted in parts: 1-part `field`, 2-part `alias.field`, 3-part `alias.fn.field`. (not: parti) |
|
|
22
|
+
| Tenant scope | Row-level isolation: `direct` (own column) or `indirect` (via FK). |
|
|
23
|
+
| Engine | Dialect-agnostic function performing one operation. (not: service, handler) |
|
|
24
|
+
| Route | Fastify plugin exposing an engine over HTTP. (not: controller) |
|
|
25
|
+
| Dialect | Per-DB SQL differences (`postgres` \| `mysql`). |
|
|
26
|
+
| QueryClient | Connection wrapper the engines run SQL through. (not: db, connection) |
|
|
27
|
+
| Paginator | Request object carrying page and items-per-page. (not: pagination, pager) |
|
|
28
|
+
| Manifest | Runtime description of a deployment's tables, for agent clients. |
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# 0012. Repository rules are enforced by executable gates, not by documentation
|
|
2
|
+
|
|
3
|
+
- **Status**: accepted
|
|
4
|
+
- **Date**: 2026-08-01
|
|
5
|
+
|
|
6
|
+
## Context
|
|
7
|
+
|
|
8
|
+
Every session starts with an empty context window. An agent that cannot find existing code
|
|
9
|
+
rewrites it, invents a second name for an existing concept, and scatters logic across the
|
|
10
|
+
tree. Documentation does not prevent this: it is only read when someone remembers to read
|
|
11
|
+
it, and `CLAUDE.md` is weighted context, not configuration.
|
|
12
|
+
|
|
13
|
+
The repository had no ESLint, no Prettier, no git hooks and no CI beyond publish-on-tag.
|
|
14
|
+
Measured starting point: 2.50% duplicated lines (3.27% of tokens, 18 clones), naming drift
|
|
15
|
+
effectively zero, layering already clean, and `fastify-plugin` imported but absent from
|
|
16
|
+
`package.json` — a real packaging bug that no human review had caught.
|
|
17
|
+
|
|
18
|
+
## Decision
|
|
19
|
+
|
|
20
|
+
Anything that must not happen is a hook or a lint rule. Prose is reserved for what cannot
|
|
21
|
+
be executed.
|
|
22
|
+
|
|
23
|
+
Gates are **silent and zero-tolerance by default**; pre-existing debt is switched off and
|
|
24
|
+
surfaced by `npm run debt` (`lint:strict`, `dup:strict`, `depcruise:strict`,
|
|
25
|
+
`knip:strict`). npm scripts are the source of truth — git hooks only call them — so any
|
|
26
|
+
gate can be run without touching git history.
|
|
27
|
+
|
|
28
|
+
Rejected the ratchet-on-a-warning-count approach that was tried first: a run that always
|
|
29
|
+
prints 241 warnings trains everyone to ignore its output, and the number drifts upward
|
|
30
|
+
unnoticed. A rule is either enforced or explicitly deferred, never "warned about".
|
|
31
|
+
|
|
32
|
+
## Alternatives considered
|
|
33
|
+
|
|
34
|
+
- **Baseline thresholds with warnings** (`--max-warnings 241`) — rejected after
|
|
35
|
+
implementing it: noisy output is ignored output, and the boundary is invisible.
|
|
36
|
+
- **Barrel enforcement via `no-restricted-imports`** — rejected: the repo has one barrel,
|
|
37
|
+
`src/index.ts`, which is the npm surface. Blocking deep imports would break every
|
|
38
|
+
internal import and require restructuring into per-module barrels. `dependency-cruiser`
|
|
39
|
+
encodes the same boundaries without moving a single file.
|
|
40
|
+
- **Intent tags with a closed vocabulary, and `cspell`** — rejected: 64 files, coherent
|
|
41
|
+
naming, one maintainer. A half-maintained tag system is worse than none, and spellchecking
|
|
42
|
+
a 951-line README full of SQL identifiers is pure noise.
|
|
43
|
+
- **Session worklog** — rejected: without a compaction ritual nobody will run, an
|
|
44
|
+
append-only log becomes context noise within weeks.
|
|
45
|
+
|
|
46
|
+
## Consequences
|
|
47
|
+
|
|
48
|
+
- New violations fail immediately; the deferred list in `eslint.config.js` is the debt
|
|
49
|
+
ledger and may only shrink.
|
|
50
|
+
- `docs/INDEX.md` is generated and CI fails when it is stale, like a lockfile. It is
|
|
51
|
+
excluded from the npm package: publishing 204 internal declarations would invite
|
|
52
|
+
consumers to import what is not public API.
|
|
53
|
+
- Type-only import cycles through `src/types.ts` are deferred, not fixed. Verified they
|
|
54
|
+
are erased on emit — the compiled `dist/` graph has zero cycles.
|
|
55
|
+
- `knip` checks exports and types with no exclusions. The 16 symbols that were exported but
|
|
56
|
+
used only inside their own file lost the keyword; the 5 that only unit tests use are
|
|
57
|
+
tagged `@testonly`, which documents why they stay exported instead of hiding them.
|
|
58
|
+
- Maintainer scripts require Node ≥22.18 (native TypeScript type stripping). The published
|
|
59
|
+
package still supports Node ≥18.
|