@rebasepro/server-postgres 0.10.1-canary.d8d45b2 → 0.10.1-canary.ed8caed
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/dist/{ensure-collection-tables-CNlIONzj.js → ensure-collection-tables-BVvtkkRm.js} +10 -10
- package/dist/ensure-collection-tables-BVvtkkRm.js.map +1 -0
- package/dist/index.es.js +344 -119
- package/dist/index.es.js.map +1 -1
- package/dist/services/FetchService.d.ts +21 -8
- package/dist/services/PersistService.d.ts +12 -0
- package/dist/services/RelationService.d.ts +30 -0
- package/dist/services/nested-path.d.ts +59 -0
- package/dist/{src-B0v4IKaI.js → src-CBgtrPhJ.js} +8 -1
- package/dist/{src-B0v4IKaI.js.map → src-CBgtrPhJ.js.map} +1 -1
- package/dist/{src-DmsRg8MR.js → src-lcfUP4xg.js} +126 -76
- package/dist/src-lcfUP4xg.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +41 -0
- package/package.json +8 -9
- package/src/schema/doctor.ts +6 -6
- package/src/schema/generate-drizzle-schema-logic.ts +13 -9
- package/src/schema/generate-postgres-ddl-logic.ts +19 -12
- package/src/schema/introspect-db-inference.ts +13 -13
- package/src/schema/introspect-db-logic.ts +6 -6
- package/src/services/FetchService.ts +104 -114
- package/src/services/PersistService.ts +127 -85
- package/src/services/RelationService.ts +95 -6
- package/src/services/nested-path.ts +130 -0
- package/src/utils/drizzle-conditions.ts +143 -0
- package/dist/ensure-collection-tables-CNlIONzj.js.map +0 -1
- package/dist/src-DmsRg8MR.js.map +0 -1
package/dist/index.es.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createRequire as __createRequire } from "module";
|
|
|
2
2
|
import process from "process";
|
|
3
3
|
__createRequire(import.meta.url);
|
|
4
4
|
import { i as __toESM, n as __exportAll, r as __require, t as __commonJSMin } from "./chunk-DSJWtz9O.js";
|
|
5
|
-
import { a as isSchemaAdmin, i as isSQLAdmin, l as isPostgresCollectionConfig, m as Vector, n as getDataSourceCapabilities, o as ANONYMOUS_USER_ID, r as isChannelBusInstance } from "./src-
|
|
6
|
-
import { C as createRelationRefWithData, D as mergeDeep, E as getPolicyNamesForRule, O as camelCase, S as createRelationRef, T as updateDateAutoValues, _ as getTableVarName, a as getJunctionCollectionConfig, b as getDeclaredPrimaryKeys, c as getEffectiveSecurityRules, d as securityRuleToConditions, f as findAnonymousGrants, g as getTableName$1, h as getEnumVarName, i as CollectionRegistry, k as toSnakeCase, l as buildPropertyCallbacks, m as getColumnName, n as detectJunctionTables, o as getJunctionSecurityRules, p as findRelation, r as buildSdkData, s as resolveJunctionSpecs, t as classifyTable, u as policyToPostgres, v as resolveCollectionRelations, w as normalizeToEntityRelation, x as parseIdValues, y as buildCompositeId } from "./src-
|
|
5
|
+
import { a as isSchemaAdmin, i as isSQLAdmin, l as isPostgresCollectionConfig, m as Vector, n as getDataSourceCapabilities, o as ANONYMOUS_USER_ID, r as isChannelBusInstance } from "./src-CBgtrPhJ.js";
|
|
6
|
+
import { C as createRelationRefWithData, D as mergeDeep, E as getPolicyNamesForRule, O as camelCase, S as createRelationRef, T as updateDateAutoValues, _ as getTableVarName, a as getJunctionCollectionConfig, b as getDeclaredPrimaryKeys, c as getEffectiveSecurityRules, d as securityRuleToConditions, f as findAnonymousGrants, g as getTableName$1, h as getEnumVarName, i as CollectionRegistry, k as toSnakeCase, l as buildPropertyCallbacks, m as getColumnName, n as detectJunctionTables, o as getJunctionSecurityRules, p as findRelation, r as buildSdkData, s as resolveJunctionSpecs, t as classifyTable, u as policyToPostgres, v as resolveCollectionRelations, w as normalizeToEntityRelation, x as parseIdValues, y as buildCompositeId } from "./src-lcfUP4xg.js";
|
|
7
7
|
import { Client, Pool } from "pg";
|
|
8
8
|
import { drizzle } from "drizzle-orm/node-postgres";
|
|
9
9
|
import { ApiError, createEmailService, extractUserFromToken, loadCollectionsFromDirectory, logger, safeCompare } from "@rebasepro/server";
|
|
@@ -392,6 +392,85 @@ function unwrapRelationFilterValue(value) {
|
|
|
392
392
|
* const builder: ConditionBuilderStatic<SQL> = DrizzleConditionBuilder;
|
|
393
393
|
*/
|
|
394
394
|
var DrizzleConditionBuilder = class {
|
|
395
|
+
/**
|
|
396
|
+
* Express "reachable from this parent through this relation" as a plain
|
|
397
|
+
* `WHERE` condition on the target table.
|
|
398
|
+
*
|
|
399
|
+
* This is the primitive that lets a relation be a *filter* rather than an
|
|
400
|
+
* addressing scheme. A nested listing used to be served by its own query
|
|
401
|
+
* builder — `fetchEntitiesUsingJoins`, which grew joins the root pipeline
|
|
402
|
+
* did not have and lost the options the root pipeline did have (offset,
|
|
403
|
+
* filter, orderBy, include). Reduced to a condition, the same listing runs
|
|
404
|
+
* through the ordinary collection query, so it inherits all of them and
|
|
405
|
+
* there is one read path instead of two.
|
|
406
|
+
*
|
|
407
|
+
* The shapes:
|
|
408
|
+
* - inverse FK → `target.<fk> = :parentId`, a column comparison.
|
|
409
|
+
* - `through` → `EXISTS (SELECT 1 FROM junction …)`, correlated on the
|
|
410
|
+
* target's key, so the junction never multiplies rows the
|
|
411
|
+
* way an `INNER JOIN` would.
|
|
412
|
+
* - `joinPath` → the same `EXISTS`, with the path's steps joined inside
|
|
413
|
+
* it and the final step correlating to the outer row.
|
|
414
|
+
*/
|
|
415
|
+
static buildRelationScopeCondition(relation, parent, parentId, targetTable, targetIdColumn, registry) {
|
|
416
|
+
if (relation.joinPath && relation.joinPath.length > 0) {
|
|
417
|
+
const { table, idColumn } = parent();
|
|
418
|
+
return this.buildJoinPathScopeCondition(relation.joinPath, table, idColumn, parentId, targetIdColumn, registry);
|
|
419
|
+
}
|
|
420
|
+
if (relation.through) {
|
|
421
|
+
const junctionTable = registry.getTable(relation.through.table);
|
|
422
|
+
if (!junctionTable) throw new Error(`Junction table not found: ${relation.through.table}`);
|
|
423
|
+
const sourceCol = junctionTable[relation.through.sourceColumn];
|
|
424
|
+
const targetCol = junctionTable[relation.through.targetColumn];
|
|
425
|
+
if (!sourceCol || !targetCol) throw new Error(`Junction columns '${relation.through.sourceColumn}'/'${relation.through.targetColumn}' not found in '${relation.through.table}'`);
|
|
426
|
+
return sql`EXISTS (SELECT 1 FROM ${junctionTable} WHERE ${targetCol} = ${targetIdColumn} AND ${sourceCol} = ${parentId})`;
|
|
427
|
+
}
|
|
428
|
+
if (relation.foreignKeyOnTarget) {
|
|
429
|
+
const fkColumn = targetTable[relation.foreignKeyOnTarget];
|
|
430
|
+
if (!fkColumn) throw new Error(`Foreign key column '${relation.foreignKeyOnTarget}' not found in the target table of relation '${relation.relationName}'. A many-to-many relation needs \`through\` instead.`);
|
|
431
|
+
return eq(fkColumn, parentId);
|
|
432
|
+
}
|
|
433
|
+
if (relation.localKey) {
|
|
434
|
+
const { table, idColumn } = parent();
|
|
435
|
+
return sql`${targetIdColumn} = (SELECT ${sql.identifier(relation.localKey)} FROM ${table} WHERE ${idColumn} = ${parentId})`;
|
|
436
|
+
}
|
|
437
|
+
throw new Error(`Relation '${relation.relationName}' declares no \`foreignKeyOnTarget\`, \`through\`, \`joinPath\` or \`localKey\`, so there is no way to tell which target rows belong to a parent.`);
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* `EXISTS` for an explicit `joinPath`.
|
|
441
|
+
*
|
|
442
|
+
* The path is declared source → target. The subquery replays every step but
|
|
443
|
+
* the last from inside, and turns the last one into the correlation with the
|
|
444
|
+
* outer target row — so the target table is never named twice and needs no
|
|
445
|
+
* alias. Each intermediate table is aliased positionally, which keeps a path
|
|
446
|
+
* that revisits a table (a self-referencing many-to-many) unambiguous.
|
|
447
|
+
*/
|
|
448
|
+
static buildJoinPathScopeCondition(joinPath, parentTable, parentIdColumn, parentId, targetIdColumn, registry) {
|
|
449
|
+
const sourceAlias = "__rel_src";
|
|
450
|
+
const aliasFor = (index) => `__rel_j${index}`;
|
|
451
|
+
const fromRef = (stepIndex, column) => sql`${sql.identifier(stepIndex === 0 ? sourceAlias : aliasFor(stepIndex - 1))}.${sql.identifier(getColumnName(column))}`;
|
|
452
|
+
const pairs = (step) => {
|
|
453
|
+
const from = Array.isArray(step.on.from) ? step.on.from : [step.on.from];
|
|
454
|
+
const to = Array.isArray(step.on.to) ? step.on.to : [step.on.to];
|
|
455
|
+
if (from.length !== to.length) throw new Error(`Join step on '${step.table}' has ${from.length} \`from\` columns and ${to.length} \`to\` columns`);
|
|
456
|
+
return from.map((f, i) => ({
|
|
457
|
+
from: f,
|
|
458
|
+
to: to[i]
|
|
459
|
+
}));
|
|
460
|
+
};
|
|
461
|
+
const inner = joinPath.slice(0, -1);
|
|
462
|
+
const last = joinPath[joinPath.length - 1];
|
|
463
|
+
const joins = inner.map((step, index) => {
|
|
464
|
+
const table = registry.getTable(step.table);
|
|
465
|
+
if (!table) throw new Error(`Join table not found: ${step.table}`);
|
|
466
|
+
const on = pairs(step).map(({ from, to }) => sql`${fromRef(index, from)} = ${sql.identifier(aliasFor(index))}.${sql.identifier(getColumnName(to))}`);
|
|
467
|
+
return sql`JOIN ${table} AS ${sql.identifier(aliasFor(index))} ON ${sql.join(on, sql` AND `)}`;
|
|
468
|
+
});
|
|
469
|
+
const lastPairs = pairs(last);
|
|
470
|
+
const correlation = lastPairs.length === 1 ? sql`${fromRef(inner.length, lastPairs[0].from)} = ${targetIdColumn}` : sql.join(lastPairs.map(({ from, to }) => sql`${fromRef(inner.length, from)} = ${sql.identifier(getColumnName(to))}`), sql` AND `);
|
|
471
|
+
const joinsSql = joins.length > 0 ? sql` ${sql.join(joins, sql` `)}` : sql``;
|
|
472
|
+
return sql`EXISTS (SELECT 1 FROM ${parentTable} AS ${sql.identifier(sourceAlias)}${joinsSql} WHERE ${sql.identifier(sourceAlias)}.${sql.identifier(parentIdColumn.name)} = ${parentId} AND ${correlation})`;
|
|
473
|
+
}
|
|
395
474
|
/**
|
|
396
475
|
* Build filter conditions from FilterValues
|
|
397
476
|
*/
|
|
@@ -1551,6 +1630,18 @@ var RelationService = class {
|
|
|
1551
1630
|
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
1552
1631
|
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollectionPath}'. Available relations: [${available}]`);
|
|
1553
1632
|
}
|
|
1633
|
+
return this.countRelatedRows(parentCollection, parentId, relation, []);
|
|
1634
|
+
}
|
|
1635
|
+
/**
|
|
1636
|
+
* Count the target rows a parent reaches through `relation`, narrowed by
|
|
1637
|
+
* `additionalFilters` (conditions on the target table).
|
|
1638
|
+
*
|
|
1639
|
+
* Shared by the public count and by {@link isRelated}, so "how many children
|
|
1640
|
+
* does this parent have" and "is this row one of them" are answered by the
|
|
1641
|
+
* same join — a membership test that reconstructed the join separately would
|
|
1642
|
+
* be free to disagree with the listing it is supposed to gate.
|
|
1643
|
+
*/
|
|
1644
|
+
async countRelatedRows(parentCollection, parentId, relation, additionalFilters) {
|
|
1554
1645
|
const targetCollection = relation.target();
|
|
1555
1646
|
const targetTable = getTableForCollection(targetCollection, this.registry);
|
|
1556
1647
|
const targetIdField = targetTable[requirePrimaryKeys(targetCollection, this.registry)[0].fieldName];
|
|
@@ -1561,11 +1652,54 @@ var RelationService = class {
|
|
|
1561
1652
|
if (!parentTable) throw new Error("Parent table not found");
|
|
1562
1653
|
const parentIdCol = parentTable[parentIdInfo.fieldName];
|
|
1563
1654
|
let query = this.db.select({ count: sql`count(distinct ${targetIdField})` }).from(targetTable).$dynamic();
|
|
1564
|
-
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation, parsedParentId, targetTable, parentTable, parentIdCol, targetIdField, this.registry,
|
|
1655
|
+
query = DrizzleConditionBuilder.buildRelationCountQuery(query, relation, parsedParentId, targetTable, parentTable, parentIdCol, targetIdField, this.registry, additionalFilters);
|
|
1565
1656
|
const result = await query;
|
|
1566
1657
|
return Number(result[0]?.count || 0);
|
|
1567
1658
|
}
|
|
1568
1659
|
/**
|
|
1660
|
+
* Whether `targetId` is actually reachable from the parent named in `hop`.
|
|
1661
|
+
*
|
|
1662
|
+
* A nested address like `authors/1/posts/43` used to resolve to the target
|
|
1663
|
+
* collection and then match on the primary key alone, so the parent segment
|
|
1664
|
+
* decided nothing: the row came back, and was updated or deleted, whoever it
|
|
1665
|
+
* belonged to. Reads, updates and deletes now all gate on this.
|
|
1666
|
+
*/
|
|
1667
|
+
async isRelated(hop, targetId) {
|
|
1668
|
+
const targetTable = getTableForCollection(hop.targetCollection, this.registry);
|
|
1669
|
+
const targetPks = requirePrimaryKeys(hop.targetCollection, this.registry);
|
|
1670
|
+
const parsedTargetId = parseIdValues(targetId, targetPks);
|
|
1671
|
+
const identity = targetPks.map((pk) => {
|
|
1672
|
+
const column = targetTable[pk.fieldName];
|
|
1673
|
+
if (!column) throw new Error(`ID field '${pk.fieldName}' not found in table for collection '${hop.targetCollection.slug}'`);
|
|
1674
|
+
return eq(column, parsedTargetId[pk.fieldName]);
|
|
1675
|
+
});
|
|
1676
|
+
return await this.countRelatedRows(hop.parentCollection, hop.parentId, hop.relation, identity) > 0;
|
|
1677
|
+
}
|
|
1678
|
+
/**
|
|
1679
|
+
* Remove the junction row linking a parent to `targetId`, leaving the target
|
|
1680
|
+
* row itself alone.
|
|
1681
|
+
*
|
|
1682
|
+
* This is what `DELETE authors/1/tags/5` has to mean for a many-to-many: the
|
|
1683
|
+
* target is shared, so deleting the row would remove the tag from every other
|
|
1684
|
+
* post that uses it. It used to do exactly that — resolve the path to the
|
|
1685
|
+
* `tags` table and delete by primary key.
|
|
1686
|
+
*/
|
|
1687
|
+
async unlinkRelatedEntity(tx, hop, targetId) {
|
|
1688
|
+
const through = hop.relation.through;
|
|
1689
|
+
if (!through) throw new Error(`Relation '${hop.relationKey}' has no junction table to unlink through`);
|
|
1690
|
+
const junctionTable = this.registry.getTable(through.table);
|
|
1691
|
+
if (!junctionTable) throw new Error(`Junction table not found: ${through.table}`);
|
|
1692
|
+
const sourceJunctionColumn = junctionTable[through.sourceColumn];
|
|
1693
|
+
const targetJunctionColumn = junctionTable[through.targetColumn];
|
|
1694
|
+
if (!sourceJunctionColumn || !targetJunctionColumn) throw new Error(`Junction columns not found for relation '${hop.relationKey}' on table '${through.table}'`);
|
|
1695
|
+
const parentPks = requirePrimaryKeys(hop.parentCollection, this.registry);
|
|
1696
|
+
const parsedParentId = parseIdValues(hop.parentId, parentPks)[parentPks[0].fieldName];
|
|
1697
|
+
const targetPks = requirePrimaryKeys(hop.targetCollection, this.registry);
|
|
1698
|
+
const parsedTargetId = parseIdValues(targetId, targetPks)[targetPks[0].fieldName];
|
|
1699
|
+
await tx.delete(junctionTable).where(and(eq(sourceJunctionColumn, parsedParentId), eq(targetJunctionColumn, parsedTargetId)));
|
|
1700
|
+
logger.info(`Unlinked '${hop.relationKey}' ${parsedTargetId} from ${hop.parentCollection.slug} ${parsedParentId}`);
|
|
1701
|
+
}
|
|
1702
|
+
/**
|
|
1569
1703
|
* Batch fetch related rows for multiple parent rows to avoid N+1 queries
|
|
1570
1704
|
*/
|
|
1571
1705
|
async batchFetchRelatedEntities(parentCollectionPath, parentIds, _relationKey, relation) {
|
|
@@ -2102,8 +2236,8 @@ var RelationService = class {
|
|
|
2102
2236
|
[sourceJunctionColumn.name]: parentId,
|
|
2103
2237
|
[targetJunctionColumn.name]: parsedNewEntityId
|
|
2104
2238
|
};
|
|
2105
|
-
await tx.insert(junctionTable).values(junctionData);
|
|
2106
|
-
logger.info(`
|
|
2239
|
+
await tx.insert(junctionTable).values(junctionData).onConflictDoNothing();
|
|
2240
|
+
logger.info(`Linked '${relationKey}' ${parsedNewEntityId} to ${parentId}`);
|
|
2107
2241
|
} catch (error) {
|
|
2108
2242
|
logger.error(`Failed to create junction table entry for relation '${relationKey}'`, { error });
|
|
2109
2243
|
throw error;
|
|
@@ -2255,6 +2389,79 @@ function toRestRow(row, collection, registry) {
|
|
|
2255
2389
|
return stripExcluded(flat, collection);
|
|
2256
2390
|
}
|
|
2257
2391
|
//#endregion
|
|
2392
|
+
//#region src/services/nested-path.ts
|
|
2393
|
+
/**
|
|
2394
|
+
* True when `path` addresses rows through a relation rather than a root
|
|
2395
|
+
* collection.
|
|
2396
|
+
*
|
|
2397
|
+
* Any separator at all counts — a root collection slug never contains one — so
|
|
2398
|
+
* a malformed path like `collection/id` is a *broken* nested path and gets
|
|
2399
|
+
* reported as one by {@link resolveNestedPath}, rather than being looked up as
|
|
2400
|
+
* a root collection whose slug happens to contain a slash.
|
|
2401
|
+
*/
|
|
2402
|
+
function isNestedPath(path) {
|
|
2403
|
+
return path.includes("/");
|
|
2404
|
+
}
|
|
2405
|
+
function splitPathSegments(path) {
|
|
2406
|
+
return path.split("/").filter((s) => s && s !== "undefined");
|
|
2407
|
+
}
|
|
2408
|
+
/**
|
|
2409
|
+
* Walk a nested collection path down to the relation it ends in.
|
|
2410
|
+
*
|
|
2411
|
+
* Returns `undefined` for a plain root-collection path so callers can keep the
|
|
2412
|
+
* root case on its existing code path. Throws when the path is malformed, or
|
|
2413
|
+
* when a segment names a relation that does not exist — the same errors the
|
|
2414
|
+
* individual walks used to raise, with the available names attached.
|
|
2415
|
+
*/
|
|
2416
|
+
function resolveNestedPath(path, registry) {
|
|
2417
|
+
if (!isNestedPath(path)) return void 0;
|
|
2418
|
+
const segments = splitPathSegments(path);
|
|
2419
|
+
if (segments.length < 3 || segments.length % 2 === 0) throw new Error(`Invalid relation path: ${path}. Expected format: collection/id/relation`);
|
|
2420
|
+
let parentCollection = getCollectionByPath(segments[0], registry);
|
|
2421
|
+
let parentId = segments[1];
|
|
2422
|
+
for (let i = 2; i < segments.length; i += 2) {
|
|
2423
|
+
const relationKey = segments[i];
|
|
2424
|
+
const resolvedRelations = resolveCollectionRelations(parentCollection);
|
|
2425
|
+
const relation = findRelation(resolvedRelations, relationKey);
|
|
2426
|
+
if (!relation) {
|
|
2427
|
+
const available = Object.keys(resolvedRelations).join(", ") || "(none)";
|
|
2428
|
+
throw new Error(`Relation '${relationKey}' not found in collection '${parentCollection.slug}'. Available relations: [${available}]`);
|
|
2429
|
+
}
|
|
2430
|
+
const targetCollection = relation.target();
|
|
2431
|
+
if (i === segments.length - 1) return {
|
|
2432
|
+
parentCollection,
|
|
2433
|
+
parentId,
|
|
2434
|
+
relationKey,
|
|
2435
|
+
relation,
|
|
2436
|
+
targetCollection
|
|
2437
|
+
};
|
|
2438
|
+
parentCollection = targetCollection;
|
|
2439
|
+
parentId = segments[i + 1];
|
|
2440
|
+
}
|
|
2441
|
+
throw new Error(`Unable to resolve path: ${path}`);
|
|
2442
|
+
}
|
|
2443
|
+
/**
|
|
2444
|
+
* A relation reached through a junction table — many-to-many, or a multi-hop
|
|
2445
|
+
* `joinPath`. The target row is shared with other parents, so writing "through"
|
|
2446
|
+
* such a path addresses the *link*, not the row.
|
|
2447
|
+
*/
|
|
2448
|
+
function isJunctionBackedRelation(relation) {
|
|
2449
|
+
return Boolean(relation.through) || Boolean(relation.joinPath && relation.joinPath.length > 1);
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Reject a nested write whose final segment is a to-one relation.
|
|
2453
|
+
*
|
|
2454
|
+
* There is no column on the target row that records a to-one parent — the
|
|
2455
|
+
* foreign key lives on the *parent* table. The write path used to fall through
|
|
2456
|
+
* to `relation.localKey` here and stamp the parent's own FK column onto the
|
|
2457
|
+
* target row, which either raised an opaque "column does not exist" or, when a
|
|
2458
|
+
* column of that name happened to exist on the target, silently wrote the wrong
|
|
2459
|
+
* one.
|
|
2460
|
+
*/
|
|
2461
|
+
function assertWritableThrough(hop, path) {
|
|
2462
|
+
if (hop.relation.cardinality !== "many") throw ApiError.badRequest(`"${path}" ends in the to-one relation '${hop.relationKey}', which cannot be written through: the foreign key for a to-one relation lives on '${hop.parentCollection.slug}', not on '${hop.targetCollection.slug}'. Write the target row at "${hop.targetCollection.slug}" and set '${hop.relationKey}' on the parent instead.`, "RELATION_NOT_WRITABLE");
|
|
2463
|
+
}
|
|
2464
|
+
//#endregion
|
|
2258
2465
|
//#region src/services/FetchService.ts
|
|
2259
2466
|
/**
|
|
2260
2467
|
* Service for handling all row read operations.
|
|
@@ -2386,10 +2593,11 @@ var FetchService = class {
|
|
|
2386
2593
|
* Build db.query-compatible options from standard fetch options.
|
|
2387
2594
|
* Handles filter, search, orderBy, limit, and cursor-based pagination.
|
|
2388
2595
|
*/
|
|
2389
|
-
buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig) {
|
|
2596
|
+
buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig, scopeCondition) {
|
|
2390
2597
|
const queryOpts = {};
|
|
2391
2598
|
if (withConfig) queryOpts.with = withConfig;
|
|
2392
2599
|
const allConditions = [];
|
|
2600
|
+
if (scopeCondition) allConditions.push(scopeCondition);
|
|
2393
2601
|
if (options.searchString) {
|
|
2394
2602
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
2395
2603
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
@@ -2447,9 +2655,49 @@ var FetchService = class {
|
|
|
2447
2655
|
return [];
|
|
2448
2656
|
}
|
|
2449
2657
|
/**
|
|
2658
|
+
* Compile "rows reachable from this parent" into a `WHERE` condition on the
|
|
2659
|
+
* target table, so a nested listing can run as an ordinary collection query.
|
|
2660
|
+
*/
|
|
2661
|
+
buildRelationScope(hop) {
|
|
2662
|
+
const parentPks = requirePrimaryKeys(hop.parentCollection, this.registry);
|
|
2663
|
+
const parentIdInfo = parentPks[0];
|
|
2664
|
+
const parsedParentId = parseIdValues(hop.parentId, parentPks)[parentIdInfo.fieldName];
|
|
2665
|
+
const parent = () => {
|
|
2666
|
+
const table = getTableForCollection(hop.parentCollection, this.registry);
|
|
2667
|
+
const idColumn = table[parentIdInfo.fieldName];
|
|
2668
|
+
if (!idColumn) throw new Error(`ID field '${parentIdInfo.fieldName}' not found in table for collection '${hop.parentCollection.slug}'`);
|
|
2669
|
+
return {
|
|
2670
|
+
table,
|
|
2671
|
+
idColumn
|
|
2672
|
+
};
|
|
2673
|
+
};
|
|
2674
|
+
const targetTable = getTableForCollection(hop.targetCollection, this.registry);
|
|
2675
|
+
const targetPks = requirePrimaryKeys(hop.targetCollection, this.registry);
|
|
2676
|
+
const targetIdColumn = targetTable[targetPks[0].fieldName];
|
|
2677
|
+
if (!targetIdColumn) throw new Error(`ID field '${targetPks[0].fieldName}' not found in table for collection '${hop.targetCollection.slug}'`);
|
|
2678
|
+
return DrizzleConditionBuilder.buildRelationScopeCondition(hop.relation, parent, parsedParentId, targetTable, targetIdColumn, this.registry);
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* Whether `id` is actually reachable at `collectionPath`.
|
|
2682
|
+
*
|
|
2683
|
+
* Trivially true for a root path. For a nested one it is a real question:
|
|
2684
|
+
* the path resolves to the target collection, and matching on the primary
|
|
2685
|
+
* key alone made the parent segment decorative — `authors/1/posts/43`
|
|
2686
|
+
* returned post 43 whoever wrote it, and the REST layer's delete then
|
|
2687
|
+
* deleted it. A row that is not under this parent is reported as absent,
|
|
2688
|
+
* which is what a caller addressing it through the parent should see.
|
|
2689
|
+
*/
|
|
2690
|
+
async isAddressableUnder(collectionPath, id) {
|
|
2691
|
+
if (!isNestedPath(collectionPath)) return true;
|
|
2692
|
+
const hop = resolveNestedPath(collectionPath, this.registry);
|
|
2693
|
+
if (!hop) return true;
|
|
2694
|
+
return this.relationService.isRelated(hop, id);
|
|
2695
|
+
}
|
|
2696
|
+
/**
|
|
2450
2697
|
* Fetch a single row by ID
|
|
2451
2698
|
*/
|
|
2452
2699
|
async fetchOne(collectionPath, id, databaseId) {
|
|
2700
|
+
if (!await this.isAddressableUnder(collectionPath, id)) return void 0;
|
|
2453
2701
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
2454
2702
|
const table = getTableForCollection(collection, this.registry);
|
|
2455
2703
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
@@ -2506,6 +2754,7 @@ var FetchService = class {
|
|
|
2506
2754
|
* Unified method to fetch rows with optional search functionality
|
|
2507
2755
|
*/
|
|
2508
2756
|
async fetchRowsWithConditions(collectionPath, options = {}) {
|
|
2757
|
+
const scopeCondition = options.relatedTo ? this.buildRelationScope(options.relatedTo) : void 0;
|
|
2509
2758
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
2510
2759
|
const table = getTableForCollection(collection, this.registry);
|
|
2511
2760
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
@@ -2517,7 +2766,7 @@ var FetchService = class {
|
|
|
2517
2766
|
const withConfig = this.buildWithConfig(collection);
|
|
2518
2767
|
const hasRelations = withConfig && Object.keys(withConfig).length > 0;
|
|
2519
2768
|
if (qb && !options.searchString && !hasRelations && !options.vectorSearch) try {
|
|
2520
|
-
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0);
|
|
2769
|
+
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, void 0, scopeCondition);
|
|
2521
2770
|
return (await qb.findMany(queryOpts)).map((row) => toCmsRow(row, collection, this.registry));
|
|
2522
2771
|
} catch (e) {
|
|
2523
2772
|
if (e instanceof Error && e.message.includes("not enough information to infer relation")) {
|
|
@@ -2533,6 +2782,7 @@ var FetchService = class {
|
|
|
2533
2782
|
_distance: vectorMeta.distanceSelect
|
|
2534
2783
|
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
2535
2784
|
const allConditions = [];
|
|
2785
|
+
if (scopeCondition) allConditions.push(scopeCondition);
|
|
2536
2786
|
if (options.searchString) {
|
|
2537
2787
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
2538
2788
|
if (searchConditions.length === 0) return [];
|
|
@@ -2637,7 +2887,11 @@ var FetchService = class {
|
|
|
2637
2887
|
* Fetch a collection of rows
|
|
2638
2888
|
*/
|
|
2639
2889
|
async fetchCollection(collectionPath, options = {}) {
|
|
2640
|
-
|
|
2890
|
+
const hop = isNestedPath(collectionPath) ? resolveNestedPath(collectionPath, this.registry) : void 0;
|
|
2891
|
+
if (hop) return this.fetchRowsWithConditions(hop.targetCollection.slug, {
|
|
2892
|
+
...options,
|
|
2893
|
+
relatedTo: hop
|
|
2894
|
+
});
|
|
2641
2895
|
return this.fetchRowsWithConditions(collectionPath, options);
|
|
2642
2896
|
}
|
|
2643
2897
|
/**
|
|
@@ -2650,43 +2904,23 @@ var FetchService = class {
|
|
|
2650
2904
|
});
|
|
2651
2905
|
}
|
|
2652
2906
|
/**
|
|
2653
|
-
* Fetch collection from multi-segment path
|
|
2654
|
-
*/
|
|
2655
|
-
async fetchCollectionFromPath(path, options = {}) {
|
|
2656
|
-
const pathSegments = path.split("/").filter((p) => p && p !== "undefined");
|
|
2657
|
-
if (pathSegments.length < 3 || pathSegments.length % 2 === 0) throw new Error(`Invalid relation path: ${path}. Expected format: collection/id/relation`);
|
|
2658
|
-
const rootCollectionPath = pathSegments[0];
|
|
2659
|
-
let currentCollection = getCollectionByPath(rootCollectionPath, this.registry);
|
|
2660
|
-
let currentId = pathSegments[1];
|
|
2661
|
-
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
2662
|
-
const relationKey = pathSegments[i];
|
|
2663
|
-
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2664
|
-
if (!relation) throw new Error(`Relation '${relationKey}' not found in collection '${currentCollection.slug}'`);
|
|
2665
|
-
if (i === pathSegments.length - 1) return (await this.relationService.fetchRelatedEntities(currentCollection.slug, currentId, relationKey, options)).map((row) => ({ ...row.values }));
|
|
2666
|
-
if (i + 1 < pathSegments.length) {
|
|
2667
|
-
const nextEntityId = pathSegments[i + 1];
|
|
2668
|
-
currentCollection = relation.target();
|
|
2669
|
-
currentId = nextEntityId;
|
|
2670
|
-
}
|
|
2671
|
-
}
|
|
2672
|
-
throw new Error(`Unable to resolve path: ${path}`);
|
|
2673
|
-
}
|
|
2674
|
-
/**
|
|
2675
2907
|
* Count rows in a collection
|
|
2676
2908
|
*/
|
|
2677
2909
|
async count(collectionPath, options = {}) {
|
|
2678
|
-
|
|
2679
|
-
const
|
|
2910
|
+
const hop = isNestedPath(collectionPath) ? resolveNestedPath(collectionPath, this.registry) : void 0;
|
|
2911
|
+
const effectivePath = hop ? hop.targetCollection.slug : collectionPath;
|
|
2912
|
+
const collection = getCollectionByPath(effectivePath, this.registry);
|
|
2680
2913
|
const table = getTableForCollection(collection, this.registry);
|
|
2681
2914
|
let query = this.db.select({ count: count() }).from(table).$dynamic();
|
|
2682
2915
|
const allConditions = [];
|
|
2916
|
+
if (hop) allConditions.push(this.buildRelationScope(hop));
|
|
2683
2917
|
if (options.searchString) {
|
|
2684
2918
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
2685
2919
|
if (searchConditions.length === 0) return 0;
|
|
2686
2920
|
allConditions.push(DrizzleConditionBuilder.combineConditionsWithOr(searchConditions));
|
|
2687
2921
|
}
|
|
2688
2922
|
if (options.filter) {
|
|
2689
|
-
const filterConditions = this.buildFilterConditions(options.filter, table,
|
|
2923
|
+
const filterConditions = this.buildFilterConditions(options.filter, table, effectivePath);
|
|
2690
2924
|
if (filterConditions.length > 0) allConditions.push(...filterConditions);
|
|
2691
2925
|
}
|
|
2692
2926
|
if (allConditions.length > 0) {
|
|
@@ -2697,27 +2931,6 @@ var FetchService = class {
|
|
|
2697
2931
|
return Number(result[0]?.count || 0);
|
|
2698
2932
|
}
|
|
2699
2933
|
/**
|
|
2700
|
-
* Count rows from multi-segment path
|
|
2701
|
-
*/
|
|
2702
|
-
async countEntitiesFromPath(path, options = {}) {
|
|
2703
|
-
const pathSegments = path.split("/").filter((p) => p && p !== "undefined");
|
|
2704
|
-
if (pathSegments.length < 3 || pathSegments.length % 2 === 0) throw new Error(`Invalid relation path: ${path}`);
|
|
2705
|
-
const rootCollectionPath = pathSegments[0];
|
|
2706
|
-
let currentCollection = getCollectionByPath(rootCollectionPath, this.registry);
|
|
2707
|
-
let currentId = pathSegments[1];
|
|
2708
|
-
for (let i = 2; i < pathSegments.length; i += 2) {
|
|
2709
|
-
const relationKey = pathSegments[i];
|
|
2710
|
-
const relation = findRelation(resolveCollectionRelations(currentCollection), relationKey);
|
|
2711
|
-
if (!relation) throw new Error(`Relation '${relationKey}' not found`);
|
|
2712
|
-
if (i === pathSegments.length - 1) return this.relationService.countRelatedEntities(currentCollection.slug, currentId, relationKey, options);
|
|
2713
|
-
if (i + 1 < pathSegments.length) {
|
|
2714
|
-
currentCollection = relation.target();
|
|
2715
|
-
currentId = pathSegments[i + 1];
|
|
2716
|
-
}
|
|
2717
|
-
}
|
|
2718
|
-
throw new Error(`Unable to count for path: ${path}`);
|
|
2719
|
-
}
|
|
2720
|
-
/**
|
|
2721
2934
|
* Check if a field value is unique
|
|
2722
2935
|
*/
|
|
2723
2936
|
async checkUniqueField(collectionPath, fieldName, value, excludeEntityId, _databaseId) {
|
|
@@ -2749,6 +2962,14 @@ var FetchService = class {
|
|
|
2749
2962
|
* @param include - Array of relation keys to populate, or ["*"] for all
|
|
2750
2963
|
*/
|
|
2751
2964
|
async fetchCollectionForRest(collectionPath, options = {}, include) {
|
|
2965
|
+
if (isNestedPath(collectionPath)) {
|
|
2966
|
+
const hop = resolveNestedPath(collectionPath, this.registry);
|
|
2967
|
+
if (hop) return this.fetchCollectionForRest(hop.targetCollection.slug, {
|
|
2968
|
+
...options,
|
|
2969
|
+
relatedTo: hop
|
|
2970
|
+
}, include);
|
|
2971
|
+
}
|
|
2972
|
+
const scopeCondition = options.relatedTo ? this.buildRelationScope(options.relatedTo) : void 0;
|
|
2752
2973
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
2753
2974
|
const table = getTableForCollection(collection, this.registry);
|
|
2754
2975
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
@@ -2758,7 +2979,7 @@ var FetchService = class {
|
|
|
2758
2979
|
const qb = this.getQueryBuilder(tableName);
|
|
2759
2980
|
if (qb && !options.searchString && !options.vectorSearch) try {
|
|
2760
2981
|
const withConfig = include && include.length > 0 ? this.buildWithConfig(collection, include) : void 0;
|
|
2761
|
-
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig);
|
|
2982
|
+
const queryOpts = this.buildDrizzleQueryOptions(table, idField, idInfo, options, collectionPath, withConfig, scopeCondition);
|
|
2762
2983
|
const restRows = (await qb.findMany(queryOpts)).map((row) => toRestRow(row, collection, this.registry));
|
|
2763
2984
|
await this.resolveJoinPathRelationsBatchRest(restRows, collection, collectionPath, idInfoArray, include);
|
|
2764
2985
|
return restRows;
|
|
@@ -2806,6 +3027,7 @@ var FetchService = class {
|
|
|
2806
3027
|
* Fetch a single row with optional relation includes for REST API.
|
|
2807
3028
|
*/
|
|
2808
3029
|
async fetchOneForRest(collectionPath, id, include, databaseId) {
|
|
3030
|
+
if (!await this.isAddressableUnder(collectionPath, id)) return null;
|
|
2809
3031
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
2810
3032
|
const table = getTableForCollection(collection, this.registry);
|
|
2811
3033
|
const idInfoArray = requirePrimaryKeys(collection, this.registry);
|
|
@@ -2874,6 +3096,7 @@ var FetchService = class {
|
|
|
2874
3096
|
_distance: vectorMeta.distanceSelect
|
|
2875
3097
|
}).from(table).$dynamic() : this.db.select().from(table).$dynamic();
|
|
2876
3098
|
const allConditions = [];
|
|
3099
|
+
if (options.relatedTo) allConditions.push(this.buildRelationScope(options.relatedTo));
|
|
2877
3100
|
if (options.searchString) {
|
|
2878
3101
|
const searchConditions = DrizzleConditionBuilder.buildSearchConditions(options.searchString, collection.properties, table);
|
|
2879
3102
|
if (searchConditions.length === 0) return [];
|
|
@@ -3193,6 +3416,16 @@ var PersistService = class {
|
|
|
3193
3416
|
* Delete an row by ID
|
|
3194
3417
|
*/
|
|
3195
3418
|
async delete(collectionPath, id, _databaseId) {
|
|
3419
|
+
const hop = isNestedPath(collectionPath) ? resolveNestedPath(collectionPath, this.registry) : void 0;
|
|
3420
|
+
if (hop) {
|
|
3421
|
+
assertWritableThrough(hop, collectionPath);
|
|
3422
|
+
if (!await this.relationService.isRelated(hop, id)) throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to delete.`);
|
|
3423
|
+
if (isJunctionBackedRelation(hop.relation)) {
|
|
3424
|
+
if (!hop.relation.through) throw ApiError.badRequest(`"${collectionPath}" reaches '${hop.targetCollection.slug}' through a multi-hop joinPath, so there is no single link to remove. Delete the row at "${hop.targetCollection.slug}" directly if that is what you meant.`, "RELATION_NOT_UNLINKABLE");
|
|
3425
|
+
await this.relationService.unlinkRelatedEntity(this.db, hop, id);
|
|
3426
|
+
return;
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3196
3429
|
const collection = getCollectionByPath(collectionPath, this.registry);
|
|
3197
3430
|
const table = getTableForCollection(collection, this.registry);
|
|
3198
3431
|
const idInfoArray = getPrimaryKeys(collection, this.registry);
|
|
@@ -3213,6 +3446,29 @@ var PersistService = class {
|
|
|
3213
3446
|
await this.db.delete(table);
|
|
3214
3447
|
}
|
|
3215
3448
|
/**
|
|
3449
|
+
* The column on the *target* table that records the parent, for a create
|
|
3450
|
+
* under a nested one-to-many path.
|
|
3451
|
+
*
|
|
3452
|
+
* Returns `undefined` when the link is not a column at all (a multi-hop
|
|
3453
|
+
* `joinPath`), so the caller writes the row without stamping anything.
|
|
3454
|
+
*
|
|
3455
|
+
* `relation.localKey` is deliberately not consulted: it names a column on
|
|
3456
|
+
* the *source* table. Falling back to it here — which is what this used to
|
|
3457
|
+
* do, and first — stamped the parent's own foreign key onto the child row.
|
|
3458
|
+
*/
|
|
3459
|
+
resolveParentForeignKeyColumn(hop) {
|
|
3460
|
+
const { relation, relationKey, targetCollection } = hop;
|
|
3461
|
+
if (relation.foreignKeyOnTarget) return relation.foreignKeyOnTarget;
|
|
3462
|
+
if (relation.joinPath && relation.joinPath.length === 1) {
|
|
3463
|
+
const joinStep = relation.joinPath[0];
|
|
3464
|
+
const targetTableName = getTableName$1(targetCollection);
|
|
3465
|
+
if (joinStep.table !== targetTableName) logger.warn(`Join step for relation '${relationKey}' targets '${joinStep.table}', not the target table '${targetTableName}'.`);
|
|
3466
|
+
return DrizzleConditionBuilder.getColumnNamesFromColumns(joinStep.on.to)[0];
|
|
3467
|
+
}
|
|
3468
|
+
if (relation.joinPath && relation.joinPath.length > 1) return;
|
|
3469
|
+
throw ApiError.badRequest(`Relation '${relationKey}' on '${hop.parentCollection.slug}' cannot be written through: it declares no \`foreignKeyOnTarget\` (the column on '${targetCollection.slug}' that records the parent) and no \`joinPath\`.`, "RELATION_NOT_WRITABLE");
|
|
3470
|
+
}
|
|
3471
|
+
/**
|
|
3216
3472
|
* Save an row (create or update)
|
|
3217
3473
|
*
|
|
3218
3474
|
* With `options.upsert`, the row is written with INSERT ... ON CONFLICT DO
|
|
@@ -3225,60 +3481,29 @@ var PersistService = class {
|
|
|
3225
3481
|
let effectiveCollectionPath = collectionPath;
|
|
3226
3482
|
const effectiveValues = { ...values };
|
|
3227
3483
|
let junctionTableInfo;
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
parentId: parsedParentId,
|
|
3252
|
-
relation,
|
|
3253
|
-
relationKey
|
|
3254
|
-
};
|
|
3255
|
-
break;
|
|
3256
|
-
}
|
|
3257
|
-
let targetColumnName;
|
|
3258
|
-
if (relation.localKey) targetColumnName = relation.localKey;
|
|
3259
|
-
else if (relation.foreignKeyOnTarget) targetColumnName = relation.foreignKeyOnTarget;
|
|
3260
|
-
else if (relation.joinPath && relation.joinPath.length === 1) {
|
|
3261
|
-
const targetTableName = getTableName$1(targetCollection);
|
|
3262
|
-
const relevantJoinStep = relation.joinPath.find((joinStep) => joinStep.table === targetTableName);
|
|
3263
|
-
if (relevantJoinStep) targetColumnName = DrizzleConditionBuilder.getColumnNamesFromColumns(relevantJoinStep.on.to)[0];
|
|
3264
|
-
else {
|
|
3265
|
-
logger.warn(`Could not find specific join step for target table ${targetTableName} in relation '${relationKey}'.`);
|
|
3266
|
-
targetColumnName = DrizzleConditionBuilder.getColumnNamesFromColumns(relation.joinPath[0].on.to)[0];
|
|
3267
|
-
}
|
|
3268
|
-
} else if (relation.joinPath && relation.joinPath.length > 1) break;
|
|
3269
|
-
else throw new Error(`Relation '${relationKey}' lacks configuration for path-based saving.`);
|
|
3270
|
-
const parentIdInfoArray = getPrimaryKeys(currentCollection, this.registry);
|
|
3271
|
-
const parentIdInfo = parentIdInfoArray[0];
|
|
3272
|
-
const parsedParentId = parseIdValues(currentId, parentIdInfoArray)[parentIdInfo.fieldName];
|
|
3273
|
-
const existingValue = effectiveValues[targetColumnName];
|
|
3274
|
-
if (existingValue !== void 0 && existingValue !== null && existingValue !== parsedParentId) logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
|
|
3275
|
-
effectiveValues[targetColumnName] = parsedParentId;
|
|
3276
|
-
break;
|
|
3277
|
-
} else {
|
|
3278
|
-
const nextEntityId = segments[i + 1];
|
|
3279
|
-
currentCollection = relation.target();
|
|
3280
|
-
currentId = nextEntityId;
|
|
3281
|
-
}
|
|
3484
|
+
const hop = isNestedPath(collectionPath) ? resolveNestedPath(collectionPath, this.registry) : void 0;
|
|
3485
|
+
if (hop) {
|
|
3486
|
+
assertWritableThrough(hop, collectionPath);
|
|
3487
|
+
effectiveCollectionPath = hop.targetCollection.slug;
|
|
3488
|
+
const parentIdForWrite = () => {
|
|
3489
|
+
const parentPks = getPrimaryKeys(hop.parentCollection, this.registry);
|
|
3490
|
+
return parseIdValues(hop.parentId, parentPks)[parentPks[0].fieldName];
|
|
3491
|
+
};
|
|
3492
|
+
if (hop.relation.through) junctionTableInfo = {
|
|
3493
|
+
parentCollection: hop.parentCollection,
|
|
3494
|
+
parentId: parentIdForWrite(),
|
|
3495
|
+
relation: hop.relation,
|
|
3496
|
+
relationKey: hop.relationKey
|
|
3497
|
+
};
|
|
3498
|
+
else if (id !== void 0) {
|
|
3499
|
+
if (!await this.relationService.isRelated(hop, id)) throw ApiError.notFound(`No row "${id}" in "${collectionPath}" to update.`);
|
|
3500
|
+
} else {
|
|
3501
|
+
const targetColumnName = this.resolveParentForeignKeyColumn(hop);
|
|
3502
|
+
if (targetColumnName) {
|
|
3503
|
+
const parsedParentId = parentIdForWrite();
|
|
3504
|
+
const existingValue = effectiveValues[targetColumnName];
|
|
3505
|
+
if (existingValue !== void 0 && existingValue !== null && existingValue !== parsedParentId) logger.warn(`Overriding provided value '${existingValue}' for FK '${targetColumnName}' with path parent id '${parsedParentId}'.`);
|
|
3506
|
+
effectiveValues[targetColumnName] = parsedParentId;
|
|
3282
3507
|
}
|
|
3283
3508
|
}
|
|
3284
3509
|
}
|
|
@@ -3349,7 +3574,7 @@ var PersistService = class {
|
|
|
3349
3574
|
}
|
|
3350
3575
|
if (inverseRelationUpdates.length > 0) await this.relationService.updateInverseRelations(tx, collection, currentId, inverseRelationUpdates);
|
|
3351
3576
|
if (Object.keys(relationValues).length > 0) await this.relationService.updateRelationsUsingJoins(tx, collection, currentId, relationValues);
|
|
3352
|
-
if (junctionTableInfo
|
|
3577
|
+
if (junctionTableInfo) await this.relationService.handleJunctionTableCreation(tx, currentId, junctionTableInfo);
|
|
3353
3578
|
return currentId;
|
|
3354
3579
|
});
|
|
3355
3580
|
} catch (error) {
|
|
@@ -5246,9 +5471,9 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5246
5471
|
if (stringProp.enum) columnDefinition = `${getEnumVarName(getTableName$1(collection), propName)}("${colName}")`;
|
|
5247
5472
|
else if ("isId" in stringProp && stringProp.isId === "uuid") columnDefinition = `uuid("${colName}")`;
|
|
5248
5473
|
else if (stringProp.columnType === "uuid") columnDefinition = `uuid("${colName}")`;
|
|
5249
|
-
else if (stringProp.columnType === "text" || stringProp.ui?.markdown || stringProp.ui?.multiline) columnDefinition = `text("${colName}")`;
|
|
5250
5474
|
else if (stringProp.columnType === "char") columnDefinition = `char("${colName}")`;
|
|
5251
|
-
else columnDefinition = `varchar("${colName}")`;
|
|
5475
|
+
else if (stringProp.columnType === "varchar") columnDefinition = `varchar("${colName}")`;
|
|
5476
|
+
else columnDefinition = `text("${colName}")`;
|
|
5252
5477
|
if (isIdProperty(propName, prop, collection)) columnDefinition += ".primaryKey()";
|
|
5253
5478
|
if ("isId" in stringProp && stringProp.isId !== "manual" && stringProp.isId !== true) {
|
|
5254
5479
|
if (stringProp.isId === "uuid") columnDefinition += ".defaultRandom()";
|
|
@@ -5334,7 +5559,7 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5334
5559
|
const targetTableVar = getTableVarName(getTableName$1(targetCollection));
|
|
5335
5560
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
5336
5561
|
const targetIdField = pkProp.name;
|
|
5337
|
-
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `
|
|
5562
|
+
const baseColumn = pkProp.type === "number" ? `integer("${fkColumnName}")` : pkProp.isUuid ? `uuid("${fkColumnName}")` : `text("${fkColumnName}")`;
|
|
5338
5563
|
const onUpdate = relation.onUpdate ? `onUpdate: "${relation.onUpdate}"` : "";
|
|
5339
5564
|
const required = prop.validation?.required;
|
|
5340
5565
|
const refOptionsParts = [onUpdate, `onDelete: \"${relation.onDelete ?? (required ? "cascade" : "set null")}\"`].filter(Boolean);
|
|
@@ -5347,13 +5572,13 @@ var getDrizzleColumn = (propName, prop, collection, collections) => {
|
|
|
5347
5572
|
const refProp = prop;
|
|
5348
5573
|
const targetCollection = collections.find((c) => c.slug === refProp.path || getTableName$1(c) === refProp.path);
|
|
5349
5574
|
if (!targetCollection) {
|
|
5350
|
-
columnDefinition = `
|
|
5575
|
+
columnDefinition = `text("${colName}")`;
|
|
5351
5576
|
break;
|
|
5352
5577
|
}
|
|
5353
5578
|
const pkProp = getPrimaryKeyProp(targetCollection);
|
|
5354
5579
|
const targetTableVar = getTableVarName(getTableName$1(targetCollection));
|
|
5355
5580
|
const targetIdField = pkProp.name;
|
|
5356
|
-
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `
|
|
5581
|
+
const baseColumn = pkProp.type === "number" ? `integer("${colName}")` : pkProp.isUuid ? `uuid("${colName}")` : `text("${colName}")`;
|
|
5357
5582
|
const required = prop.validation?.required;
|
|
5358
5583
|
columnDefinition = `${baseColumn}.references(() => ${targetTableVar}.${targetIdField}, ${`{ onDelete: "${required ? "cascade" : "set null"}" }`})`;
|
|
5359
5584
|
if (required) columnDefinition += ".notNull()";
|
|
@@ -5529,8 +5754,8 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5529
5754
|
const baseTableName = tableName.includes(".") ? tableName.split(".").pop() : tableName;
|
|
5530
5755
|
const { sourceColumn, targetColumn } = relation.through;
|
|
5531
5756
|
const refOptions = `{ onDelete: \"${relation.onDelete ?? "cascade"}\" }`;
|
|
5532
|
-
const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "
|
|
5533
|
-
const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "
|
|
5757
|
+
const sourceColType = isNumericId(sourceCollection) ? "integer" : getPrimaryKeyProp(sourceCollection).isUuid ? "uuid" : "text";
|
|
5758
|
+
const targetColType = isNumericId(targetCollection) ? "integer" : getPrimaryKeyProp(targetCollection).isUuid ? "uuid" : "text";
|
|
5534
5759
|
const sourceId = getPrimaryKeyName(sourceCollection);
|
|
5535
5760
|
const targetId = getPrimaryKeyName(targetCollection);
|
|
5536
5761
|
schemaContent += `export const ${tableVarName} = ${tableCreator}(\"${baseTableName}\", {\n`;
|
|
@@ -5557,7 +5782,7 @@ var generateSchema = async (collections, stripPolicies = false) => {
|
|
|
5557
5782
|
const columnString = getDrizzleColumn(propName, prop, collection, collections);
|
|
5558
5783
|
if (columnString) columns.add(columnString);
|
|
5559
5784
|
});
|
|
5560
|
-
if (!Array.from(columns).some((col) => col.includes(".primaryKey()"))) columns.add(" id:
|
|
5785
|
+
if (!Array.from(columns).some((col) => col.includes(".primaryKey()"))) columns.add(" id: text(\"id\").primaryKey()");
|
|
5561
5786
|
schemaContent += `${Array.from(columns).join(",\n")}`;
|
|
5562
5787
|
const securityRules = getEffectiveSecurityRules(collection);
|
|
5563
5788
|
if (!stripPolicies && securityRules.length > 0) {
|
|
@@ -20252,7 +20477,7 @@ function createPostgresBootstrapper(pgConfig) {
|
|
|
20252
20477
|
*/
|
|
20253
20478
|
async ensureCollectionSchema(collections, driverResult, log) {
|
|
20254
20479
|
const internals = driverResult.internals;
|
|
20255
|
-
const { ensureCollectionTables } = await import("./ensure-collection-tables-
|
|
20480
|
+
const { ensureCollectionTables } = await import("./ensure-collection-tables-BVvtkkRm.js");
|
|
20256
20481
|
return { applied: (await ensureCollectionTables({ async query(text) {
|
|
20257
20482
|
const result = await internals.db.execute(sql.raw(text));
|
|
20258
20483
|
return { rows: result.rows ?? (Array.isArray(result) ? result : []) };
|