@rebasepro/common 0.17.3 → 0.18.1

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.
Files changed (58) hide show
  1. package/README.md +4 -0
  2. package/dist/collections/CollectionRegistry.d.ts +1 -1
  3. package/dist/collections/default-collections.d.ts +15 -84
  4. package/dist/data/buildRebaseData.d.ts +1 -1
  5. package/dist/data/filter-dialect.d.ts +11 -0
  6. package/dist/data/sort-dialect.d.ts +15 -3
  7. package/dist/index.es.js +375 -63
  8. package/dist/index.es.js.map +1 -1
  9. package/dist/util/builders.d.ts +69 -24
  10. package/dist/util/callback-errors.d.ts +77 -0
  11. package/dist/util/callback-errors.test.d.ts +1 -0
  12. package/dist/util/index.d.ts +1 -0
  13. package/dist/util/policy/evaluatePolicy.d.ts +6 -0
  14. package/dist/util/relations.d.ts +41 -0
  15. package/dist/util/table-name.test.d.ts +1 -0
  16. package/package.json +26 -22
  17. package/src/collections/CollectionRegistry.ts +0 -485
  18. package/src/collections/default-collections.ts +0 -109
  19. package/src/collections/index.ts +0 -2
  20. package/src/data/buildRebaseData.ts +0 -816
  21. package/src/data/buildRoutedRebaseData.ts +0 -103
  22. package/src/data/filter-conditions.ts +0 -46
  23. package/src/data/filter-dialect.ts +0 -737
  24. package/src/data/paginate.ts +0 -334
  25. package/src/data/query_builder.ts +0 -176
  26. package/src/data/resolveDataSource.ts +0 -135
  27. package/src/data/sort-dialect.ts +0 -237
  28. package/src/index.ts +0 -11
  29. package/src/table-classification.ts +0 -109
  30. package/src/types/json-logic-js.d.ts +0 -8
  31. package/src/util/auth-default-policies.ts +0 -215
  32. package/src/util/builders.ts +0 -82
  33. package/src/util/callbacks.ts +0 -122
  34. package/src/util/collections.ts +0 -117
  35. package/src/util/common.ts +0 -2
  36. package/src/util/conditions.ts +0 -168
  37. package/src/util/email.ts +0 -32
  38. package/src/util/entities.ts +0 -282
  39. package/src/util/enums.ts +0 -26
  40. package/src/util/identity.ts +0 -202
  41. package/src/util/index.ts +0 -21
  42. package/src/util/internal-tables.test.ts +0 -188
  43. package/src/util/internal-tables.ts +0 -197
  44. package/src/util/junction-policies.ts +0 -355
  45. package/src/util/paths.ts +0 -27
  46. package/src/util/permissions.test.ts +0 -866
  47. package/src/util/permissions.ts +0 -206
  48. package/src/util/pg-column-to-property.ts +0 -377
  49. package/src/util/policy/evaluatePolicy.ts +0 -194
  50. package/src/util/policy/index.ts +0 -4
  51. package/src/util/policy/policyToPostgres.ts +0 -263
  52. package/src/util/policy/securityRuleToConditions.ts +0 -67
  53. package/src/util/policy/sqlToPolicy.ts +0 -422
  54. package/src/util/relations.ts +0 -236
  55. package/src/util/resolutions.ts +0 -534
  56. package/src/util/resolve-relation.ts +0 -243
  57. package/src/util/storage.ts +0 -177
  58. package/src/util/string-column-length.ts +0 -31
