@rebasepro/codegen 0.13.1-canary.g3660bd5 → 0.13.1-canary.g501e3cb
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 +39 -10
- package/dist/generate-types.d.ts +11 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.es.js +185 -45
- package/dist/index.es.js.map +1 -1
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ Generates typed TypeScript definitions from Rebase collection definitions — pr
|
|
|
8
8
|
pnpm add @rebasepro/codegen
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
###
|
|
11
|
+
### Dependencies
|
|
12
12
|
|
|
13
|
-
- `@rebasepro/common`
|
|
14
|
-
- `@rebasepro/types`
|
|
13
|
+
- `@rebasepro/common` — a runtime dependency (`resolveCollectionRelations`)
|
|
14
|
+
- `@rebasepro/types` — a peer dependency (types only)
|
|
15
15
|
|
|
16
16
|
## What This Package Does
|
|
17
17
|
|
|
@@ -29,20 +29,49 @@ This is typically invoked via the CLI (`npx rebase generate-sdk`) rather than ca
|
|
|
29
29
|
| `GenerateSDKOptions` | Interface | `{ includeReadme?: boolean }` (default: `true`) |
|
|
30
30
|
| `toPascalCase` | Function | `"my_collection"` → `"MyCollection"` |
|
|
31
31
|
| `toCamelCase` | Function | `"my_collection"` → `"myCollection"` |
|
|
32
|
-
| `toSafeIdentifier` | Function | Converts
|
|
32
|
+
| `toSafeIdentifier` | Function | Converts a **slug** to a valid JS identifier (collection accessors only — never column names) |
|
|
33
33
|
| `indent` | Function | Indent text by N spaces |
|
|
34
|
+
| `CodegenError` | Class | Thrown for a schema that cannot be expressed as valid TypeScript |
|
|
34
35
|
|
|
35
36
|
## Generated Output
|
|
36
37
|
|
|
37
38
|
`generateSDK()` produces:
|
|
38
39
|
|
|
39
|
-
1. **`database.types.ts`** — A `Database` interface
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
1. **`database.types.ts`** — A `Database` interface keyed by each collection's
|
|
41
|
+
*accessor* (`my-notes` → `myNotes`, the property name on `client.data`),
|
|
42
|
+
alongside a `collectionsDictionary` const mapping each accessor back to the
|
|
43
|
+
slug the wire uses. Each entry contains:
|
|
44
|
+
- `Row` — what a read serves. Column names are the **real** ones, unchanged:
|
|
45
|
+
a `created_at` column is `row.created_at`. Nullable columns are `T | null`,
|
|
46
|
+
the primary key is always present, relations appear only when `include`
|
|
47
|
+
names them, and `excludeFromApi` columns are absent.
|
|
48
|
+
- `Insert` — what `create()` accepts. Server-assigned ids are optional; a
|
|
49
|
+
`belongsTo` target may be named either way (`{ author: 5 }` or
|
|
50
|
+
`{ author_id: 5 }`); `excludeFromApi` columns are still writable.
|
|
51
|
+
- `Update` — what `update()` accepts. Everything optional, primary key
|
|
52
|
+
omitted.
|
|
43
53
|
|
|
44
54
|
2. **`README.md`** — Usage instructions (opt out with `includeReadme: false`)
|
|
45
55
|
|
|
56
|
+
### Names
|
|
57
|
+
|
|
58
|
+
Only the collection accessor is transformed. Column names are emitted verbatim,
|
|
59
|
+
quoted when they are not valid identifiers (`"user id"?: string | null`), because
|
|
60
|
+
`where` and `orderBy` are keyed off `Row` — a renamed column makes the correct
|
|
61
|
+
filter fail to compile and the wrong one fail at runtime.
|
|
62
|
+
|
|
63
|
+
Generation **fails** rather than emitting a broken file when two slugs would
|
|
64
|
+
produce the same accessor: the interface would not compile, and
|
|
65
|
+
`collectionsDictionary` would silently keep only one, routing a collection's
|
|
66
|
+
reads to another's table.
|
|
67
|
+
|
|
68
|
+
### Untrusted schemas
|
|
69
|
+
|
|
70
|
+
`rebase generate-sdk --from <url>` generates from a remote contract, so slugs,
|
|
71
|
+
column names and enum values come from that server. Every one of them is emitted
|
|
72
|
+
as an escaped literal — untrusted input cannot add a declaration to the generated
|
|
73
|
+
file. There is a test that asserts exactly this.
|
|
74
|
+
|
|
46
75
|
### Property Type Mapping
|
|
47
76
|
|
|
48
77
|
| Rebase Type | TypeScript Type |
|
|
@@ -53,8 +82,8 @@ This is typically invoked via the CLI (`npx rebase generate-sdk`) rather than ca
|
|
|
53
82
|
| `date` | `string` (ISO 8601) |
|
|
54
83
|
| `geopoint` | `{ latitude: number; longitude: number }` |
|
|
55
84
|
| `reference` | `string \| number` |
|
|
56
|
-
| `relation` |
|
|
57
|
-
| `map` | Inline object type or `Record<string,
|
|
85
|
+
| `relation` | The target's own `Row`, inlined (what `include` serves) |
|
|
86
|
+
| `map` | Inline object type or `Record<string, unknown>` |
|
|
58
87
|
| `array` | `Array<T>` with inferred inner type |
|
|
59
88
|
| `vector` | `number[]` |
|
|
60
89
|
| `binary` | `string` |
|
package/dist/generate-types.d.ts
CHANGED
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import { CollectionConfig } from "@rebasepro/types";
|
|
2
|
+
/**
|
|
3
|
+
* A schema that cannot be expressed as a valid TypeScript file.
|
|
4
|
+
*
|
|
5
|
+
* Thrown rather than emitted. The generator used to concatenate whatever it was
|
|
6
|
+
* given, so a slug that collided with another one, or that was not an
|
|
7
|
+
* identifier, produced a file that either failed to compile or — worse —
|
|
8
|
+
* compiled while quietly routing one collection to another's slug.
|
|
9
|
+
*/
|
|
10
|
+
export declare class CodegenError extends Error {
|
|
11
|
+
constructor(message: string);
|
|
12
|
+
}
|
|
2
13
|
export declare function generateTypedefs(collections: CollectionConfig[]): string;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Generates a purely typed Typescript database definition.
|
|
5
5
|
*/
|
|
6
6
|
import { CollectionConfig } from "@rebasepro/types";
|
|
7
|
-
export { generateTypedefs } from "./generate-types";
|
|
7
|
+
export { generateTypedefs, CodegenError } from "./generate-types";
|
|
8
8
|
export { toPascalCase, toCamelCase, toSafeIdentifier, indent } from "./utils";
|
|
9
9
|
export interface GeneratedFile {
|
|
10
10
|
/** Relative file path within the output directory */
|
package/dist/index.es.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveCollectionRelations } from "@rebasepro/common";
|
|
1
|
+
import { findRelation, resolveCollectionRelations } from "@rebasepro/common";
|
|
2
2
|
//#region src/utils.ts
|
|
3
3
|
/**
|
|
4
4
|
* Utility functions for the SDK generator
|
|
@@ -43,16 +43,70 @@ function indent(text, spaces) {
|
|
|
43
43
|
}
|
|
44
44
|
//#endregion
|
|
45
45
|
//#region src/generate-types.ts
|
|
46
|
+
/**
|
|
47
|
+
* A schema that cannot be expressed as a valid TypeScript file.
|
|
48
|
+
*
|
|
49
|
+
* Thrown rather than emitted. The generator used to concatenate whatever it was
|
|
50
|
+
* given, so a slug that collided with another one, or that was not an
|
|
51
|
+
* identifier, produced a file that either failed to compile or — worse —
|
|
52
|
+
* compiled while quietly routing one collection to another's slug.
|
|
53
|
+
*/
|
|
54
|
+
var CodegenError = class extends Error {
|
|
55
|
+
constructor(message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.name = "CodegenError";
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
var IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
61
|
+
/**
|
|
62
|
+
* A property name for the emitted TypeScript: verbatim when it is a valid
|
|
63
|
+
* identifier, quoted otherwise.
|
|
64
|
+
*
|
|
65
|
+
* Every key that reaches the output goes through here. Column names are not
|
|
66
|
+
* required to be identifiers — `"order"`, `"user id"`, a quoted Postgres
|
|
67
|
+
* identifier — and the previous behaviour of camel-casing them into shape
|
|
68
|
+
* renamed the column in the type while the wire kept the original, so the
|
|
69
|
+
* generated `Row` described fields that did not exist.
|
|
70
|
+
*/
|
|
71
|
+
function emitKey(key) {
|
|
72
|
+
return IDENTIFIER.test(key) ? key : JSON.stringify(key);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A string literal, escaped.
|
|
76
|
+
*
|
|
77
|
+
* `"${value}"` was the previous form. A value containing a quote closed the
|
|
78
|
+
* literal early, which at best broke the file and at worst let a slug from a
|
|
79
|
+
* remote contract inject top-level statements into a file the developer
|
|
80
|
+
* compiles and bundles.
|
|
81
|
+
*/
|
|
82
|
+
function emitString(value) {
|
|
83
|
+
return JSON.stringify(value);
|
|
84
|
+
}
|
|
85
|
+
/** The `id`s of an enum declared as an array, an array of `{ id }`, or an object map. */
|
|
86
|
+
function enumIds(raw) {
|
|
87
|
+
if (Array.isArray(raw)) return raw.map((entry) => entry && typeof entry === "object" ? entry.id : entry);
|
|
88
|
+
if (raw && typeof raw === "object") return Object.keys(raw);
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
46
91
|
function propertyToTypeScriptType(prop) {
|
|
47
92
|
switch (prop.type) {
|
|
48
93
|
case "string": {
|
|
49
94
|
const sp = prop;
|
|
50
|
-
if (sp.enum)
|
|
95
|
+
if (sp.enum) {
|
|
96
|
+
const ids = enumIds(sp.enum);
|
|
97
|
+
if (ids.length === 0) return "string";
|
|
98
|
+
return ids.map((v) => emitString(String(v))).join(" | ");
|
|
99
|
+
}
|
|
51
100
|
return "string";
|
|
52
101
|
}
|
|
53
102
|
case "number": {
|
|
54
103
|
const np = prop;
|
|
55
|
-
if (np.enum)
|
|
104
|
+
if (np.enum) {
|
|
105
|
+
const ids = enumIds(np.enum);
|
|
106
|
+
const numbers = ids.map(Number);
|
|
107
|
+
if (ids.length === 0 || numbers.some((n) => !Number.isFinite(n))) return "number";
|
|
108
|
+
return numbers.map((n) => String(n)).join(" | ");
|
|
109
|
+
}
|
|
56
110
|
return "number";
|
|
57
111
|
}
|
|
58
112
|
case "boolean": return "boolean";
|
|
@@ -62,7 +116,12 @@ function propertyToTypeScriptType(prop) {
|
|
|
62
116
|
case "relation": return "string | number";
|
|
63
117
|
case "map": {
|
|
64
118
|
const mapProp = prop;
|
|
65
|
-
if (mapProp.properties) return `{ ${Object.entries(mapProp.properties).map(([k, v]) =>
|
|
119
|
+
if (mapProp.properties) return `{ ${Object.entries(mapProp.properties).map(([k, v]) => {
|
|
120
|
+
const child = v;
|
|
121
|
+
const optional = !child.validation?.required;
|
|
122
|
+
const type = propertyToTypeScriptType(child);
|
|
123
|
+
return `${emitKey(k)}${optional ? "?" : ""}: ${optional ? `${type} | null` : type};`;
|
|
124
|
+
}).join(" ")} }`;
|
|
66
125
|
return "Record<string, unknown>";
|
|
67
126
|
}
|
|
68
127
|
case "array": {
|
|
@@ -96,6 +155,18 @@ function foreignKeyType(relation) {
|
|
|
96
155
|
if (!idProp) return "string | number";
|
|
97
156
|
return idProp[1].type === "number" ? "number" : "string";
|
|
98
157
|
}
|
|
158
|
+
/** Whether a property is the collection's primary key. */
|
|
159
|
+
function isPrimaryKey(prop) {
|
|
160
|
+
return Boolean(prop.isId);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether the server assigns this primary key, so a write does not have to.
|
|
164
|
+
* `true` and `"manual"` both mean the caller supplies it.
|
|
165
|
+
*/
|
|
166
|
+
function isAutoAssignedId(prop) {
|
|
167
|
+
const isId = prop.isId;
|
|
168
|
+
return Boolean(isId) && isId !== "manual" && isId !== true;
|
|
169
|
+
}
|
|
99
170
|
/**
|
|
100
171
|
* The type an *included* relation arrives as: the target's own row, inlined.
|
|
101
172
|
*
|
|
@@ -108,13 +179,43 @@ function foreignKeyType(relation) {
|
|
|
108
179
|
* Falls back to an open record when the target is not part of this generation
|
|
109
180
|
* run, since there is no `Row` to point at.
|
|
110
181
|
*/
|
|
111
|
-
function includedRelationType(relation,
|
|
182
|
+
function includedRelationType(relation, accessors) {
|
|
112
183
|
const slug = resolveTargetCollection(relation)?.slug ?? relation.targetSlug;
|
|
113
|
-
const
|
|
184
|
+
const accessor = slug ? accessors.get(slug) : void 0;
|
|
185
|
+
const rowType = accessor ? `Database[${emitString(accessor)}]["Row"]` : "Record<string, unknown>";
|
|
114
186
|
return relation.cardinality === "many" ? `Array<${rowType}>` : rowType;
|
|
115
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* Map every slug to the property name it is reachable under on `client.data`.
|
|
190
|
+
*
|
|
191
|
+
* The accessor is a safe identifier because `client.data.myNotes` is the point
|
|
192
|
+
* of generating this at all, and `collectionsDictionary` maps it back to the
|
|
193
|
+
* slug the wire uses. Two slugs that safe down to the same identifier cannot
|
|
194
|
+
* both have it: the interface would not compile, and the dictionary — an object
|
|
195
|
+
* literal — would silently keep only the last, routing one collection's reads
|
|
196
|
+
* to the other's table. There is no defensible way to pick, so this refuses.
|
|
197
|
+
*/
|
|
198
|
+
function buildAccessors(collections) {
|
|
199
|
+
const accessors = /* @__PURE__ */ new Map();
|
|
200
|
+
const bySafeName = /* @__PURE__ */ new Map();
|
|
201
|
+
for (const collection of collections) {
|
|
202
|
+
const slug = collection.slug;
|
|
203
|
+
if (typeof slug !== "string" || slug.length === 0) throw new CodegenError("A collection has no slug, so it has no name to generate a type for. Every collection needs a unique `slug`.");
|
|
204
|
+
const safe = toSafeIdentifier(slug);
|
|
205
|
+
if (safe.length === 0) throw new CodegenError(`The slug ${emitString(slug)} has no characters that can form a property name, so it cannot be reached as \`client.data.<name>\`. Use a slug containing letters, digits, underscores or dashes.`);
|
|
206
|
+
const existing = bySafeName.get(safe);
|
|
207
|
+
if (existing !== void 0) throw new CodegenError(`The collections ${emitString(existing)} and ${emitString(slug)} both generate the accessor "${safe}", so only one of them could be reached from the generated client and the other's reads would silently go to the wrong table. Rename one of the slugs.`);
|
|
208
|
+
bySafeName.set(safe, slug);
|
|
209
|
+
accessors.set(slug, safe);
|
|
210
|
+
}
|
|
211
|
+
return accessors;
|
|
212
|
+
}
|
|
213
|
+
/** One emitted `key: type;` line, already indented. */
|
|
214
|
+
function line(key, type, optional) {
|
|
215
|
+
return ` ${emitKey(key)}${optional ? "?" : ""}: ${type};`;
|
|
216
|
+
}
|
|
116
217
|
function generateTypedefs(collections) {
|
|
117
|
-
const
|
|
218
|
+
const accessors = buildAccessors(collections);
|
|
118
219
|
const lines = [
|
|
119
220
|
"/**",
|
|
120
221
|
" * This file was auto-generated by Rebase.",
|
|
@@ -124,7 +225,6 @@ function generateTypedefs(collections) {
|
|
|
124
225
|
"export interface Database {"
|
|
125
226
|
];
|
|
126
227
|
for (const collection of collections) {
|
|
127
|
-
toPascalCase(collection.slug);
|
|
128
228
|
const properties = collection.properties ?? {};
|
|
129
229
|
let resolvedRelations = {};
|
|
130
230
|
try {
|
|
@@ -132,15 +232,18 @@ function generateTypedefs(collections) {
|
|
|
132
232
|
} catch (e) {
|
|
133
233
|
console.warn(`[rebase] Could not resolve the relations of "${collection.slug}", so its generated type has no relation fields and none of their foreign-key columns. This is usually a circular import in the collection files — make sure the target is \`() => otherCollection\` and not evaluated at module load.\n ${e instanceof Error ? e.message : String(e)}`);
|
|
134
234
|
}
|
|
135
|
-
|
|
235
|
+
const subcollections = collection.subcollections;
|
|
236
|
+
if (Array.isArray(subcollections) && subcollections.length > 0) console.warn(`[rebase] "${collection.slug}" declares ${subcollections.length} subcollection(s), which are not part of the generated Database: they are reached over a nested path (\`data/${collection.slug}/<id>/<relation>\`), not as a top-level accessor. Register a subcollection as a collection of its own if you want a typed accessor for it.`);
|
|
237
|
+
lines.push(` ${emitKey(accessors.get(collection.slug))}: {`);
|
|
136
238
|
lines.push(" Row: {");
|
|
137
239
|
const emittedKeys = /* @__PURE__ */ new Set();
|
|
138
240
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
139
241
|
const prop = rawProp;
|
|
140
242
|
if (prop.type === "relation") continue;
|
|
243
|
+
if (prop.excludeFromApi) continue;
|
|
141
244
|
const tsType = propertyToTypeScriptType(prop);
|
|
142
|
-
const isRequired = prop.validation?.required;
|
|
143
|
-
lines.push(
|
|
245
|
+
const isRequired = Boolean(prop.validation?.required) || isPrimaryKey(prop);
|
|
246
|
+
lines.push(line(key, isRequired ? tsType : `${tsType} | null`, !isRequired));
|
|
144
247
|
emittedKeys.add(key);
|
|
145
248
|
}
|
|
146
249
|
for (const [relKey, relation] of Object.entries(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) {
|
|
@@ -148,20 +251,20 @@ function generateTypedefs(collections) {
|
|
|
148
251
|
if (emittedKeys.has(fkKey)) continue;
|
|
149
252
|
const fkType = foreignKeyType(relation);
|
|
150
253
|
const shadowedByInclude = relKey === fkKey;
|
|
151
|
-
const tsType = shadowedByInclude ? `${fkType} | ${includedRelationType(relation,
|
|
152
|
-
const isRequired = relation.validation?.required && !shadowedByInclude;
|
|
153
|
-
lines.push(
|
|
254
|
+
const tsType = shadowedByInclude ? `${fkType} | ${includedRelationType(relation, accessors)}` : fkType;
|
|
255
|
+
const isRequired = Boolean(relation.validation?.required) && !shadowedByInclude;
|
|
256
|
+
lines.push(line(fkKey, isRequired ? tsType : `${tsType} | null`, !isRequired));
|
|
154
257
|
emittedKeys.add(fkKey);
|
|
155
258
|
}
|
|
156
259
|
for (const [key, relation] of Object.entries(resolvedRelations)) {
|
|
157
260
|
if (emittedKeys.has(key)) continue;
|
|
158
|
-
lines.push(
|
|
261
|
+
lines.push(line(key, includedRelationType(relation, accessors), true));
|
|
159
262
|
emittedKeys.add(key);
|
|
160
263
|
}
|
|
161
264
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
162
265
|
if (rawProp.type !== "relation") continue;
|
|
163
266
|
if (emittedKeys.has(key)) continue;
|
|
164
|
-
lines.push(
|
|
267
|
+
lines.push(line(key, "Record<string, unknown>", true));
|
|
165
268
|
emittedKeys.add(key);
|
|
166
269
|
}
|
|
167
270
|
lines.push(" };");
|
|
@@ -171,51 +274,67 @@ function generateTypedefs(collections) {
|
|
|
171
274
|
const prop = rawProp;
|
|
172
275
|
if (prop.type === "relation") continue;
|
|
173
276
|
const tsType = propertyToTypeScriptType(prop);
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
const isAutoId = "isId" in prop && typedProp.isId && typedProp.isId !== "manual" && typedProp.isId !== true;
|
|
177
|
-
const isOptional = !isRequired || isAutoId;
|
|
178
|
-
lines.push(` ${toSafeIdentifier(key)}${isOptional ? "?" : ""}: ${tsType};`);
|
|
277
|
+
const isOptional = !prop.validation?.required || isAutoAssignedId(prop);
|
|
278
|
+
lines.push(line(key, tsType, isOptional));
|
|
179
279
|
emittedKeys.add(key);
|
|
180
280
|
}
|
|
181
|
-
|
|
182
|
-
const fkKey = relation.localKey;
|
|
183
|
-
if (emittedKeys.has(fkKey)) continue;
|
|
184
|
-
const fkType = "string | number";
|
|
185
|
-
const isRequired = relation.validation?.required;
|
|
186
|
-
lines.push(` ${toSafeIdentifier(fkKey)}${isRequired ? "" : "?"}: ${fkType};`);
|
|
187
|
-
emittedKeys.add(fkKey);
|
|
188
|
-
}
|
|
281
|
+
emitWritableRelations(lines, properties, resolvedRelations, emittedKeys, false);
|
|
189
282
|
lines.push(" };");
|
|
190
283
|
lines.push(" Update: {");
|
|
191
284
|
emittedKeys.clear();
|
|
192
285
|
for (const [key, rawProp] of Object.entries(properties)) {
|
|
193
286
|
const prop = rawProp;
|
|
194
287
|
if (prop.type === "relation") continue;
|
|
195
|
-
|
|
196
|
-
lines.push(
|
|
288
|
+
if (isPrimaryKey(prop)) continue;
|
|
289
|
+
lines.push(line(key, propertyToTypeScriptType(prop), true));
|
|
197
290
|
emittedKeys.add(key);
|
|
198
291
|
}
|
|
199
|
-
|
|
200
|
-
const fkKey = relation.localKey;
|
|
201
|
-
if (emittedKeys.has(fkKey)) continue;
|
|
202
|
-
lines.push(` ${toSafeIdentifier(fkKey)}?: string | number;`);
|
|
203
|
-
emittedKeys.add(fkKey);
|
|
204
|
-
}
|
|
292
|
+
emitWritableRelations(lines, properties, resolvedRelations, emittedKeys, true);
|
|
205
293
|
lines.push(" };");
|
|
206
294
|
lines.push(" };");
|
|
207
295
|
}
|
|
208
296
|
lines.push("}");
|
|
209
297
|
lines.push("");
|
|
210
298
|
lines.push("export type CollectionName = keyof Database;");
|
|
211
|
-
lines.push("export type CollectionsDictionary = { [K in CollectionName]: K };");
|
|
212
299
|
lines.push("");
|
|
213
300
|
lines.push("export const collectionsDictionary = {");
|
|
214
|
-
for (const collection of collections) lines.push(` ${
|
|
301
|
+
for (const collection of collections) lines.push(` ${emitKey(accessors.get(collection.slug))}: ${emitString(collection.slug)},`);
|
|
215
302
|
lines.push("} as const;");
|
|
216
303
|
lines.push("");
|
|
304
|
+
lines.push("export type CollectionsDictionary = typeof collectionsDictionary;");
|
|
305
|
+
lines.push("");
|
|
217
306
|
return lines.join("\n");
|
|
218
307
|
}
|
|
308
|
+
/**
|
|
309
|
+
* The two ways a write can name a `belongsTo` target, both of which the server
|
|
310
|
+
* accepts: the foreign-key column itself (`{ author_id: 5 }`, which passes
|
|
311
|
+
* through untouched) and the relation *property* (`{ author: 5 }`, which the
|
|
312
|
+
* write transformer maps onto that column).
|
|
313
|
+
*
|
|
314
|
+
* Only the first was generated, so the documented and idiomatic write shape was
|
|
315
|
+
* a type error.
|
|
316
|
+
*
|
|
317
|
+
* The second form is emitted under the **property key**, not the resolved
|
|
318
|
+
* relation name, because that is what the transformer keys off: it looks the
|
|
319
|
+
* payload key up in `properties` and only treats it as a relation if what it
|
|
320
|
+
* finds there is one. A relation whose `relationName` differs from its property
|
|
321
|
+
* key is reachable as the property and not as the name, so emitting the name
|
|
322
|
+
* would have offered a key that writes to a column that does not exist.
|
|
323
|
+
*/
|
|
324
|
+
function emitWritableRelations(lines, properties, resolvedRelations, emittedKeys, allOptional) {
|
|
325
|
+
const emit = (key, relation) => {
|
|
326
|
+
if (emittedKeys.has(key)) return;
|
|
327
|
+
const optional = allOptional || !relation.validation?.required;
|
|
328
|
+
lines.push(line(key, foreignKeyType(relation), optional));
|
|
329
|
+
emittedKeys.add(key);
|
|
330
|
+
};
|
|
331
|
+
for (const relation of Object.values(resolvedRelations)) if (relation.kind === "belongsTo" && relation.localKey) emit(relation.localKey, relation);
|
|
332
|
+
for (const [key, rawProp] of Object.entries(properties)) {
|
|
333
|
+
if (rawProp.type !== "relation") continue;
|
|
334
|
+
const relation = findRelation(resolvedRelations, key);
|
|
335
|
+
if (relation?.kind === "belongsTo" && relation.localKey) emit(key, relation);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
219
338
|
//#endregion
|
|
220
339
|
//#region src/index.ts
|
|
221
340
|
function generateSDK(collections, options = {}) {
|
|
@@ -240,25 +359,46 @@ function generateSDK(collections, options = {}) {
|
|
|
240
359
|
2. Initialize with your generated types:
|
|
241
360
|
\`\`\`typescript
|
|
242
361
|
import { createRebaseClient } from '@rebasepro/client';
|
|
243
|
-
import {
|
|
362
|
+
import { collectionsDictionary, type Database } from './database.types';
|
|
244
363
|
|
|
245
364
|
const rebase = createRebaseClient<Database>({
|
|
246
365
|
baseUrl: 'http://localhost:3001',
|
|
366
|
+
// Maps each accessor back to the slug the wire uses. Without it a
|
|
367
|
+
// hyphenated slug is not resolvable from the property name alone.
|
|
247
368
|
collections: collectionsDictionary,
|
|
248
369
|
});
|
|
249
370
|
|
|
250
|
-
//
|
|
371
|
+
// Property access is the typed surface: rows, filters and sorts are all
|
|
372
|
+
// checked against the generated Database.
|
|
251
373
|
const { data: users } = await rebase.data.users.find();
|
|
252
374
|
console.log(users[0].email); // flat access — no .values wrapper
|
|
253
|
-
|
|
254
|
-
const { data: posts } = await rebase.data.collection('posts').find();
|
|
255
|
-
console.log(posts[0].title); // just post.title, not post.values.title
|
|
256
375
|
\`\`\`
|
|
376
|
+
|
|
377
|
+
## Field names are the ones the API serves
|
|
378
|
+
|
|
379
|
+
The generated \`Row\` uses each column's real name, unchanged — a \`created_at\`
|
|
380
|
+
column is \`row.created_at\`, not \`row.createdAt\`. \`where\` and \`orderBy\` are keyed
|
|
381
|
+
off the same type, so what compiles is what the backend answers to.
|
|
382
|
+
|
|
383
|
+
Only the *collection accessor* is turned into a property name
|
|
384
|
+
(\`my-notes\` → \`rebase.data.myNotes\`), which is what \`collectionsDictionary\` maps
|
|
385
|
+
back.
|
|
386
|
+
|
|
387
|
+
## \`Row\` vs \`Insert\` vs \`Update\`
|
|
388
|
+
|
|
389
|
+
| Type | What it describes |
|
|
390
|
+
|---|---|
|
|
391
|
+
| \`Row\` | What a read serves. Nullable columns are \`T \\| null\`; relations appear only when \`include\` names them; \`excludeFromApi\` columns are absent. |
|
|
392
|
+
| \`Insert\` | What \`create()\` accepts. Server-assigned ids are optional; a \`belongsTo\` target may be named either way (\`{ author: 5 }\` or \`{ author_id: 5 }\`). |
|
|
393
|
+
| \`Update\` | What \`update()\` accepts. Everything optional, and the primary key is not settable. |
|
|
394
|
+
|
|
395
|
+
If you need an untyped escape hatch, \`rebase.data.collection(slug)\` still works —
|
|
396
|
+
but it is generic over \`Record<string, unknown>\` and gives up everything above.
|
|
257
397
|
`
|
|
258
398
|
});
|
|
259
399
|
return files;
|
|
260
400
|
}
|
|
261
401
|
//#endregion
|
|
262
|
-
export { generateSDK, generateTypedefs, indent, toCamelCase, toPascalCase, toSafeIdentifier };
|
|
402
|
+
export { CodegenError, generateSDK, generateTypedefs, indent, toCamelCase, toPascalCase, toSafeIdentifier };
|
|
263
403
|
|
|
264
404
|
//# sourceMappingURL=index.es.js.map
|
package/dist/index.es.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.es.js","names":[],"sources":["../src/utils.ts","../src/generate-types.ts","../src/index.ts"],"sourcesContent":["/**\n * Utility functions for the SDK generator\n */\n\n/**\n * Convert a slug/snake_case string to PascalCase\n * e.g. \"private_notes\" → \"PrivateNotes\"\n *\n * Capitals already inside a word are meaningful and are kept: lowercasing the\n * tail of every chunk turned \"TestEntities\" into \"Testentities\", which is what\n * ended up in the generated type names. SHOUTING_CASE is the one shape where\n * the tail is not meaningful, so it is folded down.\n */\nexport function toPascalCase(str: string): string {\n return str\n .split(/[_\\-\\s]+/)\n .filter(Boolean)\n .map(word => {\n const rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);\n return word.charAt(0).toUpperCase() + rest;\n })\n .join(\"\");\n}\n\n/**\n * Convert a slug/snake_case string to camelCase\n * e.g. \"private_notes\" → \"privateNotes\"\n */\nexport function toCamelCase(str: string): string {\n if (!/[_\\-\\s]/.test(str)) {\n return str.charAt(0).toLowerCase() + str.slice(1);\n }\n const pascal = toPascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/**\n * Convert a slug to a safe JS identifier\n * e.g. \"private-notes\" → \"privateNotes\"\n */\nexport function toSafeIdentifier(str: string): string {\n return toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, \"_\"));\n}\n\n/**\n * Indent a block of text by a given number of spaces\n */\nexport function indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map(line => (line.trim() ? pad + line : line))\n .join(\"\\n\");\n}\n","import { CollectionConfig, PostgresCollectionConfig, Property, Properties, MapProperty, ArrayProperty, Relation, RelationProperty, StringProperty, NumberProperty, ResolvedRelation } from \"@rebasepro/types\";\nimport { resolveCollectionRelations } from \"@rebasepro/common\";\nimport { toPascalCase, toSafeIdentifier } from \"./utils\";\n\nfunction propertyToTypeScriptType(prop: Property): string {\n switch (prop.type) {\n case \"string\": {\n const sp = prop as StringProperty;\n if (sp.enum) {\n const ids = Array.isArray(sp.enum)\n ? sp.enum.map((e: string | number | { id: string | number }) => typeof e === \"object\" ? String(e.id) : String(e))\n : Object.keys(sp.enum);\n return ids.map(v => `\"${v}\"`).join(\" | \");\n }\n return \"string\";\n }\n case \"number\": {\n const np = prop as NumberProperty;\n if (np.enum) {\n const ids = Array.isArray(np.enum)\n ? np.enum.map((e: string | number | { id: string | number }) => typeof e === \"object\" ? String(e.id) : String(e))\n : Object.keys(np.enum);\n return ids.join(\" | \");\n }\n return \"number\";\n }\n case \"boolean\":\n return \"boolean\";\n case \"date\":\n return \"string\"; // ISO 8601 string over the wire\n case \"geopoint\":\n return \"{ latitude: number; longitude: number; }\";\n case \"reference\":\n return \"string | number\";\n case \"relation\":\n return \"string | number\";\n case \"map\": {\n const mapProp = prop as MapProperty;\n if (mapProp.properties) {\n const inner = Object.entries(mapProp.properties)\n .map(([k, v]) => `${toSafeIdentifier(k)}: ${propertyToTypeScriptType(v as Property)};`)\n .join(\" \");\n return `{ ${inner} }`;\n }\n return \"Record<string, unknown>\";\n }\n case \"array\": {\n const arrProp = prop as ArrayProperty;\n if (arrProp.of) {\n return `Array<${propertyToTypeScriptType(arrProp.of as Property)}>`;\n }\n return \"Array<unknown>\";\n }\n case \"vector\":\n return \"number[]\";\n case \"binary\":\n return \"string\";\n default:\n return \"unknown\";\n }\n}\n\n/**\n * Unwrap a relation target that arrived as a module namespace rather than the\n * collection itself — `target: () => import(\"./authors\")` is a common slip.\n */\nfunction resolveTargetCollection(relation: ResolvedRelation): CollectionConfig | undefined {\n try {\n let target = relation.target() as CollectionConfig & { default?: CollectionConfig; __esModule?: boolean };\n if (target && (target.default || target.__esModule)) {\n target = (target.default ?? target) as typeof target;\n }\n return target;\n } catch {\n return undefined;\n }\n}\n\n/** The TypeScript type of a foreign key: whatever the target's primary key is. */\nfunction foreignKeyType(relation: ResolvedRelation): string {\n const target = resolveTargetCollection(relation);\n if (!target?.properties) return \"string | number\";\n const idProp = Object.entries(target.properties).find(([_, p]) => (p as Record<string, unknown>).isId);\n if (!idProp) return \"string | number\";\n return (idProp[1] as Property).type === \"number\" ? \"number\" : \"string\";\n}\n\n/**\n * The type an *included* relation arrives as: the target's own row, inlined.\n *\n * This is what the read pipeline actually serves — `toRestRow` puts the\n * target's flat columns where the relation was, and the SDK and the HTTP API\n * both go through it. It is deliberately *not* a `{ __type: \"relation\" }`\n * envelope: that shape is the admin's view-model and never reaches a\n * developer's `find()`.\n *\n * Falls back to an open record when the target is not part of this generation\n * run, since there is no `Row` to point at.\n */\nfunction includedRelationType(relation: ResolvedRelation, knownSlugs: Set<string>): string {\n const target = resolveTargetCollection(relation);\n const slug = target?.slug ?? relation.targetSlug;\n const rowType = slug && knownSlugs.has(slug)\n ? `Database[${JSON.stringify(toSafeIdentifier(slug))}][\"Row\"]`\n : \"Record<string, unknown>\";\n return relation.cardinality === \"many\" ? `Array<${rowType}>` : rowType;\n}\n\nexport function generateTypedefs(collections: CollectionConfig[]): string {\n const knownSlugs = new Set(collections.map(c => c.slug).filter(Boolean) as string[]);\n const lines: string[] = [\n \"/**\",\n \" * This file was auto-generated by Rebase.\",\n \" * Do not make direct changes to the file.\",\n \" */\",\n \"\",\n \"export interface Database {\"\n ];\n\n for (const collection of collections) {\n const typeName = toPascalCase(collection.slug);\n const properties = (collection.properties ?? {}) as Properties;\n\n // Resolve relations\n let resolvedRelations: Record<string, ResolvedRelation> = {};\n try {\n resolvedRelations = resolveCollectionRelations(collection);\n } catch (e) {\n // Swallowed before, which made this the quietest way to ship a\n // wrong type. The foreign-key columns are emitted from the resolved\n // relations rather than from the properties, so losing them drops\n // both the relation fields *and* columns that exist in the\n // database — and the resulting error surfaces in the user's code,\n // typechecking against a `Database` that is missing `author_id`,\n // with nothing pointing back at generation.\n //\n // A target thunk usually throws because of a circular import; the\n // boot-time relation validator names the same cause.\n console.warn(\n `[rebase] Could not resolve the relations of \"${collection.slug}\", so its generated ` +\n \"type has no relation fields and none of their foreign-key columns. This is usually a \" +\n \"circular import in the collection files — make sure the target is `() => otherCollection` \" +\n `and not evaluated at module load.\\n ${e instanceof Error ? e.message : String(e)}`\n );\n }\n\n lines.push(` ${toSafeIdentifier(collection.slug)}: {`);\n\n // ── Row Type ──\n lines.push(\" Row: {\");\n const emittedKeys = new Set<string>();\n\n // 1. Direct properties\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n\n const tsType = propertyToTypeScriptType(prop);\n const isRequired = prop.validation?.required;\n lines.push(` ${toSafeIdentifier(key)}${isRequired ? \"\" : \"?\"}: ${tsType};`);\n emittedKeys.add(key);\n }\n\n // 2. FK columns from relations\n for (const [relKey, relation] of Object.entries(resolvedRelations)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n const fkKey = relation.localKey;\n if (emittedKeys.has(fkKey)) continue;\n\n const fkType = foreignKeyType(relation);\n\n // A relation addressed by the same name as its own foreign key\n // is served *over* that column when the read includes it: the\n // query nests the target under the relation name, and the\n // scalar it shadows is gone. Both outcomes are real, so the\n // column is typed as both — which is what stops a plain\n // `const id: string = row.author_id` from compiling.\n const shadowedByInclude = relKey === fkKey;\n const tsType = shadowedByInclude\n ? `${fkType} | ${includedRelationType(relation, knownSlugs)}`\n : fkType;\n\n const isRequired = relation.validation?.required && !shadowedByInclude;\n lines.push(` ${toSafeIdentifier(fkKey)}${isRequired ? \"\" : \"?\"}: ${tsType};`);\n emittedKeys.add(fkKey);\n }\n }\n\n // 3. Relation fields — the target's own row, inlined.\n //\n // Optional throughout: a relation is only loaded when the read names it\n // in `include`, so it is absent from every other read.\n for (const [key, relation] of Object.entries(resolvedRelations)) {\n if (emittedKeys.has(key)) continue;\n lines.push(` ${toSafeIdentifier(key)}?: ${includedRelationType(relation, knownSlugs)};`);\n emittedKeys.add(key);\n }\n\n // A `relation` property whose relation could not be resolved — an\n // engine without relation support, or a target that did not load. It is\n // still a column on the row, so it is still typed, just not precisely.\n for (const [key, rawProp] of Object.entries(properties)) {\n if ((rawProp as Property).type !== \"relation\") continue;\n if (emittedKeys.has(key)) continue;\n lines.push(` ${toSafeIdentifier(key)}?: Record<string, unknown>;`);\n emittedKeys.add(key);\n }\n lines.push(\" };\");\n\n // ── Insert Type ──\n lines.push(\" Insert: {\");\n emittedKeys.clear();\n\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n const tsType = propertyToTypeScriptType(prop);\n const isRequired = prop.validation?.required;\n const typedProp = prop as StringProperty | NumberProperty;\n const isAutoId = \"isId\" in prop && typedProp.isId && typedProp.isId !== \"manual\" && typedProp.isId !== true;\n const isOptional = !isRequired || isAutoId;\n lines.push(` ${toSafeIdentifier(key)}${isOptional ? \"?\" : \"\"}: ${tsType};`);\n emittedKeys.add(key);\n }\n\n for (const [relKey, relation] of Object.entries(resolvedRelations)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n const fkKey = relation.localKey;\n if (emittedKeys.has(fkKey)) continue;\n const fkType = \"string | number\";\n // simple fallback\n const isRequired = relation.validation?.required;\n lines.push(` ${toSafeIdentifier(fkKey)}${isRequired ? \"\" : \"?\"}: ${fkType};`);\n emittedKeys.add(fkKey);\n }\n }\n lines.push(\" };\");\n\n // ── Update Type ──\n lines.push(\" Update: {\");\n emittedKeys.clear();\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n const tsType = propertyToTypeScriptType(prop);\n lines.push(` ${toSafeIdentifier(key)}?: ${tsType};`);\n emittedKeys.add(key);\n }\n for (const [relKey, relation] of Object.entries(resolvedRelations)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n const fkKey = relation.localKey;\n if (emittedKeys.has(fkKey)) continue;\n lines.push(` ${toSafeIdentifier(fkKey)}?: string | number;`);\n emittedKeys.add(fkKey);\n }\n }\n lines.push(\" };\");\n\n lines.push(\" };\");\n }\n\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"export type CollectionName = keyof Database;\");\n lines.push(\"export type CollectionsDictionary = { [K in CollectionName]: K };\");\n lines.push(\"\");\n lines.push(\"export const collectionsDictionary = {\");\n for (const collection of collections) {\n lines.push(` ${toSafeIdentifier(collection.slug)}: \"${collection.slug}\",`);\n }\n lines.push(\"} as const;\");\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n}\n","/**\n * @rebasepro/codegen\n *\n * Generates a purely typed Typescript database definition.\n */\n\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { generateTypedefs } from \"./generate-types\";\n\nexport { generateTypedefs } from \"./generate-types\";\nexport { toPascalCase, toCamelCase, toSafeIdentifier, indent } from \"./utils\";\n\n// ─── Public API ────────────────────────────────────────────────────\n\nexport interface GeneratedFile {\n /** Relative file path within the output directory */\n path: string;\n /** File content */\n content: string;\n}\n\nexport interface GenerateSDKOptions {\n /** Whether to include a README file (default: true) */\n includeReadme?: boolean;\n}\n\nexport function generateSDK(\n collections: CollectionConfig[],\n options: GenerateSDKOptions = {}\n): GeneratedFile[] {\n const files: GeneratedFile[] = [];\n\n files.push({\n path: \"database.types.ts\",\n content: generateTypedefs(collections)\n });\n\n if (options.includeReadme !== false) {\n files.push({\n path: \"README.md\",\n content: `# Rebase SDK\n\n> Auto-generated by \\`rebase generate-sdk\\`. Do not edit manually.\n\n## Usage\n\n1. Install the client package:\n \\`\\`\\`bash\n npm install @rebasepro/client\n \\`\\`\\`\n\n2. Initialize with your generated types:\n \\`\\`\\`typescript\n import { createRebaseClient } from '@rebasepro/client';\n import { Database, collectionsDictionary } from './database.types';\n\n const rebase = createRebaseClient<Database>({\n baseUrl: 'http://localhost:3001',\n collections: collectionsDictionary,\n });\n\n // Both syntax styles are fully typed!\n const { data: users } = await rebase.data.users.find();\n console.log(users[0].email); // flat access — no .values wrapper\n\n const { data: posts } = await rebase.data.collection('posts').find();\n console.log(posts[0].title); // just post.title, not post.values.title\n \\`\\`\\`\n`\n });\n }\n\n return files;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.es.js","names":[],"sources":["../src/utils.ts","../src/generate-types.ts","../src/index.ts"],"sourcesContent":["/**\n * Utility functions for the SDK generator\n */\n\n/**\n * Convert a slug/snake_case string to PascalCase\n * e.g. \"private_notes\" → \"PrivateNotes\"\n *\n * Capitals already inside a word are meaningful and are kept: lowercasing the\n * tail of every chunk turned \"TestEntities\" into \"Testentities\", which is what\n * ended up in the generated type names. SHOUTING_CASE is the one shape where\n * the tail is not meaningful, so it is folded down.\n */\nexport function toPascalCase(str: string): string {\n return str\n .split(/[_\\-\\s]+/)\n .filter(Boolean)\n .map(word => {\n const rest = /^[A-Z0-9]+$/.test(word) ? word.slice(1).toLowerCase() : word.slice(1);\n return word.charAt(0).toUpperCase() + rest;\n })\n .join(\"\");\n}\n\n/**\n * Convert a slug/snake_case string to camelCase\n * e.g. \"private_notes\" → \"privateNotes\"\n */\nexport function toCamelCase(str: string): string {\n if (!/[_\\-\\s]/.test(str)) {\n return str.charAt(0).toLowerCase() + str.slice(1);\n }\n const pascal = toPascalCase(str);\n return pascal.charAt(0).toLowerCase() + pascal.slice(1);\n}\n\n/**\n * Convert a slug to a safe JS identifier\n * e.g. \"private-notes\" → \"privateNotes\"\n */\nexport function toSafeIdentifier(str: string): string {\n return toCamelCase(str.replace(/[^a-zA-Z0-9_]/g, \"_\"));\n}\n\n/**\n * Indent a block of text by a given number of spaces\n */\nexport function indent(text: string, spaces: number): string {\n const pad = \" \".repeat(spaces);\n return text\n .split(\"\\n\")\n .map(line => (line.trim() ? pad + line : line))\n .join(\"\\n\");\n}\n","import { CollectionConfig, Property, Properties, MapProperty, ArrayProperty, StringProperty, NumberProperty, ResolvedRelation } from \"@rebasepro/types\";\nimport { findRelation, resolveCollectionRelations } from \"@rebasepro/common\";\nimport { toSafeIdentifier } from \"./utils\";\n\n/**\n * A schema that cannot be expressed as a valid TypeScript file.\n *\n * Thrown rather than emitted. The generator used to concatenate whatever it was\n * given, so a slug that collided with another one, or that was not an\n * identifier, produced a file that either failed to compile or — worse —\n * compiled while quietly routing one collection to another's slug.\n */\nexport class CodegenError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"CodegenError\";\n }\n}\n\nconst IDENTIFIER = /^[A-Za-z_$][A-Za-z0-9_$]*$/;\n\n/**\n * A property name for the emitted TypeScript: verbatim when it is a valid\n * identifier, quoted otherwise.\n *\n * Every key that reaches the output goes through here. Column names are not\n * required to be identifiers — `\"order\"`, `\"user id\"`, a quoted Postgres\n * identifier — and the previous behaviour of camel-casing them into shape\n * renamed the column in the type while the wire kept the original, so the\n * generated `Row` described fields that did not exist.\n */\nfunction emitKey(key: string): string {\n return IDENTIFIER.test(key) ? key : JSON.stringify(key);\n}\n\n/**\n * A string literal, escaped.\n *\n * `\"${value}\"` was the previous form. A value containing a quote closed the\n * literal early, which at best broke the file and at worst let a slug from a\n * remote contract inject top-level statements into a file the developer\n * compiles and bundles.\n */\nfunction emitString(value: string): string {\n return JSON.stringify(value);\n}\n\n/** The `id`s of an enum declared as an array, an array of `{ id }`, or an object map. */\nfunction enumIds(raw: unknown): (string | number)[] {\n if (Array.isArray(raw)) {\n return raw.map((entry: string | number | { id: string | number }) =>\n entry && typeof entry === \"object\" ? entry.id : entry);\n }\n if (raw && typeof raw === \"object\") return Object.keys(raw);\n return [];\n}\n\nfunction propertyToTypeScriptType(prop: Property): string {\n switch (prop.type) {\n case \"string\": {\n const sp = prop as StringProperty;\n if (sp.enum) {\n const ids = enumIds(sp.enum);\n if (ids.length === 0) return \"string\";\n return ids.map(v => emitString(String(v))).join(\" | \");\n }\n return \"string\";\n }\n case \"number\": {\n const np = prop as NumberProperty;\n if (np.enum) {\n const ids = enumIds(np.enum);\n const numbers = ids.map(Number);\n // A numeric enum carrying something that is not a number cannot\n // be written as a union of numeric literals. Widening to\n // `number` is imprecise; emitting `NaN | undefined` is invalid.\n if (ids.length === 0 || numbers.some(n => !Number.isFinite(n))) return \"number\";\n return numbers.map(n => String(n)).join(\" | \");\n }\n return \"number\";\n }\n case \"boolean\":\n return \"boolean\";\n case \"date\":\n return \"string\"; // ISO 8601 string over the wire\n case \"geopoint\":\n return \"{ latitude: number; longitude: number; }\";\n case \"reference\":\n return \"string | number\";\n case \"relation\":\n return \"string | number\";\n case \"map\": {\n const mapProp = prop as MapProperty;\n if (mapProp.properties) {\n const inner = Object.entries(mapProp.properties)\n .map(([k, v]) => {\n const child = v as Property;\n // Nested fields carry validation like any other. Emitting\n // them all required claimed a shape the payload does not\n // have to satisfy.\n const optional = !child.validation?.required;\n const type = propertyToTypeScriptType(child);\n return `${emitKey(k)}${optional ? \"?\" : \"\"}: ${optional ? `${type} | null` : type};`;\n })\n .join(\" \");\n return `{ ${inner} }`;\n }\n return \"Record<string, unknown>\";\n }\n case \"array\": {\n const arrProp = prop as ArrayProperty;\n if (arrProp.of) {\n return `Array<${propertyToTypeScriptType(arrProp.of as Property)}>`;\n }\n return \"Array<unknown>\";\n }\n case \"vector\":\n return \"number[]\";\n case \"binary\":\n return \"string\";\n default:\n return \"unknown\";\n }\n}\n\n/**\n * Unwrap a relation target that arrived as a module namespace rather than the\n * collection itself — `target: () => import(\"./authors\")` is a common slip.\n */\nfunction resolveTargetCollection(relation: ResolvedRelation): CollectionConfig | undefined {\n try {\n let target = relation.target() as CollectionConfig & { default?: CollectionConfig; __esModule?: boolean };\n if (target && (target.default || target.__esModule)) {\n target = (target.default ?? target) as typeof target;\n }\n return target;\n } catch {\n return undefined;\n }\n}\n\n/** The TypeScript type of a foreign key: whatever the target's primary key is. */\nfunction foreignKeyType(relation: ResolvedRelation): string {\n const target = resolveTargetCollection(relation);\n if (!target?.properties) return \"string | number\";\n const idProp = Object.entries(target.properties).find(([_, p]) => (p as Record<string, unknown>).isId);\n if (!idProp) return \"string | number\";\n return (idProp[1] as Property).type === \"number\" ? \"number\" : \"string\";\n}\n\n/** Whether a property is the collection's primary key. */\nfunction isPrimaryKey(prop: Property): boolean {\n return Boolean((prop as unknown as Record<string, unknown>).isId);\n}\n\n/**\n * Whether the server assigns this primary key, so a write does not have to.\n * `true` and `\"manual\"` both mean the caller supplies it.\n */\nfunction isAutoAssignedId(prop: Property): boolean {\n const isId = (prop as unknown as Record<string, unknown>).isId;\n return Boolean(isId) && isId !== \"manual\" && isId !== true;\n}\n\n/**\n * The type an *included* relation arrives as: the target's own row, inlined.\n *\n * This is what the read pipeline actually serves — `toRestRow` puts the\n * target's flat columns where the relation was, and the SDK and the HTTP API\n * both go through it. It is deliberately *not* a `{ __type: \"relation\" }`\n * envelope: that shape is the admin's view-model and never reaches a\n * developer's `find()`.\n *\n * Falls back to an open record when the target is not part of this generation\n * run, since there is no `Row` to point at.\n */\nfunction includedRelationType(\n relation: ResolvedRelation,\n accessors: Map<string, string>\n): string {\n const target = resolveTargetCollection(relation);\n const slug = target?.slug ?? relation.targetSlug;\n const accessor = slug ? accessors.get(slug) : undefined;\n const rowType = accessor\n ? `Database[${emitString(accessor)}][\"Row\"]`\n : \"Record<string, unknown>\";\n return relation.cardinality === \"many\" ? `Array<${rowType}>` : rowType;\n}\n\n/**\n * Map every slug to the property name it is reachable under on `client.data`.\n *\n * The accessor is a safe identifier because `client.data.myNotes` is the point\n * of generating this at all, and `collectionsDictionary` maps it back to the\n * slug the wire uses. Two slugs that safe down to the same identifier cannot\n * both have it: the interface would not compile, and the dictionary — an object\n * literal — would silently keep only the last, routing one collection's reads\n * to the other's table. There is no defensible way to pick, so this refuses.\n */\nfunction buildAccessors(collections: CollectionConfig[]): Map<string, string> {\n const accessors = new Map<string, string>();\n const bySafeName = new Map<string, string>();\n\n for (const collection of collections) {\n const slug = collection.slug;\n if (typeof slug !== \"string\" || slug.length === 0) {\n throw new CodegenError(\n \"A collection has no slug, so it has no name to generate a type for. \" +\n \"Every collection needs a unique `slug`.\"\n );\n }\n\n const safe = toSafeIdentifier(slug);\n if (safe.length === 0) {\n throw new CodegenError(\n `The slug ${emitString(slug)} has no characters that can form a property name, ` +\n \"so it cannot be reached as `client.data.<name>`. Use a slug containing \" +\n \"letters, digits, underscores or dashes.\"\n );\n }\n\n const existing = bySafeName.get(safe);\n if (existing !== undefined) {\n throw new CodegenError(\n `The collections ${emitString(existing)} and ${emitString(slug)} both generate the ` +\n `accessor \"${safe}\", so only one of them could be reached from the generated client ` +\n \"and the other's reads would silently go to the wrong table. Rename one of the slugs.\"\n );\n }\n\n bySafeName.set(safe, slug);\n accessors.set(slug, safe);\n }\n\n return accessors;\n}\n\n/** One emitted `key: type;` line, already indented. */\nfunction line(key: string, type: string, optional: boolean): string {\n return ` ${emitKey(key)}${optional ? \"?\" : \"\"}: ${type};`;\n}\n\nexport function generateTypedefs(collections: CollectionConfig[]): string {\n const accessors = buildAccessors(collections);\n const lines: string[] = [\n \"/**\",\n \" * This file was auto-generated by Rebase.\",\n \" * Do not make direct changes to the file.\",\n \" */\",\n \"\",\n \"export interface Database {\"\n ];\n\n for (const collection of collections) {\n const properties = (collection.properties ?? {}) as Properties;\n\n // Resolve relations\n let resolvedRelations: Record<string, ResolvedRelation> = {};\n try {\n resolvedRelations = resolveCollectionRelations(collection);\n } catch (e) {\n // Swallowed before, which made this the quietest way to ship a\n // wrong type. The foreign-key columns are emitted from the resolved\n // relations rather than from the properties, so losing them drops\n // both the relation fields *and* columns that exist in the\n // database — and the resulting error surfaces in the user's code,\n // typechecking against a `Database` that is missing `author_id`,\n // with nothing pointing back at generation.\n //\n // A target thunk usually throws because of a circular import; the\n // boot-time relation validator names the same cause.\n console.warn(\n `[rebase] Could not resolve the relations of \"${collection.slug}\", so its generated ` +\n \"type has no relation fields and none of their foreign-key columns. This is usually a \" +\n \"circular import in the collection files — make sure the target is `() => otherCollection` \" +\n `and not evaluated at module load.\\n ${e instanceof Error ? e.message : String(e)}`\n );\n }\n\n // Subcollections are collections in their own right and are addressed\n // over a nested path, not as `client.data.<name>`. Generating them here\n // would invent an accessor the client does not serve, so they are\n // skipped — loudly, because doing it silently is how a developer\n // concludes the generator is broken.\n const subcollections = (collection as unknown as { subcollections?: unknown[] }).subcollections;\n if (Array.isArray(subcollections) && subcollections.length > 0) {\n console.warn(\n `[rebase] \"${collection.slug}\" declares ${subcollections.length} subcollection(s), which are ` +\n \"not part of the generated Database: they are reached over a nested path \" +\n `(\\`data/${collection.slug}/<id>/<relation>\\`), not as a top-level accessor. Register a ` +\n \"subcollection as a collection of its own if you want a typed accessor for it.\"\n );\n }\n\n lines.push(` ${emitKey(accessors.get(collection.slug)!)}: {`);\n\n // ── Row Type ──\n //\n // What a read serves. There is no field selection in the query API, so\n // every column of a row comes back on every read; a column is optional\n // here only because the value may be absent or null, never because the\n // caller might not have asked for it.\n lines.push(\" Row: {\");\n const emittedKeys = new Set<string>();\n\n // 1. Direct properties\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n\n // `excludeFromApi` is a server-side guarantee that the column is\n // stripped from every row the API serves, for every caller. Typing\n // it as readable is a lie the scaffolded `users` collection makes\n // about its own password hash.\n if (prop.excludeFromApi) continue;\n\n const tsType = propertyToTypeScriptType(prop);\n // A primary key is on every row a read can return, whether or not\n // anyone wrote `validation: { required: true }` next to it —\n // introspection never does, so `row.id` was `string | undefined`\n // for every baas project.\n const isRequired = Boolean(prop.validation?.required) || isPrimaryKey(prop);\n lines.push(line(key, isRequired ? tsType : `${tsType} | null`, !isRequired));\n emittedKeys.add(key);\n }\n\n // 2. FK columns from relations.\n //\n // Emitted under `localKey` verbatim: that is the Drizzle field key, so\n // it is the JSON key the row arrives with. Reshaping it into\n // `authorId` described a column that does not exist and hid the one\n // that does — including from `where` and `orderBy`, which are keyed off\n // this type.\n for (const [relKey, relation] of Object.entries(resolvedRelations)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) {\n const fkKey = relation.localKey;\n if (emittedKeys.has(fkKey)) continue;\n\n const fkType = foreignKeyType(relation);\n\n // A relation addressed by the same name as its own foreign key\n // is served *over* that column when the read includes it: the\n // query nests the target under the relation name, and the\n // scalar it shadows is gone. Both outcomes are real, so the\n // column is typed as both — which is what stops a plain\n // `const id: string = row.author_id` from compiling.\n const shadowedByInclude = relKey === fkKey;\n const tsType = shadowedByInclude\n ? `${fkType} | ${includedRelationType(relation, accessors)}`\n : fkType;\n\n const isRequired = Boolean(relation.validation?.required) && !shadowedByInclude;\n lines.push(line(fkKey, isRequired ? tsType : `${tsType} | null`, !isRequired));\n emittedKeys.add(fkKey);\n }\n }\n\n // 3. Relation fields — the target's own row, inlined.\n //\n // Optional throughout: a relation is only loaded when the read names it\n // in `include`, so it is absent from every other read.\n for (const [key, relation] of Object.entries(resolvedRelations)) {\n if (emittedKeys.has(key)) continue;\n lines.push(line(key, includedRelationType(relation, accessors), true));\n emittedKeys.add(key);\n }\n\n // A `relation` property whose relation could not be resolved — an\n // engine without relation support, or a target that did not load. It is\n // still a column on the row, so it is still typed, just not precisely.\n for (const [key, rawProp] of Object.entries(properties)) {\n if ((rawProp as Property).type !== \"relation\") continue;\n if (emittedKeys.has(key)) continue;\n lines.push(line(key, \"Record<string, unknown>\", true));\n emittedKeys.add(key);\n }\n lines.push(\" };\");\n\n // ── Insert Type ──\n //\n // What `create()` accepts. `excludeFromApi` columns are included: they\n // are stripped from *responses*, not from writes.\n lines.push(\" Insert: {\");\n emittedKeys.clear();\n\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n const tsType = propertyToTypeScriptType(prop);\n const isOptional = !prop.validation?.required || isAutoAssignedId(prop);\n lines.push(line(key, tsType, isOptional));\n emittedKeys.add(key);\n }\n\n emitWritableRelations(lines, properties, resolvedRelations, emittedKeys, false);\n lines.push(\" };\");\n\n // ── Update Type ──\n //\n // Everything optional, and the primary key left out: an update\n // addresses a row by id, it does not reassign one. Accepting `id` here\n // typechecked `update(id, { id: someoneElses })`.\n lines.push(\" Update: {\");\n emittedKeys.clear();\n for (const [key, rawProp] of Object.entries(properties)) {\n const prop = rawProp as Property;\n if (prop.type === \"relation\") continue;\n if (isPrimaryKey(prop)) continue;\n lines.push(line(key, propertyToTypeScriptType(prop), true));\n emittedKeys.add(key);\n }\n emitWritableRelations(lines, properties, resolvedRelations, emittedKeys, true);\n lines.push(\" };\");\n\n lines.push(\" };\");\n }\n\n lines.push(\"}\");\n lines.push(\"\");\n lines.push(\"export type CollectionName = keyof Database;\");\n lines.push(\"\");\n lines.push(\"export const collectionsDictionary = {\");\n for (const collection of collections) {\n lines.push(` ${emitKey(accessors.get(collection.slug)!)}: ${emitString(collection.slug)},`);\n }\n lines.push(\"} as const;\");\n lines.push(\"\");\n // Describes the const above rather than restating its keys. The previous\n // `{ [K in CollectionName]: K }` said every value equalled its key, which is\n // false for any slug that is not already an identifier — `myNotes` maps to\n // `\"my-notes\"` — so the export the CLI tells people to pass did not satisfy\n // its own published type.\n lines.push(\"export type CollectionsDictionary = typeof collectionsDictionary;\");\n lines.push(\"\");\n\n return lines.join(\"\\n\");\n}\n\n/**\n * The two ways a write can name a `belongsTo` target, both of which the server\n * accepts: the foreign-key column itself (`{ author_id: 5 }`, which passes\n * through untouched) and the relation *property* (`{ author: 5 }`, which the\n * write transformer maps onto that column).\n *\n * Only the first was generated, so the documented and idiomatic write shape was\n * a type error.\n *\n * The second form is emitted under the **property key**, not the resolved\n * relation name, because that is what the transformer keys off: it looks the\n * payload key up in `properties` and only treats it as a relation if what it\n * finds there is one. A relation whose `relationName` differs from its property\n * key is reachable as the property and not as the name, so emitting the name\n * would have offered a key that writes to a column that does not exist.\n */\nfunction emitWritableRelations(\n lines: string[],\n properties: Properties,\n resolvedRelations: Record<string, ResolvedRelation>,\n emittedKeys: Set<string>,\n allOptional: boolean\n): void {\n // The target's primary key type is the same one `Row` uses. A hardcoded\n // `string | number` here accepted a string for a numeric-keyed target.\n const emit = (key: string, relation: ResolvedRelation): void => {\n if (emittedKeys.has(key)) return;\n const optional = allOptional || !relation.validation?.required;\n lines.push(line(key, foreignKeyType(relation), optional));\n emittedKeys.add(key);\n };\n\n for (const relation of Object.values(resolvedRelations)) {\n if (relation.kind === \"belongsTo\" && relation.localKey) emit(relation.localKey, relation);\n }\n\n for (const [key, rawProp] of Object.entries(properties)) {\n if ((rawProp as Property).type !== \"relation\") continue;\n const relation = findRelation(resolvedRelations, key);\n if (relation?.kind === \"belongsTo\" && relation.localKey) emit(key, relation);\n }\n}\n","/**\n * @rebasepro/codegen\n *\n * Generates a purely typed Typescript database definition.\n */\n\nimport { CollectionConfig } from \"@rebasepro/types\";\nimport { generateTypedefs } from \"./generate-types\";\n\nexport { generateTypedefs, CodegenError } from \"./generate-types\";\nexport { toPascalCase, toCamelCase, toSafeIdentifier, indent } from \"./utils\";\n\n// ─── Public API ────────────────────────────────────────────────────\n\nexport interface GeneratedFile {\n /** Relative file path within the output directory */\n path: string;\n /** File content */\n content: string;\n}\n\nexport interface GenerateSDKOptions {\n /** Whether to include a README file (default: true) */\n includeReadme?: boolean;\n}\n\nexport function generateSDK(\n collections: CollectionConfig[],\n options: GenerateSDKOptions = {}\n): GeneratedFile[] {\n const files: GeneratedFile[] = [];\n\n files.push({\n path: \"database.types.ts\",\n content: generateTypedefs(collections)\n });\n\n if (options.includeReadme !== false) {\n files.push({\n path: \"README.md\",\n content: `# Rebase SDK\n\n> Auto-generated by \\`rebase generate-sdk\\`. Do not edit manually.\n\n## Usage\n\n1. Install the client package:\n \\`\\`\\`bash\n npm install @rebasepro/client\n \\`\\`\\`\n\n2. Initialize with your generated types:\n \\`\\`\\`typescript\n import { createRebaseClient } from '@rebasepro/client';\n import { collectionsDictionary, type Database } from './database.types';\n\n const rebase = createRebaseClient<Database>({\n baseUrl: 'http://localhost:3001',\n // Maps each accessor back to the slug the wire uses. Without it a\n // hyphenated slug is not resolvable from the property name alone.\n collections: collectionsDictionary,\n });\n\n // Property access is the typed surface: rows, filters and sorts are all\n // checked against the generated Database.\n const { data: users } = await rebase.data.users.find();\n console.log(users[0].email); // flat access — no .values wrapper\n \\`\\`\\`\n\n## Field names are the ones the API serves\n\nThe generated \\`Row\\` uses each column's real name, unchanged — a \\`created_at\\`\ncolumn is \\`row.created_at\\`, not \\`row.createdAt\\`. \\`where\\` and \\`orderBy\\` are keyed\noff the same type, so what compiles is what the backend answers to.\n\nOnly the *collection accessor* is turned into a property name\n(\\`my-notes\\` → \\`rebase.data.myNotes\\`), which is what \\`collectionsDictionary\\` maps\nback.\n\n## \\`Row\\` vs \\`Insert\\` vs \\`Update\\`\n\n| Type | What it describes |\n|---|---|\n| \\`Row\\` | What a read serves. Nullable columns are \\`T \\\\| null\\`; relations appear only when \\`include\\` names them; \\`excludeFromApi\\` columns are absent. |\n| \\`Insert\\` | What \\`create()\\` accepts. Server-assigned ids are optional; a \\`belongsTo\\` target may be named either way (\\`{ author: 5 }\\` or \\`{ author_id: 5 }\\`). |\n| \\`Update\\` | What \\`update()\\` accepts. Everything optional, and the primary key is not settable. |\n\nIf you need an untyped escape hatch, \\`rebase.data.collection(slug)\\` still works —\nbut it is generic over \\`Record<string, unknown>\\` and gives up everything above.\n`\n });\n }\n\n return files;\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rebasepro/codegen",
|
|
3
|
-
"version": "0.13.1-canary.
|
|
3
|
+
"version": "0.13.1-canary.g501e3cb",
|
|
4
4
|
"description": "Generate a typed JS SDK from Rebase collection definitions",
|
|
5
5
|
"main": "./dist/index.es.js",
|
|
6
6
|
"module": "./dist/index.es.js",
|
|
@@ -22,8 +22,7 @@
|
|
|
22
22
|
"author": "rebase.pro",
|
|
23
23
|
"license": "MIT",
|
|
24
24
|
"peerDependencies": {
|
|
25
|
-
"@rebasepro/
|
|
26
|
-
"@rebasepro/types": "0.13.1-canary.g3660bd5"
|
|
25
|
+
"@rebasepro/types": "0.13.1-canary.g501e3cb"
|
|
27
26
|
},
|
|
28
27
|
"devDependencies": {
|
|
29
28
|
"@jest/globals": "^30.4.1",
|
|
@@ -33,8 +32,7 @@
|
|
|
33
32
|
"ts-jest": "^29.4.12",
|
|
34
33
|
"typescript": "^6.0.3",
|
|
35
34
|
"vite": "^8.1.5",
|
|
36
|
-
"@rebasepro/types": "0.13.1-canary.
|
|
37
|
-
"@rebasepro/common": "0.13.1-canary.g3660bd5"
|
|
35
|
+
"@rebasepro/types": "0.13.1-canary.g501e3cb"
|
|
38
36
|
},
|
|
39
37
|
"exports": {
|
|
40
38
|
".": {
|
|
@@ -44,7 +42,7 @@
|
|
|
44
42
|
},
|
|
45
43
|
"gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
|
|
46
44
|
"dependencies": {
|
|
47
|
-
"@rebasepro/
|
|
45
|
+
"@rebasepro/common": "0.13.1-canary.g501e3cb"
|
|
48
46
|
},
|
|
49
47
|
"repository": {
|
|
50
48
|
"type": "git",
|