uql-orm 0.80.0 → 0.82.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/README.md +3 -3
- package/dist/browser/querier/httpQuerier.d.ts +2 -2
- package/dist/browser/querier/httpQuerier.js +2 -1
- package/dist/browser/type/clientQuerier.d.ts +2 -2
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +9 -8
- package/dist/bunSql/bunSql.util.js +2 -1
- package/dist/dialect/abstractSqlDialect.d.ts +15 -5
- package/dist/dialect/abstractSqlDialect.js +109 -40
- package/dist/dialect/operators.d.ts +7 -1
- package/dist/dialect/operators.js +13 -1
- package/dist/dialect/vectorSqlDialect.d.ts +2 -0
- package/dist/dialect/vectorSqlDialect.js +4 -0
- package/dist/entity/metadata/definition.d.ts +1 -2
- package/dist/entity/metadata/definition.js +37 -39
- package/dist/http/handler.js +5 -4
- package/dist/http/query.d.ts +1 -1
- package/dist/http/query.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/migrate/acquireQuerierForMigrations.js +2 -1
- package/dist/migrate/assertCliConfig.js +7 -6
- package/dist/migrate/bin.js +0 -0
- package/dist/migrate/builder/expressions.d.ts +2 -0
- package/dist/migrate/builder/expressions.js +26 -1
- package/dist/migrate/cli-config.js +5 -4
- package/dist/migrate/cli.js +1 -1
- package/dist/migrate/codegen/entityCodeGenerator.js +2 -2
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +3 -2
- package/dist/migrate/codegen/indexDecoratorSource.js +5 -23
- package/dist/migrate/ddl/indexDdl.js +4 -3
- package/dist/migrate/ddl/mssqlTableDdl.d.ts +4 -4
- package/dist/migrate/ddl/mssqlTableDdl.js +20 -14
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +2 -2
- package/dist/migrate/ddl/mysqlIndexDdl.js +12 -10
- package/dist/migrate/ddl/pgIndexDdl.d.ts +2 -1
- package/dist/migrate/ddl/pgIndexDdl.js +10 -7
- package/dist/migrate/ddl/sqliteIndexDdl.js +2 -1
- package/dist/migrate/ddl/tableDdl.d.ts +3 -2
- package/dist/migrate/ddl/tableDdl.js +11 -8
- package/dist/migrate/drift/driftDetector.d.ts +4 -5
- package/dist/migrate/drift/driftDetector.js +21 -21
- package/dist/migrate/generator/definitionToNode.d.ts +1 -1
- package/dist/migrate/generator/definitionToNode.js +9 -20
- package/dist/migrate/generator/mongoCommand.js +2 -1
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +17 -24
- package/dist/migrate/index.d.ts +2 -1
- package/dist/migrate/index.js +1 -0
- package/dist/migrate/indexPredicate.js +2 -1
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +5 -7
- package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +13 -19
- package/dist/migrate/introspection/baseSqlIntrospector.js +7 -18
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +5 -4
- package/dist/migrate/introspection/mssqlIntrospector.js +2 -1
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +15 -5
- package/dist/migrate/introspection/mysqlIntrospector.js +32 -4
- package/dist/migrate/introspection/postgresIntrospector.d.ts +29 -21
- package/dist/migrate/introspection/postgresIntrospector.js +63 -46
- package/dist/migrate/introspection/sqliteIntrospector.js +11 -9
- package/dist/migrate/migrationTarget.js +2 -1
- package/dist/migrate/migrator.d.ts +5 -0
- package/dist/migrate/migrator.js +35 -45
- package/dist/migrate/schemaChange.d.ts +18 -0
- package/dist/migrate/schemaChange.js +37 -0
- package/dist/migrate/schemaGenerator.d.ts +13 -14
- package/dist/migrate/schemaGenerator.js +88 -181
- package/dist/migrate/triggerSql.d.ts +1 -1
- package/dist/migrate/triggerSql.js +77 -61
- package/dist/mongo/mongoDialect.d.ts +1 -3
- package/dist/mongo/mongoDialect.js +9 -14
- package/dist/mongo/mongodbQuerier.js +3 -7
- package/dist/querier/abstractQuerier.d.ts +9 -4
- package/dist/querier/abstractQuerier.js +26 -19
- package/dist/querier/abstractQuerierPool.d.ts +3 -3
- package/dist/querier/abstractSqlQuerier.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.js +1 -1
- package/dist/querier/abstractSqlQuerierPool.d.ts +2 -2
- package/dist/querier/queryError.d.ts +2 -2
- package/dist/schema/indexDifferences.d.ts +22 -6
- package/dist/schema/indexDifferences.js +23 -8
- package/dist/schema/matchByKey.d.ts +10 -0
- package/dist/schema/matchByKey.js +18 -0
- package/dist/schema/schemaAST.d.ts +6 -2
- package/dist/schema/schemaAST.js +7 -3
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +17 -11
- package/dist/schema/schemaASTDiffer.d.ts +2 -3
- package/dist/schema/schemaASTDiffer.js +15 -36
- package/dist/schema/types.d.ts +14 -15
- package/dist/type/dialect.d.ts +23 -1
- package/dist/type/entity.d.ts +15 -11
- package/dist/type/logger.d.ts +2 -2
- package/dist/type/migration.d.ts +32 -50
- package/dist/type/querier.d.ts +3 -3
- package/dist/type/query.d.ts +3 -13
- package/dist/type/queryAggregate.d.ts +4 -10
- package/dist/type/queryRaw.d.ts +17 -3
- package/dist/type/queryRaw.js +2 -1
- package/dist/type/queryWhere.d.ts +7 -7
- package/dist/type/universalQuerier.d.ts +3 -3
- package/dist/type/vector.d.ts +2 -1
- package/dist/type/vector.js +2 -1
- package/dist/util/ddlExpression.util.d.ts +5 -1
- package/dist/util/ddlExpression.util.js +6 -2
- package/dist/util/dialect.util.d.ts +13 -5
- package/dist/util/dialect.util.js +28 -20
- package/dist/util/field.util.d.ts +4 -4
- package/dist/util/field.util.js +10 -2
- package/dist/util/hook.util.d.ts +1 -1
- package/dist/util/hook.util.js +8 -1
- package/dist/util/index.d.ts +1 -0
- package/dist/util/index.js +1 -0
- package/dist/util/logger.d.ts +3 -3
- package/dist/util/object.util.js +3 -2
- package/dist/util/raw.d.ts +6 -7
- package/dist/util/raw.js +10 -12
- package/dist/util/sqlLiteral.js +3 -2
- package/dist/util/triggerWrite.d.ts +15 -0
- package/dist/util/triggerWrite.js +20 -0
- package/package.json +1 -1
- package/skills/uql-orm/SKILL.md +3 -3
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, } from '../type/index.js';
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
|
-
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
|
+
import { asSelectMap, assertNonNegativeInteger, assertWhere, definedEntries, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textSortOf, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
6
|
import { kindOf, UqlUsageError } from '../util/uqlError.js';
|
|
7
7
|
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
8
8
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
9
|
-
import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
|
|
9
|
+
import { betweenBounds, COMPARE_OPS, EQUALITY_OPS, GROUP_OPS, groupClauses, inOperands, isGroupOp, isOrderedOp, isVectorQuery, LIKE_OPS, namesRows, ORDERED_OPS, VECTOR_QUERY_KEY_SET, whereOperators, } from './operators.js';
|
|
10
10
|
import { SqlQueryContext } from './queryContext.js';
|
|
11
11
|
import { groupPathField, NO_JOINS, aggregateColumnField, resolveGroupJoins, resolveQueryJoins, relationSortTerms, resolveSortableJoin, } from './queryJoins.js';
|
|
12
12
|
import { resolveVectorCast } from './vectorCast.js';
|
|
@@ -34,6 +34,13 @@ function orRefuse(value, refusal) {
|
|
|
34
34
|
}
|
|
35
35
|
return value;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Whether an assignment builds on what each row before it left: `$inc`, `$mul`, `$push`. A set-based
|
|
39
|
+
* `UPDATE` writes a target once however many rows of its set match it, so it would apply one of them.
|
|
40
|
+
*/
|
|
41
|
+
function accumulates(value) {
|
|
42
|
+
return isFieldUpdateOp(value) || (isJsonUpdateOp(value) && value.$push !== undefined);
|
|
43
|
+
}
|
|
37
44
|
/** Conditions joined by `AND`, parenthesized where there is more than one. */
|
|
38
45
|
function conjunction(parts) {
|
|
39
46
|
return parts.length > 1 ? `(${parts.join(' AND ')})` : parts.join('');
|
|
@@ -178,7 +185,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
178
185
|
returningIdExpression(meta) {
|
|
179
186
|
const [idKey] = meta.ids;
|
|
180
187
|
return meta.ids.length === 1
|
|
181
|
-
? `${this.returnedRowPrefix}${this.
|
|
188
|
+
? `${this.returnedRowPrefix}${this.escapedColumnName(meta, idKey)} ${this.escapeId('id')}`
|
|
182
189
|
: '';
|
|
183
190
|
}
|
|
184
191
|
search(ctx, entity, q = {}, opts = {}, joins = NO_JOINS, order) {
|
|
@@ -423,7 +430,7 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
423
430
|
join.relation.references.forEach((reference, index) => {
|
|
424
431
|
if (index > 0)
|
|
425
432
|
ctx.append(' AND ');
|
|
426
|
-
const foreign = this.
|
|
433
|
+
const foreign = this.escapedColumnName(join.meta, reference.foreign);
|
|
427
434
|
// Two calls rather than one over a union: the parent is either another join's entity or the
|
|
428
435
|
// queried one, and their metadata types have nothing in common.
|
|
429
436
|
const local = this.escapeId(join.parent ? this.columnOf(join.parent.meta, reference.local) : this.columnOf(meta, reference.local));
|
|
@@ -1254,30 +1261,99 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1254
1261
|
const meta = getMeta(entity);
|
|
1255
1262
|
const [filledPayload] = fillOnFields(meta, payload, 'onUpdate');
|
|
1256
1263
|
const keys = filterFieldKeys(meta, filledPayload, 'onUpdate');
|
|
1257
|
-
|
|
1258
|
-
ctx.append(`UPDATE ${tableName} SET `);
|
|
1264
|
+
ctx.append(`UPDATE ${this.escapedTableName(meta)} SET `);
|
|
1259
1265
|
for (let i = 0; i < keys.length; i++) {
|
|
1260
1266
|
if (i > 0) {
|
|
1261
1267
|
ctx.append(', ');
|
|
1262
1268
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1269
|
+
this.appendAssignment(ctx, meta, keys[i], filledPayload[keys[i]]);
|
|
1270
|
+
}
|
|
1271
|
+
this.search(ctx, entity, q, opts);
|
|
1272
|
+
}
|
|
1273
|
+
/** `col = value`, a JSON operator or a `$inc`/`$mul` spelled as the engine's. */
|
|
1274
|
+
appendAssignment(ctx, meta, key, value) {
|
|
1275
|
+
const field = meta.fields[key];
|
|
1276
|
+
const escapedCol = this.escapedColumnName(meta, key);
|
|
1277
|
+
if (isJsonUpdateOp(value)) {
|
|
1278
|
+
this.formatJsonUpdate(ctx, escapedCol, value, field);
|
|
1279
|
+
}
|
|
1280
|
+
else if (isFieldUpdateOp(value)) {
|
|
1281
|
+
const [op, operand] = fieldUpdateOf(key, value);
|
|
1282
|
+
ctx.append(`${escapedCol} = COALESCE(${escapedCol}, 0) ${SQL_ARITHMETIC[op]} `);
|
|
1283
|
+
ctx.addValue(operand);
|
|
1284
|
+
}
|
|
1285
|
+
else {
|
|
1286
|
+
ctx.append(`${escapedCol} = `);
|
|
1287
|
+
this.formatPersistableValue(ctx, field, value);
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* A write in a trigger's body. Only the columns it names, none filled in JavaScript - that would bake
|
|
1292
|
+
* one value into the trigger - no id read back, and no entity filter, which a request resolves and a
|
|
1293
|
+
* trigger has none of. `rows` is the `FROM` a set-based engine's body reads its rows through.
|
|
1294
|
+
*/
|
|
1295
|
+
triggerWrite(ctx, write, rows) {
|
|
1296
|
+
const meta = getMeta(write.entity);
|
|
1297
|
+
const table = this.escapedTableName(meta);
|
|
1298
|
+
if (write.kind === 'insert') {
|
|
1299
|
+
const unfilled = definedEntries(meta.fields).find(([key, field]) => field.onInsert !== undefined && field.defaultValue === undefined && write.row[key] === undefined);
|
|
1300
|
+
if (unfilled) {
|
|
1301
|
+
throw new UqlUsageError(`'${meta.name}.${unfilled[0]}' is filled on insert by uql, which a trigger does not run: ` +
|
|
1302
|
+
'name it, or give it a defaultValue');
|
|
1274
1303
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1304
|
+
const entries = this.writtenEntries(meta, write.row);
|
|
1305
|
+
const columns = entries.map(([key]) => this.escapedColumnName(meta, key)).join(', ');
|
|
1306
|
+
const [open, close] = rows ? ['SELECT ', ` ${rows};`] : ['VALUES (', ');'];
|
|
1307
|
+
ctx.append(`INSERT INTO ${table} (${columns}) ${open}`);
|
|
1308
|
+
entries.forEach(([key, value], i) => {
|
|
1309
|
+
if (i > 0) {
|
|
1310
|
+
ctx.append(', ');
|
|
1311
|
+
}
|
|
1312
|
+
this.formatPersistableValue(ctx, meta.fields[key], value);
|
|
1313
|
+
});
|
|
1314
|
+
ctx.append(close);
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
assertWhere(meta, write.where);
|
|
1318
|
+
if (!namesRows(write.where)) {
|
|
1319
|
+
throw new UqlUsageError(`a trigger's ${write.kind} over '${meta.name}' names no rows, so it would address every one`);
|
|
1320
|
+
}
|
|
1321
|
+
if (write.kind === 'update') {
|
|
1322
|
+
const entries = this.writtenEntries(meta, write.set);
|
|
1323
|
+
const accumulating = rows && entries.find(([, value]) => accumulates(value));
|
|
1324
|
+
if (accumulating) {
|
|
1325
|
+
throw new UqlUsageError(`'${meta.name}.${accumulating[0]}' cannot accumulate per row in a trigger fired once per statement, ` +
|
|
1326
|
+
'which applies $inc, $mul and $push once');
|
|
1278
1327
|
}
|
|
1328
|
+
ctx.append(`UPDATE ${table} SET `);
|
|
1329
|
+
entries.forEach(([key, value], i) => {
|
|
1330
|
+
if (i > 0) {
|
|
1331
|
+
ctx.append(', ');
|
|
1332
|
+
}
|
|
1333
|
+
this.appendAssignment(ctx, meta, key, value);
|
|
1334
|
+
});
|
|
1279
1335
|
}
|
|
1280
|
-
|
|
1336
|
+
else {
|
|
1337
|
+
ctx.append(`DELETE FROM ${table}`);
|
|
1338
|
+
}
|
|
1339
|
+
if (rows) {
|
|
1340
|
+
ctx.append(` ${rows}`);
|
|
1341
|
+
}
|
|
1342
|
+
// Qualified by the table: on a set-based engine `inserted` holds the same column names.
|
|
1343
|
+
this.renderWhere(ctx, write.entity, write.where, { escapedPrefix: `${table}.` });
|
|
1344
|
+
ctx.append(';');
|
|
1345
|
+
}
|
|
1346
|
+
/** The values a write names, refusing one the entity has no column for. */
|
|
1347
|
+
writtenEntries(meta, values) {
|
|
1348
|
+
const entries = Object.entries(values).filter(([, value]) => value !== undefined);
|
|
1349
|
+
const unknown = entries.find(([key]) => !meta.fields[key]);
|
|
1350
|
+
if (unknown) {
|
|
1351
|
+
throw new UqlUsageError(`'${meta.name}' has no field '${unknown[0]}' for a trigger to write`);
|
|
1352
|
+
}
|
|
1353
|
+
if (!entries.length) {
|
|
1354
|
+
throw new UqlUsageError(`a trigger's write to '${meta.name}' names no field`);
|
|
1355
|
+
}
|
|
1356
|
+
return entries;
|
|
1281
1357
|
}
|
|
1282
1358
|
/**
|
|
1283
1359
|
* `INSERT ... ON CONFLICT ... DO UPDATE/NOTHING RETURNING`. The assignments are built before the insert
|
|
@@ -1312,23 +1388,18 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1312
1388
|
return fields
|
|
1313
1389
|
.filter((col) => !conflictPaths[col])
|
|
1314
1390
|
.map((col) => {
|
|
1315
|
-
const
|
|
1316
|
-
const columnName = this.resolveColumnName(col, field);
|
|
1391
|
+
const column = this.escapedColumnName(meta, col);
|
|
1317
1392
|
if (Object.hasOwn(sample, col)) {
|
|
1318
|
-
return `${
|
|
1393
|
+
return `${column} = ${callback(column)}`;
|
|
1319
1394
|
}
|
|
1320
|
-
const text = this.buildFragment(ctx, (fragmentCtx) => this.formatPersistableValue(fragmentCtx,
|
|
1321
|
-
return `${
|
|
1395
|
+
const text = this.buildFragment(ctx, (fragmentCtx) => this.formatPersistableValue(fragmentCtx, meta.fields[col], filledPayload[col]));
|
|
1396
|
+
return `${column} = ${text}`;
|
|
1322
1397
|
})
|
|
1323
1398
|
.join(', ');
|
|
1324
1399
|
}
|
|
1325
1400
|
getUpsertConflictPathsStr(meta, conflictPaths) {
|
|
1326
1401
|
return getKeys(conflictPaths)
|
|
1327
|
-
.map((key) =>
|
|
1328
|
-
const field = meta.fields[key];
|
|
1329
|
-
const columnName = this.resolveColumnName(key, field);
|
|
1330
|
-
return this.escapeId(columnName);
|
|
1331
|
-
})
|
|
1402
|
+
.map((key) => this.escapedColumnName(meta, key))
|
|
1332
1403
|
.join(', ');
|
|
1333
1404
|
}
|
|
1334
1405
|
delete(ctx, entity, q, opts = {}) {
|
|
@@ -1337,10 +1408,8 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1337
1408
|
// Soft-delete (stamp only live rows) unless `hardDelete` is requested or the entity has no
|
|
1338
1409
|
// soft-delete field (e.g. a cascade onto a non-soft-deletable child).
|
|
1339
1410
|
if (!opts.hardDelete && meta.softDelete) {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
ctx.append(`UPDATE ${tableName} SET ${this.escapeId(columnName)} = `);
|
|
1343
|
-
this.formatPersistableValue(ctx, field, getSoftDeleteValue(field));
|
|
1411
|
+
ctx.append(`UPDATE ${tableName} SET `);
|
|
1412
|
+
this.appendAssignment(ctx, meta, meta.softDelete, getSoftDeleteValue(fieldOf(meta, meta.softDelete)));
|
|
1344
1413
|
this.search(ctx, entity, q, opts);
|
|
1345
1414
|
return;
|
|
1346
1415
|
}
|
|
@@ -1537,19 +1606,19 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
1537
1606
|
jsonDiffers(elem, operand) {
|
|
1538
1607
|
return `${elem} <> ${operand}`;
|
|
1539
1608
|
}
|
|
1540
|
-
compileDdl(sql, entity) {
|
|
1609
|
+
compileDdl(sql, entity, { rows, escapedPrefix, operand } = {}) {
|
|
1541
1610
|
const ctx = this.createContext({ inlineValues: true });
|
|
1542
1611
|
if (sql instanceof QueryRaw) {
|
|
1543
|
-
sql.render({ ctx, dialect: this, prefix: '', escapedPrefix: '', entity });
|
|
1612
|
+
sql.render({ ctx, dialect: this, prefix: '', escapedPrefix: escapedPrefix ?? '', entity, rows });
|
|
1544
1613
|
}
|
|
1545
1614
|
else if (entity) {
|
|
1546
|
-
this.renderWhere(ctx, entity, sql, { clause: false });
|
|
1615
|
+
this.renderWhere(ctx, entity, sql, { clause: false, escapedPrefix, operand });
|
|
1547
1616
|
}
|
|
1548
1617
|
else {
|
|
1549
|
-
throw new
|
|
1618
|
+
throw new UqlUsageError('a predicate compiles against the entity it is written for, and none was given');
|
|
1550
1619
|
}
|
|
1551
1620
|
if (ctx.values.length) {
|
|
1552
|
-
throw new
|
|
1621
|
+
throw new UqlUsageError(`DDL has no placeholder to bind a value into, and this SQL left one bound: ${ctx.sql}`);
|
|
1553
1622
|
}
|
|
1554
1623
|
return ctx.sql;
|
|
1555
1624
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
|
|
1
|
+
import { type QueryGroupOp, type QueryOrderedOp, type QueryVectorQuery, type QueryWhere, type QueryWhereArray, type QueryWhereFieldOp } from '../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* How each grouping operator renders: the operator joining its clauses, and whether the group is
|
|
4
4
|
* negated (`$not` is `NOT (a AND b)`). Total over {@link QueryGroupOp}.
|
|
@@ -29,6 +29,12 @@ export declare function isGroupOp(key: string): key is QueryGroupOp;
|
|
|
29
29
|
* refuse the same payload rather than one throwing and the other failing further in.
|
|
30
30
|
*/
|
|
31
31
|
export declare function groupClauses<E>(key: QueryGroupOp, val: QueryWhereArray<E> | undefined): QueryWhereArray<E>;
|
|
32
|
+
/**
|
|
33
|
+
* Whether a `$where` names any rows, as the WHERE it renders would: an `undefined` value, an empty
|
|
34
|
+
* operator map and a group none of whose clauses names one all render nothing, which a write reads as
|
|
35
|
+
* the whole table.
|
|
36
|
+
*/
|
|
37
|
+
export declare function namesRows<E>(where: QueryWhere<E> | undefined): boolean;
|
|
32
38
|
/**
|
|
33
39
|
* A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
|
|
34
40
|
* anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
1
|
+
import { QueryRaw, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
2
2
|
import { isOperatorMap } from '../util/dialect.util.js';
|
|
3
|
+
import { hasKeys, isWhereMap, someKey } from '../util/object.util.js';
|
|
3
4
|
import { kindOf, UqlUsageError } from '../util/uqlError.js';
|
|
4
5
|
/**
|
|
5
6
|
* How each grouping operator renders: the operator joining its clauses, and whether the group is
|
|
@@ -26,6 +27,17 @@ export function groupClauses(key, val) {
|
|
|
26
27
|
}
|
|
27
28
|
return val ?? [];
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Whether a `$where` names any rows, as the WHERE it renders would: an `undefined` value, an empty
|
|
32
|
+
* operator map and a group none of whose clauses names one all render nothing, which a write reads as
|
|
33
|
+
* the whole table.
|
|
34
|
+
*/
|
|
35
|
+
export function namesRows(where) {
|
|
36
|
+
return (where !== undefined &&
|
|
37
|
+
someKey(where, (key) => isGroupOp(key)
|
|
38
|
+
? groupClauses(key, where[key]).some((clause) => clause instanceof QueryRaw || namesRows(clause))
|
|
39
|
+
: where[key] !== undefined && !(isWhereMap(where[key]) && !hasKeys(where[key]))));
|
|
40
|
+
}
|
|
29
41
|
/**
|
|
30
42
|
* A WHERE value as the operators it applies: an array is `$in`, an operator map its own entries, and
|
|
31
43
|
* anything else `$eq`. Each key is checked here, once, refused as `refusal` where it is no operator.
|
|
@@ -30,6 +30,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
30
30
|
readonly vectorMetrics: ReadonlyMap<VectorDistance, VectorMetric>;
|
|
31
31
|
/** Whether this engine has a vector index: the one a metric's `index` names. */
|
|
32
32
|
hasVectorIndex(): boolean;
|
|
33
|
+
/** The distance a vector index built for `metric`, as {@link vectorMetrics} names it for an index, measures. */
|
|
34
|
+
indexedDistance(metric: string | undefined): VectorDistance | undefined;
|
|
33
35
|
/** Quotes an identifier; supplied by the SQL dialect built on top of this layer. */
|
|
34
36
|
abstract escapeId(val: string | undefined, forbidQualified?: boolean, addDot?: boolean): string;
|
|
35
37
|
/** What a distance expression reads, for a `$sort` and a `$near` alike. */
|
|
@@ -45,6 +45,10 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
45
45
|
hasVectorIndex() {
|
|
46
46
|
return [...this.vectorMetrics.values()].some((metric) => metric.index);
|
|
47
47
|
}
|
|
48
|
+
/** The distance a vector index built for `metric`, as {@link vectorMetrics} names it for an index, measures. */
|
|
49
|
+
indexedDistance(metric) {
|
|
50
|
+
return [...this.vectorMetrics].find(([, { index }]) => index === metric)?.[0];
|
|
51
|
+
}
|
|
48
52
|
/** What a distance expression reads, for a `$sort` and a `$near` alike. */
|
|
49
53
|
resolveVectorDistance(meta, key, search) {
|
|
50
54
|
const field = meta.fields[key];
|
|
@@ -45,7 +45,7 @@ export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
|
|
|
45
45
|
/** The relation `key` names, for a caller that took `key` from the metadata itself. */
|
|
46
46
|
export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
|
|
47
47
|
/** A row of `E` as far as reading its key goes: a record or a write, whatever its values. */
|
|
48
|
-
type KeyedRow<E> = {
|
|
48
|
+
export type KeyedRow<E> = {
|
|
49
49
|
readonly [K in keyof E]?: unknown;
|
|
50
50
|
};
|
|
51
51
|
/** Whether the row names every column of its primary key, `0` and `''` included. */
|
|
@@ -67,4 +67,3 @@ export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
|
67
67
|
* schema build constrains and a junction joins by, settling the relations holding them first.
|
|
68
68
|
*/
|
|
69
69
|
export declare function foreignKeysOf<E>(meta: EntityMeta<E>): RelationMeta[];
|
|
70
|
-
export {};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
|
-
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
-
import { entitySql, entityWhere, fieldOptionConflict,
|
|
2
|
+
import { fieldKeys, isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
+
import { entitySql, entityWhere, fieldOptionConflict, hasKeys, isToManyRelation, memberRefs, fulltextWeights, normalizeIndexColumn, definedEntries, whereWith, } from '../../util/index.js';
|
|
4
|
+
import { UqlUsageError } from '../../util/uqlError.js';
|
|
4
5
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
5
6
|
/**
|
|
6
7
|
* A map held on `globalThis` through the global symbol registry, so a single one survives multiple
|
|
@@ -22,18 +23,18 @@ export function defineField(entity, key, opts = {}) {
|
|
|
22
23
|
// A relation aggregate reads as a correlated subquery, which no engine accepts in a generated column:
|
|
23
24
|
// keeping one on the row takes the triggers a write fires, which are not built yet.
|
|
24
25
|
if (opts.stored && sql instanceof RelationAggregate) {
|
|
25
|
-
throw new
|
|
26
|
+
throw new UqlUsageError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
|
|
26
27
|
"engine keeps in a generated column. Drop 'stored' to have it read on each query.");
|
|
27
28
|
}
|
|
28
29
|
// A stored computed column is a real column and still needs a type; only an inlined one is exempt,
|
|
29
30
|
// its expression being spliced in rather than declared.
|
|
30
31
|
if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
|
|
31
|
-
throw new
|
|
32
|
+
throw new UqlUsageError(`'${entity.name}.${key}' needs a 'type'. Declare it - '@Field({ type: String })' - or point the field ` +
|
|
32
33
|
"at another entity with 'references', which resolves the column type from its primary key.");
|
|
33
34
|
}
|
|
34
35
|
const conflict = fieldOptionConflict(opts);
|
|
35
36
|
if (conflict) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new UqlUsageError(`'${entity.name}.${key}' ${conflict}.`);
|
|
37
38
|
}
|
|
38
39
|
const fieldKey = key;
|
|
39
40
|
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
@@ -75,10 +76,10 @@ function keyMap() {
|
|
|
75
76
|
const KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
76
77
|
function addRelation(entity, key, registration) {
|
|
77
78
|
if (!registration.entity) {
|
|
78
|
-
throw new
|
|
79
|
+
throw new UqlUsageError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
79
80
|
}
|
|
80
81
|
if (registration.through && registration.references) {
|
|
81
|
-
throw new
|
|
82
|
+
throw new UqlUsageError(`'${entity.name}.${key}' joins through a junction, whose column referencing each side is the join; ` +
|
|
82
83
|
"'references' pairs the declaring entity's columns with the target's instead.");
|
|
83
84
|
}
|
|
84
85
|
const meta = ensureWritableMeta(entity);
|
|
@@ -120,10 +121,10 @@ export function defineTrigger(entity, trigger) {
|
|
|
120
121
|
const meta = ensureWritableMeta(entity);
|
|
121
122
|
// The type already refuses an empty map; this is the same answer for plain JavaScript.
|
|
122
123
|
if (typeof trigger.run !== 'function' && !hasKeys(trigger.run)) {
|
|
123
|
-
throw new
|
|
124
|
+
throw new UqlUsageError(`'${entity.name}' has a trigger whose body names at least one engine to run on`);
|
|
124
125
|
}
|
|
125
126
|
if (trigger.name && meta.triggers?.some((it) => it.name === trigger.name)) {
|
|
126
|
-
throw new
|
|
127
|
+
throw new UqlUsageError(`'${entity.name}' already has a trigger named '${trigger.name}'`);
|
|
127
128
|
}
|
|
128
129
|
(meta.triggers ??= []).push({ ...trigger, of: trigger.of?.(memberRefs()).map((ref) => ref.key) });
|
|
129
130
|
return meta;
|
|
@@ -131,12 +132,12 @@ export function defineTrigger(entity, trigger) {
|
|
|
131
132
|
export function defineFilter(entity, name, opts) {
|
|
132
133
|
const meta = ensureWritableMeta(entity);
|
|
133
134
|
if (name === SOFT_DELETE_FILTER) {
|
|
134
|
-
throw
|
|
135
|
+
throw new UqlUsageError(`'${entity.name}' filter name '${SOFT_DELETE_FILTER}' is reserved; it is auto-registered from @Field({ softDelete })`);
|
|
135
136
|
}
|
|
136
137
|
// Widened for a caller the types did not reach, which is the only one this can refuse.
|
|
137
138
|
const { security, onMissing } = opts;
|
|
138
139
|
if (security && onMissing === 'skip') {
|
|
139
|
-
throw
|
|
140
|
+
throw new UqlUsageError(`'${entity.name}' security filter '${name}' cannot use onMissing: 'skip' (it must fail closed)`);
|
|
140
141
|
}
|
|
141
142
|
(meta.filters ??= {})[name] = opts;
|
|
142
143
|
return meta;
|
|
@@ -173,7 +174,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
173
174
|
// statement builds and then fails at the database. `schema` is the way to say it.
|
|
174
175
|
if (opts.name?.includes('.')) {
|
|
175
176
|
const [schema, ...rest] = opts.name.split('.');
|
|
176
|
-
throw new
|
|
177
|
+
throw new UqlUsageError(`'${entity.name}' has a dotted name '${opts.name}'. Name the schema separately as ` +
|
|
177
178
|
`{ schema: '${schema}', name: '${rest.join('.')}' }.`);
|
|
178
179
|
}
|
|
179
180
|
const meta = ensureWritableMeta(entity);
|
|
@@ -201,7 +202,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
201
202
|
defineFilter(entity, name, filter);
|
|
202
203
|
}
|
|
203
204
|
if (!hasKeys(meta.fields)) {
|
|
204
|
-
throw
|
|
205
|
+
throw new UqlUsageError(`'${entity.name}' must have fields`);
|
|
205
206
|
}
|
|
206
207
|
// A later call composes onto the entity, so a name is only ever set, and `derivedName` records that
|
|
207
208
|
// the class name stood in, which is what a naming strategy derives from.
|
|
@@ -221,21 +222,18 @@ export function defineEntity(entity, opts = {}) {
|
|
|
221
222
|
// Derive soft-delete from the (inheritance-merged) fields, so own and inherited markers are handled
|
|
222
223
|
// uniformly. Exactly one field may be marked; it auto-registers the built-in `softDelete` read
|
|
223
224
|
// filter (a reserved name - see defineFilter - so it never clobbers a user filter).
|
|
224
|
-
const softDeleteKeys =
|
|
225
|
-
const softDelete = meta.fields[key]?.softDelete;
|
|
226
|
-
return softDelete !== undefined && softDelete !== false;
|
|
227
|
-
});
|
|
225
|
+
const softDeleteKeys = fieldKeys(meta, ({ softDelete }) => softDelete !== undefined && softDelete !== false);
|
|
228
226
|
if (softDeleteKeys.length > 1) {
|
|
229
|
-
throw
|
|
227
|
+
throw new UqlUsageError(`'${entity.name}' must have at most one field with 'softDelete'`);
|
|
230
228
|
}
|
|
231
229
|
if (softDeleteKeys.length) {
|
|
232
230
|
meta.softDelete = softDeleteKeys[0];
|
|
233
|
-
(meta.filters ??= {})[SOFT_DELETE_FILTER] = { where:
|
|
231
|
+
(meta.filters ??= {})[SOFT_DELETE_FILTER] = { where: whereWith(meta.softDelete, null), default: true };
|
|
234
232
|
}
|
|
235
233
|
// The optimistic lock, derived the same way and just as singular: one row has one version.
|
|
236
|
-
const versionKeys =
|
|
234
|
+
const versionKeys = fieldKeys(meta, (field) => field.version);
|
|
237
235
|
if (versionKeys.length > 1) {
|
|
238
|
-
throw
|
|
236
|
+
throw new UqlUsageError(`'${entity.name}' must have at most one field with 'version'`);
|
|
239
237
|
}
|
|
240
238
|
if (versionKeys.length) {
|
|
241
239
|
meta.version = versionKeys[0];
|
|
@@ -246,7 +244,7 @@ export function defineEntity(entity, opts = {}) {
|
|
|
246
244
|
}
|
|
247
245
|
const ids = getIdKeys(meta);
|
|
248
246
|
if (!ids.length) {
|
|
249
|
-
throw
|
|
247
|
+
throw new UqlUsageError(`'${entity.name}' must have at least one id field (use @Id, defineId, or defineEntity({ fields: { ..., isId: true } }))`);
|
|
250
248
|
}
|
|
251
249
|
meta.ids = ids;
|
|
252
250
|
return meta;
|
|
@@ -262,7 +260,7 @@ export function assertSoleId(meta, what) {
|
|
|
262
260
|
if (ids.length === 1) {
|
|
263
261
|
return;
|
|
264
262
|
}
|
|
265
|
-
throw new
|
|
263
|
+
throw new UqlUsageError(ids.length
|
|
266
264
|
? `'${meta.entity.name}' has a composite primary key (${ids.join(', ')}), which ${what} does not support yet.`
|
|
267
265
|
: // An entity registered with `@Field` but no `@Entity` never ran the check in `defineEntity`.
|
|
268
266
|
`'${meta.entity.name}' has no primary key, which ${what} needs.`);
|
|
@@ -276,7 +274,7 @@ export function soleIdOf(meta, what) {
|
|
|
276
274
|
export function fieldOf(meta, key) {
|
|
277
275
|
const field = meta.fields[key];
|
|
278
276
|
if (!field) {
|
|
279
|
-
throw new
|
|
277
|
+
throw new UqlUsageError(`'${meta.entity.name}' has no field '${key}'`);
|
|
280
278
|
}
|
|
281
279
|
return field;
|
|
282
280
|
}
|
|
@@ -284,7 +282,7 @@ export function fieldOf(meta, key) {
|
|
|
284
282
|
export function relationOf(meta, key) {
|
|
285
283
|
const relation = meta.relations[key];
|
|
286
284
|
if (!relation) {
|
|
287
|
-
throw new
|
|
285
|
+
throw new UqlUsageError(`'${meta.entity.name}' has no relation '${key}'`);
|
|
288
286
|
}
|
|
289
287
|
return relation;
|
|
290
288
|
}
|
|
@@ -342,7 +340,7 @@ export function getMeta(entity) {
|
|
|
342
340
|
function registeredMeta(entity) {
|
|
343
341
|
const meta = metas.get(entity);
|
|
344
342
|
if (!meta) {
|
|
345
|
-
throw
|
|
343
|
+
throw new UqlUsageError(`'${entity.name}' is not an entity`);
|
|
346
344
|
}
|
|
347
345
|
return meta;
|
|
348
346
|
}
|
|
@@ -351,7 +349,7 @@ function fillRelations(meta) {
|
|
|
351
349
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
352
350
|
const references = settledReferences(at, meta, relKey, relation);
|
|
353
351
|
if (!references.length) {
|
|
354
|
-
throw new
|
|
352
|
+
throw new UqlUsageError(`${at} has no columns to join on.`);
|
|
355
353
|
}
|
|
356
354
|
if (!relation.through) {
|
|
357
355
|
assertJoins(at, meta, relation, references);
|
|
@@ -371,7 +369,7 @@ function settledReferences(at, meta, relKey, relOpts) {
|
|
|
371
369
|
if (typeof references === 'string') {
|
|
372
370
|
const target = ensureMeta(relOpts.entity());
|
|
373
371
|
if (mappedBy || isToManyRelation(relOpts) || target.ids.length > 1) {
|
|
374
|
-
throw new
|
|
372
|
+
throw new UqlUsageError(`${at} names one column, '${references}', which only a to-one holding a foreign key to a one-column key ` +
|
|
375
373
|
'can: pair the columns, [{ local, foreign }].');
|
|
376
374
|
}
|
|
377
375
|
relOpts.references = [{ local: references, foreign: soleIdOf(target, 'a foreign key') }];
|
|
@@ -383,7 +381,7 @@ function settledReferences(at, meta, relKey, relOpts) {
|
|
|
383
381
|
return fillInverseSide(at, meta, relOpts, mappedBy);
|
|
384
382
|
if (through)
|
|
385
383
|
return fillThrough(at, meta, relOpts, through);
|
|
386
|
-
throw new
|
|
384
|
+
throw new UqlUsageError(isToManyRelation(relOpts)
|
|
387
385
|
? `${at} is a to-many relation with no way to join: it needs 'mappedBy' (the member on the other side), ` +
|
|
388
386
|
"'through' (a junction entity), or 'references' (the columns)."
|
|
389
387
|
: `${at} needs 'references', the foreign key column it joins by, or 'mappedBy', the member on the other ` +
|
|
@@ -407,7 +405,7 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
407
405
|
const other = `'${relMeta.entity.name}.${mappedBy}'`;
|
|
408
406
|
if (relMeta.fields[mappedBy]) {
|
|
409
407
|
if (meta.ids.length > 1) {
|
|
410
|
-
throw new
|
|
408
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, one column, but the primary key of ` +
|
|
411
409
|
`'${meta.entity.name}' is composite (${meta.ids.join(', ')}). Map it by the relation on the other side ` +
|
|
412
410
|
'instead, which joins every column of the key.');
|
|
413
411
|
}
|
|
@@ -417,14 +415,14 @@ function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
|
417
415
|
}
|
|
418
416
|
const owner = relMeta.relations[mappedBy];
|
|
419
417
|
if (!owner) {
|
|
420
|
-
throw new
|
|
418
|
+
throw new UqlUsageError(`${at} is mapped by '${mappedBy}', which is neither a field nor a relation of '${relMeta.entity.name}'.`);
|
|
421
419
|
}
|
|
422
420
|
if (owner.mappedBy) {
|
|
423
|
-
throw new
|
|
421
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, an inverse side too, so neither owns the foreign key.`);
|
|
424
422
|
}
|
|
425
423
|
const ownerTarget = owner.entity();
|
|
426
424
|
if (!isA(meta.entity, ownerTarget)) {
|
|
427
|
-
throw new
|
|
425
|
+
throw new UqlUsageError(`${at} is mapped by ${other}, a relation to '${ownerTarget.name}', not to '${meta.entity.name}'.`);
|
|
428
426
|
}
|
|
429
427
|
const ownerReferences = settledReferences(other, relMeta, mappedBy, owner);
|
|
430
428
|
// Two different flips: a junction's pairs are the owner's group followed by ours, so the two groups
|
|
@@ -454,11 +452,11 @@ function assertJoins(at, meta, relOpts, pairs) {
|
|
|
454
452
|
const column = `'${side.meta.entity.name}.${key}'`;
|
|
455
453
|
const field = side.meta.fields[key];
|
|
456
454
|
if (!field || isInlinedExpression(field)) {
|
|
457
|
-
throw new
|
|
455
|
+
throw new UqlUsageError(`${at} joins ${column}, which is not a column: declare it with '@Field'.`);
|
|
458
456
|
}
|
|
459
457
|
const referenced = side.holds && pairs.length === 1 ? field.references?.() : undefined;
|
|
460
458
|
if (referenced && !isA(side.joins, referenced)) {
|
|
461
|
-
throw new
|
|
459
|
+
throw new UqlUsageError(`${at} joins ${column}, a foreign key to '${referenced.name}', not to '${side.joins.name}'.`);
|
|
462
460
|
}
|
|
463
461
|
}
|
|
464
462
|
}
|
|
@@ -491,7 +489,7 @@ export function foreignKeysOf(meta) {
|
|
|
491
489
|
if (!target.ids.length)
|
|
492
490
|
return [];
|
|
493
491
|
if (target.ids.length > 1) {
|
|
494
|
-
throw new
|
|
492
|
+
throw new UqlUsageError(`'${meta.entity.name}.${key}' cannot reference '${target.entity.name}', whose primary key is composite ` +
|
|
495
493
|
`(${target.ids.join(', ')}): a column points at one. Declare a column per key and pair each with it ` +
|
|
496
494
|
`in a '@ManyToOne' to '${target.entity.name}'.`);
|
|
497
495
|
}
|
|
@@ -509,11 +507,11 @@ function junctionReferences(at, junction, side) {
|
|
|
509
507
|
const declare = side.ids.length > 1
|
|
510
508
|
? `a column per key, paired in a '@ManyToOne' to '${side.entity.name}'`
|
|
511
509
|
: `'@Field({ references: () => ${side.entity.name} })'`;
|
|
512
|
-
throw new
|
|
510
|
+
throw new UqlUsageError(`${at} joins through '${junction.entity.name}', which has no column referencing ${referenced}: declare ${declare}.`);
|
|
513
511
|
}
|
|
514
512
|
if (others.length) {
|
|
515
513
|
const columns = [pair, ...others].map(({ local }) => `'${local}'`).join(' and ');
|
|
516
|
-
throw new
|
|
514
|
+
throw new UqlUsageError(`${at} joins through '${junction.entity.name}', where ${columns} each reference ${referenced}: a junction ` +
|
|
517
515
|
'needs exactly one column per key of each side.');
|
|
518
516
|
}
|
|
519
517
|
return { local: pair.local, foreign: key };
|
|
@@ -521,7 +519,7 @@ function junctionReferences(at, junction, side) {
|
|
|
521
519
|
}
|
|
522
520
|
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
523
521
|
function getIdKeys(meta) {
|
|
524
|
-
return
|
|
522
|
+
return fieldKeys(meta, (field) => field.isId);
|
|
525
523
|
}
|
|
526
524
|
/**
|
|
527
525
|
* Merges `ancestor` and its ancestors into `meta`, nearest first, draining an undecorated base's
|
package/dist/http/handler.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { withContext } from '../context/context.js';
|
|
2
2
|
import { getEntities, getMeta, soleIdOf } from '../entity/index.js';
|
|
3
|
-
import { whereIds } from '../util/dialect.util.js';
|
|
3
|
+
import { whereIds, whereWith } from '../util/dialect.util.js';
|
|
4
|
+
import { UqlUsageError } from '../util/uqlError.js';
|
|
4
5
|
import { entityPath, matchRoute } from './contract.js';
|
|
5
6
|
import { parseQueryParams } from './query.js';
|
|
6
7
|
/** `Company (crm.Company)`: the class, and the table it maps, which is what tells two apart. */
|
|
@@ -16,14 +17,14 @@ export function createRequestHandler(opts) {
|
|
|
16
17
|
entities = entities.filter((entity) => !exclude.includes(entity));
|
|
17
18
|
}
|
|
18
19
|
if (!entities.length) {
|
|
19
|
-
throw new
|
|
20
|
+
throw new UqlUsageError('no entities for the uql middleware');
|
|
20
21
|
}
|
|
21
22
|
// All of them at once, so fixing the first collision does not just reveal the next.
|
|
22
23
|
const byPath = Map.groupBy(entities, pathOf);
|
|
23
24
|
const collisions = [...byPath].filter(([, clashing]) => clashing.length > 1);
|
|
24
25
|
if (collisions.length) {
|
|
25
26
|
const lines = collisions.map(([path, clashing]) => ` /${path} <- ${clashing.map(tableOf).join(', ')}`);
|
|
26
|
-
throw new
|
|
27
|
+
throw new UqlUsageError(`every entity below shares a route with another, so all but the first are unreachable:\n${lines.join('\n')}\n` +
|
|
27
28
|
"A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
|
|
28
29
|
"pass an 'entityPath', or pass only one of them in 'include'.");
|
|
29
30
|
}
|
|
@@ -157,6 +158,6 @@ function ok(body) {
|
|
|
157
158
|
return { status: 200, body };
|
|
158
159
|
}
|
|
159
160
|
function buildIdQuery(meta, id, query) {
|
|
160
|
-
query.$where =
|
|
161
|
+
query.$where = whereWith(soleIdOf(meta, 'the HTTP handler'), id, query.$where);
|
|
161
162
|
return query;
|
|
162
163
|
}
|
package/dist/http/query.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { WireQuery } from '../type/index.js';
|
|
|
3
3
|
* Parse raw query-string entries (with JSON-stringified values) into a UQL query object.
|
|
4
4
|
* Symmetric counterpart of {@link stringifyQuery}. Only {@link ALLOWED_QUERY_KEYS} are honored.
|
|
5
5
|
*/
|
|
6
|
-
export declare function parseQueryParams(params?: Record<string, unknown>): WireQuery<
|
|
6
|
+
export declare function parseQueryParams<E = unknown>(params?: Record<string, unknown>): WireQuery<E>;
|
|
7
7
|
/**
|
|
8
8
|
* Serialize a UQL query object into a percent-encoded query string where object values
|
|
9
9
|
* are JSON-stringified. Symmetric counterpart of {@link parseQueryParams}.
|
package/dist/http/query.js
CHANGED
|
@@ -105,11 +105,11 @@ export function wireJson(value) {
|
|
|
105
105
|
return held;
|
|
106
106
|
}
|
|
107
107
|
if (RAW_VALUE in held) {
|
|
108
|
-
throw new
|
|
108
|
+
throw new UqlUsageError('raw SQL cannot travel over HTTP: what leaves the browser is JSON');
|
|
109
109
|
}
|
|
110
110
|
// A blob is a field value, so no type parameter reaches it: this is the only place it is caught.
|
|
111
111
|
if (held instanceof ArrayBuffer || ArrayBuffer.isView(held)) {
|
|
112
|
-
throw new
|
|
112
|
+
throw new UqlUsageError('binary cannot travel over HTTP: what leaves the browser is JSON');
|
|
113
113
|
}
|
|
114
114
|
return held;
|
|
115
115
|
});
|