@@ -1,243 +0,0 @@
1
- import {
2
- CollectionConfig,
3
- Relation,
4
- ResolvedRelation
5
- } from "@rebasepro/types";
6
- import { generateForeignKeyName, toSnakeCase } from "@rebasepro/utils";
7
-
8
- import { getTableName } from "./relations";
9
-
10
- /**
11
- * Fill in a relation's defaults.
12
- *
13
- * This replaces `sanitizeRelation`, which had to work out *which kind of link
14
- * you meant* from whichever optional fields happened to be set — 194 lines of
15
- * it, including a pass that inspected the target collection's own relations to
16
- * decide whether a `many`/`inverse` pair was a one-to-many or the far side of a
17
- * many-to-many, wrapped in a `try/catch` that fell through to the wrong answer
18
- * when it could not tell. Two consumers running that logic at different moments
19
- * could reach different conclusions about the same relation.
20
- *
21
- * With the kind declared there is nothing to work out. What remains is
22
- * defaulting — a table name, a column name — which is deterministic, depends
23
- * only on the relation and its two endpoints, and cannot fail. That is why this
24
- * function returns rather than throws, and why it needs no cache to be
25
- * consistent.
26
- */
27
- export function resolveRelation(
28
- relation: Relation,
29
- sourceCollection: CollectionConfig,
30
- propertyKey?: string
31
- ): ResolvedRelation {
32
- const target = relation.target;
33
- if (typeof target !== "function") {
34
- throw new Error(
35
- `Relation${relation.relationName ? ` '${relation.relationName}'` : ""} on ` +
36
- `'${sourceCollection.slug}' has no \`target\`. Give it a thunk: \`target: () => otherCollection\`.`
37
- );
38
- }
39
-
40
- const targetCollection = callTarget(relation, sourceCollection, propertyKey, target);
41
-
42
- // The name is the address: the `include` key, the admin tab, and the
43
- // segment of a nested path. Declared name wins, then the declaring
44
- // property's key, then the target's slug.
45
- const relationName = relation.relationName ?? propertyKey ?? toSnakeCase(targetCollection.slug);
46
-
47
- const shared: Pick<ResolvedRelation, "relationName" | "target" | "targetSlug" | "onUpdate" | "onDelete" | "overrides" | "validation"> = {
48
- relationName,
49
- // Normalised, not the thunk as written. Resolution reads the target once
50
- // and every later consumer calls it again — the driver building a join,
51
- // the DDL and policy generators, the admin's relation fields — so
52
- // handing back the raw thunk would give all of them the module namespace
53
- // `callTarget` just looked past, and the fix would hold only for the
54
- // fields resolution happens to read here. Still lazy: same call at the
55
- // same moment, one unwrap on the way out.
56
- target: () => unwrapModuleNamespace(target()) as CollectionConfig,
57
- targetSlug: targetCollection.slug,
58
- onUpdate: relation.onUpdate,
59
- onDelete: relation.onDelete,
60
- overrides: relation.overrides,
61
- validation: relation.validation
62
- };
63
-
64
- const sourceName = toSnakeCase(sourceCollection.slug ?? sourceCollection.name);
65
-
66
- switch (relation.kind) {
67
- case "belongsTo":
68
- return {
69
- ...shared,
70
- kind: "belongsTo",
71
- cardinality: "one",
72
- writable: true,
73
- shared: false,
74
- localKey: relation.localKey ?? generateForeignKeyName(relationName)
75
- };
76
-
77
- case "hasOne":
78
- return {
79
- ...shared,
80
- kind: "hasOne",
81
- cardinality: "one",
82
- writable: true,
83
- shared: false,
84
- foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
85
- sourceKey: relation.sourceKey
86
- };
87
-
88
- case "hasMany":
89
- return {
90
- ...shared,
91
- kind: "hasMany",
92
- cardinality: "many",
93
- writable: true,
94
- shared: false,
95
- foreignKeyOnTarget: relation.foreignKeyOnTarget ?? generateForeignKeyName(sourceName),
96
- // Not defaulted: the source's primary key needs the driver's
97
- // schema to resolve, which resolution does not have. `undefined`
98
- // means "the primary key" — see `ResolvedHasMany.sourceKey`.
99
- sourceKey: relation.sourceKey
100
- };
101
-
102
- case "manyToMany": {
103
- const sourceTable = getTableName(sourceCollection);
104
- const targetTable = getTableName(targetCollection);
105
- return {
106
- ...shared,
107
- kind: "manyToMany",
108
- cardinality: "many",
109
- writable: true,
110
- shared: true,
111
- through: {
112
- // Sorted so both sides of the same link derive the same
113
- // table without having to agree in advance.
114
- table: relation.through?.table ?? [sourceTable, targetTable].sort().join("_"),
115
- sourceColumn: relation.through?.sourceColumn ?? generateForeignKeyName(sourceName),
116
- targetColumn: relation.through?.targetColumn ?? generateForeignKeyName(relationName)
117
- }
118
- };
119
- }
120
-
121
- case "via":
122
- return {
123
- ...shared,
124
- kind: "via",
125
- cardinality: relation.cardinality,
126
- writable: false,
127
- // A join chain reaches rows that other parents reach too, and
128
- // Rebase does not know which hop, if any, is a link it owns.
129
- shared: true,
130
- joinPath: relation.joinPath
131
- };
132
-
133
- default: {
134
- // Exhaustive: a new kind is a compile error here, not a silent
135
- // fall-through to whatever shape happened to match first.
136
- const exhaustive: never = relation;
137
- throw new Error(`Unknown relation kind: ${JSON.stringify(exhaustive)}`);
138
- }
139
- }
140
- }
141
-
142
- /** How this relation is addressed in an error message, before it has a resolved name. */
143
- function describe(relation: Relation, sourceCollection: CollectionConfig, propertyKey?: string): string {
144
- const name = relation.relationName ?? propertyKey;
145
- return `Relation${name ? ` '${name}'` : ""} on '${sourceCollection.slug}'`;
146
- }
147
-
148
- /**
149
- * A module namespace, unwrapped to the collection it exports.
150
- *
151
- * A cycle transpiled to CommonJS does not hand the importing module the
152
- * *default export* — it hands it the module object, `{ __esModule: true,
153
- * default: … }`, captured before the exporting module finished evaluating. The
154
- * `default` slot fills in later, so by the time a lazy `target` thunk runs the
155
- * collection is sitting right there, one level down. Returning the namespace is
156
- * never a thing a thunk means to do, and there is exactly one reading of it.
157
- *
158
- * Only unwrapped when the inner value is itself a collection: a `default` that
159
- * is not one is a genuinely wrong thunk, and it should reach the error below
160
- * rather than be quietly swapped in.
161
- */
162
- function unwrapModuleNamespace(value: unknown): unknown {
163
- if (!value || typeof value !== "object") return value;
164
- if ((value as { slug?: unknown }).slug) return value;
165
- const inner = (value as { default?: unknown }).default;
166
- return inner && typeof inner === "object" && (inner as { slug?: unknown }).slug ? inner : value;
167
- }
168
-
169
- /**
170
- * Call the `target` thunk, and translate the ways an import cycle breaks it into
171
- * an error that names the cause — or, where the value is recoverable, into the
172
- * collection the thunk meant.
173
- *
174
- * The thunk exists to defer the reference until every module has finished
175
- * evaluating, and for a cycle that closes at import time it does. Two cycles
176
- * leave the binding permanently unusable:
177
- *
178
- * - **ESM/TDZ.** `const` and `class` bindings in a not-yet-evaluated module are
179
- * in the temporal dead zone, so reading one throws `ReferenceError: x is not
180
- * defined`. The stack points at the thunk — a one-line arrow function that is
181
- * obviously fine — and says nothing about the cycle that made it throw.
182
- * - **CJS interop, unresolved.** The half-initialised module object has no
183
- * `default` yet, the import resolves to `undefined`, and the thunk returns it
184
- * without complaint. That one used to surface here as "did not resolve to a
185
- * collection", which is true and unhelpful.
186
- *
187
- * Both mean the same thing, and the fix for both is the same: break the cycle,
188
- * or move the relation into the collection that does not close it.
189
- *
190
- * A third shape is *not* an error, and used to be reported as one. A loader that
191
- * transpiles ESM to CJS — jiti, which is what `rebase generate-sdk` and
192
- * `rebase build` load collections with — gives the module entered second in a
193
- * cycle a namespace object rather than the default export, and never replaces it
194
- * with a live binding. The thunk then returns `{ __esModule: true, default: … }`
195
- * holding the fully-initialised collection. Native ESM resolves the same thunk
196
- * to the collection directly, so this was a loader artefact reported as an
197
- * authoring mistake, and the advice it gave — make the target a lazy thunk — was
198
- * already satisfied by the code it was rejecting. Bidirectional relations make
199
- * these cycles unavoidable, and the lazy thunk is this framework's own answer to
200
- * them, so {@link unwrapModuleNamespace} takes the collection and moves on.
201
- */
202
- function callTarget(
203
- relation: Relation,
204
- sourceCollection: CollectionConfig,
205
- propertyKey: string | undefined,
206
- target: Relation["target"]
207
- ): ReturnType<Relation["target"]> {
208
- let targetCollection: ReturnType<Relation["target"]> | undefined;
209
- try {
210
- targetCollection = unwrapModuleNamespace(target()) as ReturnType<Relation["target"]>;
211
- } catch (error) {
212
- // A ReferenceError from inside the thunk is a binding that was never
213
- // initialised — nothing else in a one-expression arrow can raise one.
214
- if (error instanceof ReferenceError) {
215
- throw new Error(
216
- `${describe(relation, sourceCollection, propertyKey)} targets a collection that is not ` +
217
- `initialized yet — almost always an import cycle between the two collection files. ` +
218
- `Break the cycle (move the shared piece into a third module, or import the target ` +
219
- `lazily) so the target's module finishes evaluating before the registry is built.`,
220
- { cause: error }
221
- );
222
- }
223
- throw error;
224
- }
225
-
226
- if (!targetCollection?.slug) {
227
- throw new Error(
228
- `${describe(relation, sourceCollection, propertyKey)} has a \`target\` that resolved to ` +
229
- `${targetCollection === undefined ? "`undefined`" : "something that is not a collection"}. ` +
230
- (targetCollection === undefined
231
- ? "Under CommonJS interop an import cycle resolves the default import to `undefined`, " +
232
- "so check whether this collection and its target import each other. Otherwise the thunk " +
233
- "is returning the wrong value — it must return the collection itself, not a promise or a module."
234
- : typeof (targetCollection as { then?: unknown }).then === "function"
235
- ? "The thunk returned a promise — `target: () => import(\"./other\")` is asynchronous. " +
236
- "Import the collection at the top of the file and return the binding: " +
237
- "`target: () => otherCollection`."
238
- : "The thunk must return a collection config with a `slug`.")
239
- );
240
- }
241
-
242
- return targetCollection;
243
- }
@@ -1,177 +0,0 @@
1
- import { ArrayProperty, EntityValues, StorageConfig, StorageSource, StorageSourceRegistry, StringProperty, UploadedFileContext } from "@rebasepro/types";
2
- import { randomString } from "@rebasepro/utils";
3
-
4
- /**
5
- * Resolve the {@link StorageSource} to use for a property, given the key
6
- * referenced by `StorageConfig.storageSource`.
7
- *
8
- * Resolution priority:
9
- * 1. No `sourceKey` → the default source (backward compatible).
10
- * 2. An explicit {@link StorageSourceRegistry} (e.g. `client.storageRegistry`).
11
- * 3. A `sources` lookup map (e.g. the `StorageSourcesContext`).
12
- * 4. Fall back to the default source.
13
- *
14
- * Shared by the upload hook, the markdown editor, and the read-only previews
15
- * so the resolution logic lives in one place.
16
- *
17
- * @group Storage
18
- */
19
- export function resolveStorageSource(params: {
20
- /** Key from `StorageConfig.storageSource`. */
21
- sourceKey?: string | null;
22
- /** Built sources keyed by storage-source key (e.g. from context). */
23
- sources?: Record<string, StorageSource>;
24
- /** Optional explicit registry — takes precedence over `sources`. */
25
- registry?: StorageSourceRegistry;
26
- /** Default source, used when no key is set or the key cannot be resolved. */
27
- defaultSource: StorageSource;
28
- }): StorageSource {
29
- const { sourceKey, sources, registry, defaultSource } = params;
30
- if (!sourceKey) return defaultSource;
31
- if (registry) return registry.getOrDefault(sourceKey);
32
- const fromSources = sources?.[sourceKey];
33
- if (fromSources) return fromSources;
34
- return defaultSource;
35
- }
36
-
37
- interface ResolveFilenameStringParams<M extends Record<string, unknown>> {
38
- input: string | ((context: UploadedFileContext) => (Promise<string> | string));
39
- storage: StorageConfig;
40
- values: EntityValues<M>;
41
- entityId?: string | number;
42
- path?: string;
43
- property: StringProperty | ArrayProperty,
44
- file: File;
45
- propertyKey: string;
46
- }
47
-
48
- export async function resolveStorageFilenameString<M extends Record<string, unknown>>(
49
- {
50
- input,
51
- storage,
52
- values,
53
- entityId,
54
- path,
55
- property,
56
- file,
57
- propertyKey
58
- }: ResolveFilenameStringParams<M>): Promise<string> {
59
- let result;
60
-
61
- if (typeof input === "function") {
62
- result = await input({
63
- path,
64
- entityId,
65
- values,
66
- property,
67
- file,
68
- storage,
69
- propertyKey
70
- });
71
- if (!result)
72
- console.warn("Storage callback returned empty result. Using default name value")
73
- } else {
74
- result = replacePlaceholders({
75
- file,
76
- input,
77
- entityId,
78
- propertyKey,
79
- path
80
- });
81
- }
82
-
83
- if (!result)
84
- result = randomString() + "_" + file.name;
85
-
86
- return result;
87
- }
88
-
89
- interface ResolveStoragePathStringParams<M extends Record<string, unknown>> {
90
- input: string | ((context: UploadedFileContext) => string);
91
- storage: StorageConfig;
92
- values: EntityValues<M>;
93
- entityId?: string | number;
94
- path?: string;
95
- property: StringProperty | ArrayProperty;
96
- file: File;
97
- propertyKey: string;
98
- }
99
-
100
- export function resolveStoragePathString<M extends Record<string, unknown>>(
101
- {
102
- input,
103
- storage,
104
- values,
105
- entityId,
106
- path,
107
- property,
108
- file,
109
- propertyKey
110
- }: ResolveStoragePathStringParams<M>): string {
111
- let result;
112
- if (typeof input === "function") {
113
- result = input({
114
- path,
115
- entityId,
116
- values,
117
- property,
118
- file,
119
- storage,
120
- propertyKey
121
- });
122
- if (!result)
123
- console.warn("Storage callback returned empty result. Using default name value")
124
- } else {
125
- result = replacePlaceholders({
126
- file,
127
- input,
128
- entityId,
129
- propertyKey,
130
- path
131
- });
132
- }
133
-
134
- if (!result)
135
- result = randomString() + "_" + file.name;
136
-
137
- return result;
138
- }
139
-
140
- interface Placeholders {
141
- file: File;
142
- input: string;
143
- entityId?: string | number;
144
- propertyKey: string;
145
- path?: string;
146
- }
147
-
148
- function replacePlaceholders({
149
- file,
150
- input,
151
- entityId,
152
- propertyKey,
153
- path
154
- }: Placeholders) {
155
- const ext = file.name.split(".").pop();
156
- let result = input
157
- .replace("{propertyKey}", propertyKey)
158
- .replace("{rand}", randomString())
159
- .replace("{file}", file.name)
160
- .replace("{file.type}", file.type);
161
- if (entityId) {
162
- result = result.replace("{entityId}", String(entityId));
163
- }
164
- if (path) {
165
- result = result.replace("{path}", path);
166
- }
167
- if (ext) {
168
- result = result.replace("{file.ext}", ext);
169
- const name = file.name.replace(`.${ext}`, "");
170
- result = result.replace("{file.name}", name)
171
- }
172
-
173
- if (!result)
174
- result = randomString() + "_" + file.name;
175
-
176
- return result;
177
- }
@@ -1,31 +0,0 @@
1
- import type { StringProperty } from "@rebasepro/types";
2
-
3
- /**
4
- * The length a bounded string column is declared with when the property does
5
- * not say. Historical: it is what the DDL generator hardcoded, kept so that
6
- * regenerating an existing schema does not silently redefine its columns.
7
- */
8
- export const DEFAULT_STRING_COLUMN_LENGTH = 255;
9
-
10
- /**
11
- * How wide a `varchar`/`char` column should be for a given property.
12
- *
13
- * One definition, three call sites, because they used to disagree. For the same
14
- * `columnType: "varchar"` property the DDL generator emitted `VARCHAR(255)`
15
- * while the Drizzle generator emitted a bare `varchar("col")` — which Postgres
16
- * reads as *unbounded* — so which of the two you ran decided whether the column
17
- * had a limit at all. Introspection then dropped the length entirely, so reading
18
- * an existing `character varying(500)` column back and regenerating it produced
19
- * a `VARCHAR(255)`: a silent narrowing of a column with data already in it.
20
- *
21
- * `validation.max` is the property's own statement about how long the value may
22
- * be, so it is the only sensible source for the column's width — and it keeps
23
- * the constraint the database enforces in step with the one the app enforces,
24
- * rather than inventing a second, different limit underneath it.
25
- */
26
- export function resolveStringColumnLength(prop: Pick<StringProperty, "validation">): number {
27
- const max = prop.validation?.max;
28
- return typeof max === "number" && Number.isInteger(max) && max > 0
29
- ? max
30
- : DEFAULT_STRING_COLUMN_LENGTH;
31
- }