@rebasepro/server-postgres 0.11.1-canary.gfd39654 → 0.12.1-canary.g181d0fe
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/PostgresBootstrapper.d.ts +8 -0
- package/dist/auth/services.d.ts +16 -0
- package/dist/backup-service-DH9kPg-E.js +8866 -0
- package/dist/backup-service-DH9kPg-E.js.map +1 -0
- package/dist/collections/buildRegistry.d.ts +1 -1
- package/dist/connection-B5Wndbr1.js +196 -0
- package/dist/connection-B5Wndbr1.js.map +1 -0
- package/dist/ensure-collection-policies-CT-zIUWA.js +57 -0
- package/dist/ensure-collection-policies-CT-zIUWA.js.map +1 -0
- package/dist/{ensure-collection-tables-DGMYK0fr.js → ensure-collection-tables-Vu-GRELM.js} +83 -6
- package/dist/ensure-collection-tables-Vu-GRELM.js.map +1 -0
- package/dist/history/HistoryService.d.ts +9 -29
- package/dist/index.es.js +773 -9607
- package/dist/index.es.js.map +1 -1
- package/dist/schema/auth-schema.d.ts +83 -144
- package/dist/schema/dynamic-tables.d.ts +1 -1
- package/dist/schema/ensure-collection-policies.d.ts +60 -0
- package/dist/schema/generate-postgres-ddl-logic.d.ts +43 -1
- package/dist/schema/introspect-runtime.d.ts +1 -1
- package/dist/services/FetchService.d.ts +36 -1
- package/dist/services/row-pipeline.d.ts +3 -1
- package/dist/{src-3VmUJ8Xn.js → src-DihrDFuP.js} +354 -165
- package/dist/src-DihrDFuP.js.map +1 -0
- package/dist/{src-D5xBTl32.js → src-DoU9yPqq.js} +79 -189
- package/dist/src-DoU9yPqq.js.map +1 -0
- package/dist/utils/drizzle-conditions.d.ts +157 -3
- package/dist/utils/pg-error-utils.d.ts +25 -3
- package/dist/websocket-BKcGvILX.js +528 -0
- package/dist/websocket-BKcGvILX.js.map +1 -0
- package/package.json +14 -14
- package/src/PostgresAdapter.ts +14 -0
- package/src/PostgresBootstrapper.ts +127 -18
- package/src/auth/ensure-tables.ts +164 -9
- package/src/auth/services.ts +21 -2
- package/src/collections/buildRegistry.ts +1 -1
- package/src/history/HistoryService.ts +13 -31
- package/src/schema/auth-schema.ts +30 -19
- package/src/schema/dynamic-tables.ts +1 -1
- package/src/schema/ensure-collection-policies.ts +105 -0
- package/src/schema/generate-drizzle-schema-logic.ts +17 -5
- package/src/schema/generate-postgres-ddl-logic.ts +100 -13
- package/src/schema/introspect-runtime.ts +1 -1
- package/src/services/FetchService.ts +79 -11
- package/src/services/row-pipeline.ts +3 -1
- package/src/utils/drizzle-conditions.ts +509 -45
- package/src/utils/pg-error-utils.ts +98 -3
- package/dist/chunk-DSJWtz9O.js +0 -40
- package/dist/ensure-collection-tables-DGMYK0fr.js.map +0 -1
- package/dist/src-3VmUJ8Xn.js.map +0 -1
- package/dist/src-D5xBTl32.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CollectionConfig, NumberProperty, Property, ResolvedRelation, RelationProperty, SecurityOperation, SecurityRule, StringProperty, isPostgresCollectionConfig, DateProperty, ArrayProperty, MapProperty, ReferenceProperty, VectorProperty, BinaryProperty, isManyToMany, type ResolvedManyToMany, type ResolvedBelongsTo } from "@rebasepro/types";
|
|
2
|
-
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig } from "@rebasepro/common";
|
|
2
|
+
import { getEnumVarName, getTableName, resolveCollectionRelations, findRelation, securityRuleToConditions, policyToPostgres, getEffectiveSecurityRules, getInjectedSecurityRules, resolveJunctionSpecs, getJunctionSecurityRules, getJunctionCollectionConfig, resolveStringColumnLength } from "@rebasepro/common";
|
|
3
3
|
import { toSnakeCase, getPolicyNamesForRule } from "@rebasepro/utils";
|
|
4
4
|
|
|
5
5
|
// --- Helper Functions ---
|
|
@@ -45,7 +45,28 @@ export const isIdProperty = (propName: string, prop: Property, collection: Colle
|
|
|
45
45
|
|
|
46
46
|
type ResolveCollection = (slug: string) => CollectionConfig | undefined;
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Render statements produced by {@link generatePolicyStatements} back into the
|
|
50
|
+
* exact string the DDL/policies files have always carried: each statement on
|
|
51
|
+
* its own line, terminated by a newline. Keeping the string form derived from
|
|
52
|
+
* the statement array means the two can never drift — the boot-time applier and
|
|
53
|
+
* the generated `policies.sql` emit the same SQL, from the same source.
|
|
54
|
+
*/
|
|
55
|
+
const statementsToDdl = (statements: string[]): string => statements.map(s => `${s}\n`).join("");
|
|
56
|
+
|
|
57
|
+
const generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string =>
|
|
58
|
+
statementsToDdl(generatePolicyStatements(collection, rule, resolveCollection));
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The individual SQL statements a single security rule compiles to: a
|
|
62
|
+
* `DROP POLICY IF EXISTS` / `CREATE POLICY` pair per operation, each a complete
|
|
63
|
+
* statement (terminated by `;`, no trailing newline).
|
|
64
|
+
*
|
|
65
|
+
* This is the primitive the boot-time RLS applier runs one statement at a time
|
|
66
|
+
* (the runtime's DB handle speaks the extended query protocol, which forbids
|
|
67
|
+
* multiple commands in one execute), while `db push` writes the joined string.
|
|
68
|
+
*/
|
|
69
|
+
export const generatePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, resolveCollection: ResolveCollection): string[] => {
|
|
49
70
|
const tableName = getTableName(collection);
|
|
50
71
|
const ops: readonly SecurityOperation[] = rule.operations && rule.operations.length > 0
|
|
51
72
|
? rule.operations
|
|
@@ -53,12 +74,12 @@ const generatePolicyDdl = (collection: CollectionConfig, rule: SecurityRule, res
|
|
|
53
74
|
|
|
54
75
|
const policyNames = getPolicyNamesForRule(rule, tableName);
|
|
55
76
|
|
|
56
|
-
return ops.
|
|
57
|
-
return
|
|
58
|
-
})
|
|
77
|
+
return ops.flatMap((op, opIdx) => {
|
|
78
|
+
return generateSinglePolicyStatements(collection, rule, op, policyNames[opIdx], resolveCollection);
|
|
79
|
+
});
|
|
59
80
|
};
|
|
60
81
|
|
|
61
|
-
const
|
|
82
|
+
const generateSinglePolicyStatements = (collection: CollectionConfig, rule: SecurityRule, operation: SecurityOperation, policyName: string, resolveCollection: ResolveCollection): string[] => {
|
|
62
83
|
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
63
84
|
const tableName = getTableName(collection);
|
|
64
85
|
const mode = (rule.mode ?? "permissive").toUpperCase();
|
|
@@ -83,11 +104,12 @@ const generateSinglePolicyDdl = (collection: CollectionConfig, rule: SecurityRul
|
|
|
83
104
|
withCheckClause = "false";
|
|
84
105
|
}
|
|
85
106
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (usingClause)
|
|
89
|
-
if (withCheckClause)
|
|
90
|
-
|
|
107
|
+
const drop = `DROP POLICY IF EXISTS "${policyName}" ON "${schema}"."${tableName}";`;
|
|
108
|
+
let create = `CREATE POLICY "${policyName}" ON "${schema}"."${tableName}" AS ${mode} FOR ${operationUpper} TO ${pgRoles.map(r => `"${r}"`).join(", ")}`;
|
|
109
|
+
if (usingClause) create += ` USING (${usingClause})`;
|
|
110
|
+
if (withCheckClause) create += ` WITH CHECK (${withCheckClause})`;
|
|
111
|
+
create += ";";
|
|
112
|
+
return [drop, create];
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
export const getSqlColumnType = (propName: string, prop: Property, collection: CollectionConfig, collections: CollectionConfig[]): string => {
|
|
@@ -103,11 +125,15 @@ export const getSqlColumnType = (propName: string, prop: Property, collection: C
|
|
|
103
125
|
if (stringProp.isId === "uuid" || stringProp.columnType === "uuid") {
|
|
104
126
|
return "UUID";
|
|
105
127
|
}
|
|
128
|
+
// Width comes from `validation.max` when the property states one.
|
|
129
|
+
// It used to be a hardcoded 255 here and *absent* on the Drizzle
|
|
130
|
+
// path, so the same property produced a bounded column down one
|
|
131
|
+
// generator and an unbounded one down the other.
|
|
106
132
|
if (stringProp.columnType === "char") {
|
|
107
|
-
return
|
|
133
|
+
return `CHAR(${resolveStringColumnLength(stringProp)})`;
|
|
108
134
|
}
|
|
109
135
|
if (stringProp.columnType === "varchar") {
|
|
110
|
-
return
|
|
136
|
+
return `VARCHAR(${resolveStringColumnLength(stringProp)})`;
|
|
111
137
|
}
|
|
112
138
|
// `text` is the default. The two generators disagreed here before:
|
|
113
139
|
// this one emitted VARCHAR(255) while the drizzle path emitted a bare
|
|
@@ -470,6 +496,67 @@ export const generatePostgresDdl = async (
|
|
|
470
496
|
return ddl;
|
|
471
497
|
};
|
|
472
498
|
|
|
499
|
+
/** The RLS statements one declared collection's table needs, ready to run. */
|
|
500
|
+
export interface CollectionPolicyPlan {
|
|
501
|
+
/** The table's schema (e.g. `public`, `rebase`). */
|
|
502
|
+
schema: string;
|
|
503
|
+
/** The bare table name, no schema prefix. */
|
|
504
|
+
table: string;
|
|
505
|
+
/** `schema.table` — matches the keys `readExistingSchema` returns. */
|
|
506
|
+
qualified: string;
|
|
507
|
+
/** `ALTER TABLE … ENABLE ROW LEVEL SECURITY;` — locked by default. */
|
|
508
|
+
enableRls: string;
|
|
509
|
+
/** `DROP POLICY IF EXISTS` / `CREATE POLICY` statements, in order. */
|
|
510
|
+
policyStatements: string[];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* The per-table RLS plan for the *declared* collections, as executable
|
|
515
|
+
* statements — what the managed runtime applies at boot so a freshly
|
|
516
|
+
* provisioned tenant database serves data instead of 401ing every read.
|
|
517
|
+
*
|
|
518
|
+
* Mirrors the non-junction half of {@link generatePostgresPoliciesDdl} exactly
|
|
519
|
+
* (same `generatePolicyStatements`, same enable-RLS, same effective rules), so
|
|
520
|
+
* boot and `db push` produce identical policies from identical collections.
|
|
521
|
+
*
|
|
522
|
+
* Junction tables are deliberately excluded: they are derived from `through`
|
|
523
|
+
* relations, not declared collections, and the boot-time *table* creator
|
|
524
|
+
* (`ensureCollectionTables`) does not create them either — enabling RLS on a
|
|
525
|
+
* table that boot never created would fail. Their RLS stays a `db push` /
|
|
526
|
+
* `db migrate` concern, which is where those tables get created in the first
|
|
527
|
+
* place. `db push` still applies junction policies via the string generator.
|
|
528
|
+
*/
|
|
529
|
+
export const planCollectionPolicies = (collections: CollectionConfig[]): CollectionPolicyPlan[] => {
|
|
530
|
+
const resolveCollection: ResolveCollection = (slug) => collections.find(c => c.slug === slug || getTableName(c) === slug);
|
|
531
|
+
const plans: CollectionPolicyPlan[] = [];
|
|
532
|
+
const seen = new Set<string>();
|
|
533
|
+
|
|
534
|
+
for (const collection of collections) {
|
|
535
|
+
const tableName = getTableName(collection);
|
|
536
|
+
if (!tableName) continue;
|
|
537
|
+
const schema = isPostgresCollectionConfig(collection) && collection.schema ? collection.schema : "public";
|
|
538
|
+
const baseTableName = tableName.includes(".") ? tableName.split(".").pop()! : tableName;
|
|
539
|
+
const qualified = `${schema}.${baseTableName}`;
|
|
540
|
+
if (seen.has(qualified)) continue;
|
|
541
|
+
seen.add(qualified);
|
|
542
|
+
|
|
543
|
+
const policyStatements: string[] = [];
|
|
544
|
+
for (const rule of getEffectiveSecurityRules(collection)) {
|
|
545
|
+
policyStatements.push(...generatePolicyStatements(collection, rule, resolveCollection));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
plans.push({
|
|
549
|
+
schema,
|
|
550
|
+
table: baseTableName,
|
|
551
|
+
qualified,
|
|
552
|
+
enableRls: `ALTER TABLE "${schema}"."${baseTableName}" ENABLE ROW LEVEL SECURITY;`,
|
|
553
|
+
policyStatements
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return plans;
|
|
558
|
+
};
|
|
559
|
+
|
|
473
560
|
export const generatePostgresPoliciesDdl = (collections: CollectionConfig[]): string => {
|
|
474
561
|
let ddl = "-- This file contains RLS policies generated by Rebase. Applied separately from migrations.\n\n";
|
|
475
562
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* single config file.
|
|
8
8
|
*
|
|
9
9
|
* Distinct from `introspect-db.ts`, which runs the same queries but emits
|
|
10
|
-
* TypeScript *source* for a developer to edit and commit (
|
|
10
|
+
* TypeScript *source* for a developer to edit and commit (declared collections). The two
|
|
11
11
|
* share the mapping helpers in `introspect-db-logic.ts` so a table is described
|
|
12
12
|
* the same way whether it was generated or introspected.
|
|
13
13
|
*/
|
|
@@ -3,10 +3,12 @@ import { AnyPgColumn, PgTable } from "drizzle-orm/pg-core";
|
|
|
3
3
|
import { CollectionConfig, FilterValues, ResolvedRelation, LogicalCondition, isManyToMany } from "@rebasepro/types";
|
|
4
4
|
import type { VectorSearchParams } from "@rebasepro/types";
|
|
5
5
|
import { resolveCollectionRelations, findRelation, createRelationRef, createRelationRefWithData } from "@rebasepro/common";
|
|
6
|
-
import {
|
|
6
|
+
import { generateForeignKeyName } from "@rebasepro/utils";
|
|
7
|
+
import { DrizzleConditionBuilder, type FilterCompilationOptions } from "../utils/drizzle-conditions";
|
|
7
8
|
import {
|
|
8
9
|
getCollectionByPath,
|
|
9
10
|
getTableForCollection,
|
|
11
|
+
getPrimaryKeys,
|
|
10
12
|
requirePrimaryKeys,
|
|
11
13
|
deriveRowAddress,
|
|
12
14
|
parseIdValues,
|
|
@@ -45,6 +47,45 @@ export class FetchService {
|
|
|
45
47
|
return query?.[tableName] as RelationalQueryBuilder<TablesRelationalConfig, TableRelationalConfig> | undefined;
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
/**
|
|
51
|
+
* The context the condition builder needs to compile a filter key that is
|
|
52
|
+
* not a column name outright.
|
|
53
|
+
*
|
|
54
|
+
* Two such keys. An owning relation's key resolves through the collection's
|
|
55
|
+
* relations to its foreign-key column; a relation whose link lives on the
|
|
56
|
+
* target table or in a junction resolves to a correlated `EXISTS`, which
|
|
57
|
+
* needs the registry to reach that other table and this table's key column
|
|
58
|
+
* to correlate back.
|
|
59
|
+
*
|
|
60
|
+
* Looked up rather than passed: every read path already has the path, only
|
|
61
|
+
* some have the collection, and a path that names no registered collection
|
|
62
|
+
* (a nested/derived one) is not an error here — the builder simply falls
|
|
63
|
+
* back to guessing the default key shapes, and a relation filter it cannot
|
|
64
|
+
* compile stays unresolvable and so fails closed.
|
|
65
|
+
*/
|
|
66
|
+
private filterContext(collectionPath: string, table: PgTable<any>): FilterCompilationOptions {
|
|
67
|
+
const collection = this.registry.getCollectionByPath(collectionPath) ?? undefined;
|
|
68
|
+
return {
|
|
69
|
+
collection,
|
|
70
|
+
registry: this.registry,
|
|
71
|
+
sourceIdColumn: collection ? this.resolveIdColumn(collection, table) : undefined
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The table column this collection's rows are keyed by, or `undefined`.
|
|
77
|
+
*
|
|
78
|
+
* `getPrimaryKeys` rather than `requirePrimaryKeys`: a collection with no
|
|
79
|
+
* resolvable key is not an error on the filter path — it only means the
|
|
80
|
+
* relation filters that would correlate on it cannot be compiled, which
|
|
81
|
+
* the builder already handles by failing that field closed.
|
|
82
|
+
*/
|
|
83
|
+
private resolveIdColumn(collection: CollectionConfig, table: PgTable<any>): AnyPgColumn | undefined {
|
|
84
|
+
const [idInfo] = getPrimaryKeys(collection, this.registry);
|
|
85
|
+
if (!idInfo) return undefined;
|
|
86
|
+
return table[idInfo.fieldName as keyof typeof table] as AnyPgColumn | undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
48
89
|
/**
|
|
49
90
|
* Build filter conditions from FilterValues
|
|
50
91
|
* Delegates to DrizzleConditionBuilder.buildFilterConditions
|
|
@@ -54,7 +95,9 @@ export class FetchService {
|
|
|
54
95
|
table: PgTable<any>,
|
|
55
96
|
collectionPath: string
|
|
56
97
|
): SQL[] {
|
|
57
|
-
return DrizzleConditionBuilder.buildFilterConditions(
|
|
98
|
+
return DrizzleConditionBuilder.buildFilterConditions(
|
|
99
|
+
filter, table, collectionPath, this.filterContext(collectionPath, table)
|
|
100
|
+
);
|
|
58
101
|
}
|
|
59
102
|
|
|
60
103
|
// =============================================================
|
|
@@ -64,20 +107,45 @@ export class FetchService {
|
|
|
64
107
|
/**
|
|
65
108
|
* Resolves the correct Drizzle column for sorting.
|
|
66
109
|
* Automatically maps owning relation property keys to their underlying foreign key column.
|
|
110
|
+
*
|
|
111
|
+
* The relation's own `localKey` is the authority for that foreign key, not
|
|
112
|
+
* `<field>_id`. The default local key comes from `generateForeignKeyName`,
|
|
113
|
+
* which snake-cases *and singularises* — `userProfile` → `user_profile_id`,
|
|
114
|
+
* `users` → `user_id` — and an author can override it outright. A wrong
|
|
115
|
+
* guess resolves to nothing, the caller drops the `ORDER BY`, and the rows
|
|
116
|
+
* come back in whatever order Postgres pleases: paging over that repeats
|
|
117
|
+
* and skips rows rather than erroring. The guesses stay, last, for a
|
|
118
|
+
* caller that hands over no collection to resolve against.
|
|
67
119
|
*/
|
|
68
120
|
private resolveOrderByField(
|
|
69
121
|
table: PgTable<any>,
|
|
70
122
|
orderBy: string,
|
|
71
123
|
collection?: CollectionConfig
|
|
72
124
|
): AnyPgColumn | undefined {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
125
|
+
const columnAt = (key: string): AnyPgColumn | undefined =>
|
|
126
|
+
(key in table ? table[key as keyof typeof table] as AnyPgColumn : undefined) || undefined;
|
|
127
|
+
|
|
128
|
+
const direct = columnAt(orderBy);
|
|
129
|
+
if (direct) return direct;
|
|
130
|
+
|
|
131
|
+
// Owning relation, resolved: the relation names its own local key.
|
|
132
|
+
if (collection) {
|
|
133
|
+
const relation = resolveCollectionRelations(collection)[orderBy];
|
|
134
|
+
if (relation?.kind === "belongsTo") {
|
|
135
|
+
const foreignKey = columnAt(relation.localKey);
|
|
136
|
+
if (foreignKey) return foreignKey;
|
|
78
137
|
}
|
|
79
138
|
}
|
|
80
|
-
|
|
139
|
+
|
|
140
|
+
// No collection in hand — the two shapes an owning relation's key takes
|
|
141
|
+
// by default (e.g. `project` → `project_id`, `userProfile` →
|
|
142
|
+
// `user_profile_id`).
|
|
143
|
+
for (const guess of [`${orderBy}_id`, generateForeignKeyName(orderBy)]) {
|
|
144
|
+
const foreignKey = columnAt(guess);
|
|
145
|
+
if (foreignKey) return foreignKey;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return undefined;
|
|
81
149
|
}
|
|
82
150
|
|
|
83
151
|
/**
|
|
@@ -85,7 +153,7 @@ export class FetchService {
|
|
|
85
153
|
* Converts collection relations to a Drizzle-compatible `with` object.
|
|
86
154
|
*
|
|
87
155
|
* When `include` is provided, only those relations are loaded.
|
|
88
|
-
* When `include` is absent, ALL relations are loaded (
|
|
156
|
+
* When `include` is absent, ALL relations are loaded (the admin path).
|
|
89
157
|
*
|
|
90
158
|
* Automatically detects many-to-many junction tables and nests
|
|
91
159
|
* the target relation so actual row data is returned.
|
|
@@ -311,7 +379,7 @@ export class FetchService {
|
|
|
311
379
|
}
|
|
312
380
|
|
|
313
381
|
if (options.logical) {
|
|
314
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
382
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
315
383
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
316
384
|
}
|
|
317
385
|
|
|
@@ -656,7 +724,7 @@ _distance: vectorMeta.distanceSelect }).from(table).$dynamic()
|
|
|
656
724
|
}
|
|
657
725
|
|
|
658
726
|
if (options.logical) {
|
|
659
|
-
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath);
|
|
727
|
+
const logicalCondition = DrizzleConditionBuilder.buildLogicalConditions(options.logical, table, collectionPath, this.filterContext(collectionPath, table));
|
|
660
728
|
if (logicalCondition) allConditions.push(logicalCondition);
|
|
661
729
|
}
|
|
662
730
|
|
|
@@ -14,7 +14,9 @@ import { PostgresCollectionRegistry } from "../collections/PostgresCollectionReg
|
|
|
14
14
|
*
|
|
15
15
|
* - `"ref"` — a `{ id, path, __type: "relation" }` reference carrying the
|
|
16
16
|
* target's values. This is what the admin renders.
|
|
17
|
-
* - `"inline"` — the target's own columns, flat. This is what REST serves
|
|
17
|
+
* - `"inline"` — the target's own columns, flat. This is what REST serves, and
|
|
18
|
+
* — since the in-process SDK reads through the same pipeline — what
|
|
19
|
+
* `rebase.data` / `context.data` serve too. A developer never sees a ref.
|
|
18
20
|
*
|
|
19
21
|
* They used to be two functions that happened to agree, and the agreement was
|
|
20
22
|
* not enforced by anything: the row-identity bug had to be fixed five times
|