@rebasepro/sdk-generator 0.2.1 → 0.2.3

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.
@@ -0,0 +1,51 @@
1
+ import { FindResponse, CollectionAccessor, QueryBuilderInterface, FilterOperator } from "@rebasepro/types";
2
+ export declare class QueryBuilder<M extends Record<string, unknown> = Record<string, unknown>> implements QueryBuilderInterface<M> {
3
+ private collection;
4
+ private params;
5
+ constructor(collection: CollectionAccessor<M>);
6
+ /**
7
+ * Add a filter condition to your query.
8
+ * @example
9
+ * client.collection('users').where('age', '>=', 18).find()
10
+ */
11
+ where(column: keyof M & string, operator: FilterOperator, value: unknown): this;
12
+ /**
13
+ * Order the results by a specific column.
14
+ * @example
15
+ * client.collection('users').orderBy('createdAt', 'desc').find()
16
+ */
17
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): this;
18
+ /**
19
+ * Limit the number of results returned.
20
+ */
21
+ limit(count: number): this;
22
+ /**
23
+ * Skip the first N results.
24
+ */
25
+ offset(count: number): this;
26
+ /**
27
+ * Set a free-text search string if supported by the backend.
28
+ */
29
+ search(searchString: string): this;
30
+ /**
31
+ * Include related entities in the response.
32
+ * Relations will be populated with full entity data instead of just IDs.
33
+ *
34
+ * @param relations - Relation names to include, or "*" for all.
35
+ * @example
36
+ * // Include specific relations
37
+ * client.data.posts.include("tags", "author").find()
38
+ *
39
+ * // Include all relations
40
+ * client.data.posts.include("*").find()
41
+ */
42
+ include(...relations: string[]): this;
43
+ /**
44
+ * Execute the find query and return the results.
45
+ */
46
+ find(): Promise<FindResponse<M>>;
47
+ /**
48
+ * Listen to realtime updates matching this query.
49
+ */
50
+ listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
51
+ }
@@ -1,3 +1,4 @@
1
1
  export * from "./util";
2
2
  export * from "./collections";
3
3
  export * from "./data/buildRebaseData";
4
+ export * from "./data/query_builder";
@@ -76,6 +76,21 @@ export interface FindResponse<M extends Record<string, unknown> = Record<string,
76
76
  hasMore: boolean;
77
77
  };
78
78
  }
79
+ export type FilterOperator = WhereFilterOpShort;
80
+ /**
81
+ * Fluent Query Builder Interface supported on both client and server accessors.
82
+ * @group Data
83
+ */
84
+ export interface QueryBuilderInterface<M extends Record<string, unknown> = Record<string, unknown>> {
85
+ where(column: keyof M & string, operator: FilterOperator, value: unknown): this;
86
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): this;
87
+ limit(count: number): this;
88
+ offset(count: number): this;
89
+ search(searchString: string): this;
90
+ include(...relations: string[]): this;
91
+ find(): Promise<FindResponse<M>>;
92
+ listen(onUpdate: (data: FindResponse<M>) => void, onError?: (error: Error) => void): () => void;
93
+ }
79
94
  /**
80
95
  * A single collection's CRUD accessor.
81
96
  *
@@ -124,6 +139,12 @@ export interface CollectionAccessor<M extends Record<string, unknown> = Record<s
124
139
  * Count the number of records matching the given filter.
125
140
  */
126
141
  count?(params?: FindParams): Promise<number>;
142
+ where(column: keyof M & string, operator: FilterOperator, value: unknown): QueryBuilderInterface<M>;
143
+ orderBy(column: keyof M & string, ascending?: "asc" | "desc"): QueryBuilderInterface<M>;
144
+ limit(count: number): QueryBuilderInterface<M>;
145
+ offset(count: number): QueryBuilderInterface<M>;
146
+ search(searchString: string): QueryBuilderInterface<M>;
147
+ include(...relations: string[]): QueryBuilderInterface<M>;
127
148
  }
128
149
  /**
129
150
  * The unified data access object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebasepro/sdk-generator",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Generate a typed JS SDK from Rebase collection definitions",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.es.js",
@@ -19,8 +19,8 @@
19
19
  "author": "rebase.pro",
20
20
  "license": "MIT",
21
21
  "peerDependencies": {
22
- "@rebasepro/common": "0.2.1",
23
- "@rebasepro/types": "0.2.1"
22
+ "@rebasepro/common": "0.2.3",
23
+ "@rebasepro/types": "0.2.3"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@jest/globals": "^29.7.0",
@@ -30,8 +30,8 @@
30
30
  "ts-jest": "^29.4.10",
31
31
  "typescript": "^5.9.3",
32
32
  "vite": "^7.3.3",
33
- "@rebasepro/common": "0.2.1",
34
- "@rebasepro/types": "0.2.1"
33
+ "@rebasepro/common": "0.2.3",
34
+ "@rebasepro/types": "0.2.3"
35
35
  },
36
36
  "exports": {
37
37
  ".": {
@@ -42,7 +42,12 @@
42
42
  },
43
43
  "gitHead": "d935eefa5aa8d1009a2398cfac2c1e4ee9aeb6b6",
44
44
  "dependencies": {
45
- "@rebasepro/client": "0.2.1"
45
+ "@rebasepro/client": "0.2.3"
46
+ },
47
+ "repository": {
48
+ "type": "git",
49
+ "url": "https://github.com/rebasepro/rebase.git",
50
+ "directory": "packages/sdk-generator"
46
51
  },
47
52
  "scripts": {
48
53
  "test": "jest --config jest.config.cjs",
@@ -12,15 +12,9 @@ const authorsCollection = {
12
12
  slug: "authors",
13
13
  table: "authors",
14
14
  properties: {
15
- id: { name: "ID",
16
- type: "number",
17
- isId: "increment",
18
- validation: { required: true } },
19
- name: { name: "Name",
20
- type: "string",
21
- validation: { required: true } },
22
- email: { name: "Email",
23
- type: "string" }
15
+ id: { name: "ID", type: "number", isId: "increment", validation: { required: true } },
16
+ name: { name: "Name", type: "string", validation: { required: true } },
17
+ email: { name: "Email", type: "string" }
24
18
  }
25
19
  } as unknown as EntityCollection;
26
20
 
@@ -30,7 +24,16 @@ describe("Utils", () => {
30
24
  expect(toPascalCase("private_notes")).toBe("PrivateNotes");
31
25
  });
32
26
  it("handles already PascalCase input", () => {
33
- expect(toPascalCase("TestEntities")).toBe("Testentities");
27
+ expect(toPascalCase("TestEntities")).toBe("Testentities"); // Note: toPascalCase implementation lowercases follow-up chars per word split
28
+ });
29
+ it("handles kebab-case", () => {
30
+ expect(toPascalCase("private-notes")).toBe("PrivateNotes");
31
+ });
32
+ it("handles space separated strings", () => {
33
+ expect(toPascalCase("private notes")).toBe("PrivateNotes");
34
+ });
35
+ it("handles multiple delimiters and empty chunks", () => {
36
+ expect(toPascalCase("private--notes__here")).toBe("PrivateNotesHere");
34
37
  });
35
38
  });
36
39
 
@@ -38,51 +41,460 @@ describe("Utils", () => {
38
41
  it("converts snake_case to camelCase", () => {
39
42
  expect(toCamelCase("private_notes")).toBe("privateNotes");
40
43
  });
44
+ it("converts kebab-case to camelCase", () => {
45
+ expect(toCamelCase("private-notes")).toBe("privateNotes");
46
+ });
41
47
  });
42
48
 
43
49
  describe("toSafeIdentifier", () => {
44
50
  it("converts slugs to camelCase", () => {
45
51
  expect(toSafeIdentifier("private-notes")).toBe("privateNotes");
46
52
  });
53
+ it("strips invalid JS identifier characters and camel cases", () => {
54
+ expect(toSafeIdentifier("my-special-slug!")).toBe("mySpecialSlug");
55
+ expect(toSafeIdentifier("some@name#here")).toBe("someNameHere");
56
+ });
57
+ });
58
+
59
+ describe("indent", () => {
60
+ it("indents multi-line string by given space count", () => {
61
+ const block = "line1\nline2\nline3";
62
+ const expected = " line1\n line2\n line3";
63
+ expect(indent(block, 2)).toBe(expected);
64
+ });
65
+ it("does not indent empty/whitespace lines", () => {
66
+ const block = "line1\n\n \nline2";
67
+ const expected = " line1\n\n \n line2";
68
+ expect(indent(block, 2)).toBe(expected);
69
+ });
70
+ });
71
+ });
72
+
73
+ describe("propertyToTypeScriptType mapping", () => {
74
+ it("maps basic types correctly", () => {
75
+ const col = {
76
+ slug: "types_collection",
77
+ properties: {
78
+ bool: { type: "boolean" },
79
+ dt: { type: "date" },
80
+ geo: { type: "geopoint" },
81
+ ref: { type: "reference" },
82
+ vec: { type: "vector" },
83
+ bin: { type: "binary" },
84
+ unknown: { type: "something-weird" }
85
+ }
86
+ } as unknown as EntityCollection;
87
+
88
+ const ts = generateTypedefs([col]);
89
+ expect(ts).toContain("bool?: boolean;");
90
+ expect(ts).toContain("dt?: string;");
91
+ expect(ts).toContain("geo?: { latitude: number; longitude: number; };");
92
+ expect(ts).toContain("ref?: string | number;");
93
+ expect(ts).toContain("vec?: number[];");
94
+ expect(ts).toContain("bin?: string;");
95
+ expect(ts).toContain("unknown?: any;");
96
+ });
97
+
98
+ describe("string enum mapping", () => {
99
+ it("maps string array enum", () => {
100
+ const col = {
101
+ slug: "posts",
102
+ properties: {
103
+ status: {
104
+ type: "string",
105
+ enum: ["draft", "published"]
106
+ }
107
+ }
108
+ } as unknown as EntityCollection;
109
+
110
+ const ts = generateTypedefs([col]);
111
+ expect(ts).toContain('status?: "draft" | "published";');
112
+ });
113
+
114
+ it("maps string array of objects enum", () => {
115
+ const col = {
116
+ slug: "posts",
117
+ properties: {
118
+ status: {
119
+ type: "string",
120
+ enum: [
121
+ { id: "draft", label: "Draft" },
122
+ { id: "published", label: "Published" }
123
+ ]
124
+ }
125
+ }
126
+ } as unknown as EntityCollection;
127
+
128
+ const ts = generateTypedefs([col]);
129
+ expect(ts).toContain('status?: "draft" | "published";');
130
+ });
131
+
132
+ it("maps string record enum using keys", () => {
133
+ const col = {
134
+ slug: "posts",
135
+ properties: {
136
+ status: {
137
+ type: "string",
138
+ enum: {
139
+ draft: "Draft",
140
+ published: "Published"
141
+ }
142
+ }
143
+ }
144
+ } as unknown as EntityCollection;
145
+
146
+ const ts = generateTypedefs([col]);
147
+ expect(ts).toContain('status?: "draft" | "published";');
148
+ });
149
+ });
150
+
151
+ describe("number enum mapping", () => {
152
+ it("maps number array enum", () => {
153
+ const col = {
154
+ slug: "posts",
155
+ properties: {
156
+ level: {
157
+ type: "number",
158
+ enum: [1, 2, 3]
159
+ }
160
+ }
161
+ } as unknown as EntityCollection;
162
+
163
+ const ts = generateTypedefs([col]);
164
+ expect(ts).toContain("level?: 1 | 2 | 3;");
165
+ });
166
+
167
+ it("maps number array of objects enum", () => {
168
+ const col = {
169
+ slug: "posts",
170
+ properties: {
171
+ level: {
172
+ type: "number",
173
+ enum: [
174
+ { id: 10, label: "Low" },
175
+ { id: 20, label: "High" }
176
+ ]
177
+ }
178
+ }
179
+ } as unknown as EntityCollection;
180
+
181
+ const ts = generateTypedefs([col]);
182
+ expect(ts).toContain("level?: 10 | 20;");
183
+ });
184
+
185
+ it("maps number record enum using keys", () => {
186
+ const col = {
187
+ slug: "posts",
188
+ properties: {
189
+ level: {
190
+ type: "number",
191
+ enum: {
192
+ 1: "Low",
193
+ 2: "High"
194
+ }
195
+ }
196
+ }
197
+ } as unknown as EntityCollection;
198
+
199
+ const ts = generateTypedefs([col]);
200
+ expect(ts).toContain("level?: 1 | 2;");
201
+ });
202
+ });
203
+
204
+ describe("map property mapping", () => {
205
+ it("maps nested properties recursively", () => {
206
+ const col = {
207
+ slug: "users",
208
+ properties: {
209
+ profile: {
210
+ type: "map",
211
+ properties: {
212
+ age: { type: "number" },
213
+ tagline: { type: "string" }
214
+ }
215
+ }
216
+ }
217
+ } as unknown as EntityCollection;
218
+
219
+ const ts = generateTypedefs([col]);
220
+ expect(ts).toContain("profile?: { age: number; tagline: string; };");
221
+ });
222
+
223
+ it("falls back to Record<string, any> if properties is absent", () => {
224
+ const col = {
225
+ slug: "users",
226
+ properties: {
227
+ metadata: {
228
+ type: "map"
229
+ }
230
+ }
231
+ } as unknown as EntityCollection;
232
+
233
+ const ts = generateTypedefs([col]);
234
+ expect(ts).toContain("metadata?: Record<string, any>;");
235
+ });
236
+ });
237
+
238
+ describe("array property mapping", () => {
239
+ it("maps typed array using the 'of' property", () => {
240
+ const col = {
241
+ slug: "articles",
242
+ properties: {
243
+ tags: {
244
+ type: "array",
245
+ of: { type: "string" }
246
+ }
247
+ }
248
+ } as unknown as EntityCollection;
249
+
250
+ const ts = generateTypedefs([col]);
251
+ expect(ts).toContain("tags?: Array<string>;");
252
+ });
253
+
254
+ it("falls back to Array<any> if 'of' property is absent", () => {
255
+ const col = {
256
+ slug: "articles",
257
+ properties: {
258
+ generic: {
259
+ type: "array"
260
+ }
261
+ }
262
+ } as unknown as EntityCollection;
263
+
264
+ const ts = generateTypedefs([col]);
265
+ expect(ts).toContain("generic?: Array<any>;");
266
+ });
267
+ });
268
+ });
269
+
270
+ describe("generateTypedefs schemas configurations", () => {
271
+ describe("Insert type requirements", () => {
272
+ it("makes properties optional in Insert if validation required is false", () => {
273
+ const col = {
274
+ slug: "books",
275
+ properties: {
276
+ title: { type: "string" }
277
+ }
278
+ } as unknown as EntityCollection;
279
+
280
+ const ts = generateTypedefs([col]);
281
+ expect(ts).toContain("Insert: {");
282
+ expect(ts).toContain("title?: string;");
283
+ });
284
+
285
+ it("makes properties required in Insert if validation required is true", () => {
286
+ const col = {
287
+ slug: "books",
288
+ properties: {
289
+ title: { type: "string", validation: { required: true } }
290
+ }
291
+ } as unknown as EntityCollection;
292
+
293
+ const ts = generateTypedefs([col]);
294
+ expect(ts).toContain("Insert: {");
295
+ expect(ts).toContain("title: string;");
296
+ });
297
+
298
+ it("makes isId: 'increment' key optional in Insert even if validation required is true", () => {
299
+ const col = {
300
+ slug: "books",
301
+ properties: {
302
+ id: { type: "number", isId: "increment", validation: { required: true } }
303
+ }
304
+ } as unknown as EntityCollection;
305
+
306
+ const ts = generateTypedefs([col]);
307
+ expect(ts).toContain("Insert: {");
308
+ expect(ts).toContain("id?: number;");
309
+ });
310
+
311
+ it("makes isId: 'uuid' key optional in Insert even if validation required is true", () => {
312
+ const col = {
313
+ slug: "books",
314
+ properties: {
315
+ id: { type: "string", isId: "uuid", validation: { required: true } }
316
+ }
317
+ } as unknown as EntityCollection;
318
+
319
+ const ts = generateTypedefs([col]);
320
+ expect(ts).toContain("Insert: {");
321
+ expect(ts).toContain("id?: string;");
322
+ });
323
+
324
+ it("keeps isId: 'manual' key required in Insert if validation required is true", () => {
325
+ const col = {
326
+ slug: "books",
327
+ properties: {
328
+ id: { type: "string", isId: "manual", validation: { required: true } }
329
+ }
330
+ } as unknown as EntityCollection;
331
+
332
+ const ts = generateTypedefs([col]);
333
+ expect(ts).toContain("Insert: {");
334
+ expect(ts).toContain("id: string;");
335
+ });
336
+
337
+ it("keeps isId: true key required in Insert if validation required is true", () => {
338
+ const col = {
339
+ slug: "books",
340
+ properties: {
341
+ id: { type: "string", isId: true, validation: { required: true } }
342
+ }
343
+ } as unknown as EntityCollection;
344
+
345
+ const ts = generateTypedefs([col]);
346
+ expect(ts).toContain("Insert: {");
347
+ expect(ts).toContain("id: string;");
348
+ });
349
+ });
350
+
351
+ describe("Update type optionality", () => {
352
+ it("makes all direct fields optional in Update", () => {
353
+ const col = {
354
+ slug: "books",
355
+ properties: {
356
+ id: { type: "number", isId: "increment", validation: { required: true } },
357
+ title: { type: "string", validation: { required: true } }
358
+ }
359
+ } as unknown as EntityCollection;
360
+
361
+ const ts = generateTypedefs([col]);
362
+ expect(ts).toContain("Update: {");
363
+ expect(ts).toContain("id?: number;");
364
+ expect(ts).toContain("title?: string;");
365
+ });
47
366
  });
48
367
  });
49
368
 
50
- describe("generateTypedefs", () => {
51
- it("generates a typescript interface for a collection", () => {
52
- const ts = generateTypedefs([authorsCollection]);
369
+ describe("Collection relations and FK resolutions", () => {
370
+ it("handles target primary key type resolution for FK columns", () => {
371
+ const authorsCol = {
372
+ slug: "authors",
373
+ driver: "postgres",
374
+ properties: {
375
+ id: { type: "number", isId: "increment" }
376
+ }
377
+ } as unknown as EntityCollection;
378
+
379
+ const postsCol = {
380
+ slug: "posts",
381
+ driver: "postgres",
382
+ properties: {
383
+ id: { type: "number", isId: "increment" },
384
+ author: {
385
+ type: "relation",
386
+ target: () => authorsCol,
387
+ cardinality: "one",
388
+ direction: "owning",
389
+ localKey: "author_id"
390
+ }
391
+ }
392
+ } as unknown as EntityCollection;
393
+
394
+ const ts = generateTypedefs([postsCol, authorsCol]);
395
+
396
+ // Row should have authorId as number since authors PK (id) is number
397
+ expect(ts).toContain("posts: {");
398
+ expect(ts).toContain("Row: {");
399
+ expect(ts).toContain("authorId?: number;");
400
+ });
401
+
402
+ it("defaults to string | number when target primary key cannot be resolved", () => {
403
+ const postsCol = {
404
+ slug: "posts",
405
+ driver: "postgres",
406
+ properties: {
407
+ id: { type: "number", isId: "increment" },
408
+ author: {
409
+ type: "relation",
410
+ target: () => ({ name: "no-properties" }),
411
+ cardinality: "one",
412
+ direction: "owning",
413
+ localKey: "author_id"
414
+ }
415
+ }
416
+ } as unknown as EntityCollection;
53
417
 
54
- expect(ts).toContain("export interface Database {");
55
- expect(ts).toContain("authors: {");
418
+ const ts = generateTypedefs([postsCol]);
56
419
 
57
- // Row Type
420
+ expect(ts).toContain("posts: {");
58
421
  expect(ts).toContain("Row: {");
59
- expect(ts).toContain("id: number;");
60
- expect(ts).toContain("name: string;");
61
- expect(ts).toContain("email?: string;");
62
-
63
- // Insert Type
64
- expect(ts).toContain("Insert: {");
65
- expect(ts).toContain("id?: number;");
66
- expect(ts).toContain("name: string;");
67
- expect(ts).toContain("email?: string;");
68
-
69
- // Update Type
70
- expect(ts).toContain("Update: {");
71
- expect(ts).toContain("id?: number;");
72
- expect(ts).toContain("name?: string;");
73
- expect(ts).toContain("email?: string;");
74
-
75
- // Dictionary
76
- expect(ts).toContain("export const collectionsDictionary = {");
77
- expect(ts).toContain("authors: \"authors\",");
422
+ expect(ts).toContain("authorId?: string | number;");
423
+ });
424
+
425
+ it("supports relation validation constraints making FK required", () => {
426
+ const authorsCol = {
427
+ slug: "authors",
428
+ driver: "postgres",
429
+ properties: {
430
+ id: { type: "string", isId: "uuid" }
431
+ }
432
+ } as unknown as EntityCollection;
433
+
434
+ const postsCol = {
435
+ slug: "posts",
436
+ driver: "postgres",
437
+ properties: {
438
+ id: { type: "number", isId: "increment" },
439
+ author: {
440
+ type: "relation",
441
+ relationName: "author_rel",
442
+ collectionPath: "authors"
443
+ }
444
+ },
445
+ relations: [
446
+ {
447
+ relationName: "author_rel",
448
+ target: () => authorsCol,
449
+ cardinality: "one",
450
+ direction: "owning",
451
+ localKey: "author_id",
452
+ validation: { required: true }
453
+ }
454
+ ]
455
+ } as unknown as EntityCollection;
456
+
457
+ const ts = generateTypedefs([postsCol, authorsCol]);
458
+
459
+ expect(ts).toContain("posts: {");
460
+ expect(ts).toContain("Row: {");
461
+ expect(ts).toContain("authorId: string;"); // target is string uuid, validation required is true
462
+ });
463
+
464
+ it("generates relation fields mapped correctly to relation type helpers", () => {
465
+ const tagsCol = {
466
+ slug: "tags",
467
+ driver: "postgres",
468
+ properties: {
469
+ id: { type: "number", isId: "increment" }
470
+ }
471
+ } as unknown as EntityCollection;
472
+
473
+ const postsCol = {
474
+ slug: "posts",
475
+ driver: "postgres",
476
+ properties: {
477
+ id: { type: "number", isId: "increment" },
478
+ tags: {
479
+ type: "relation",
480
+ target: () => tagsCol,
481
+ cardinality: "many",
482
+ direction: "owning"
483
+ }
484
+ }
485
+ } as unknown as EntityCollection;
486
+
487
+ const ts = generateTypedefs([postsCol, tagsCol]);
488
+
489
+ // Row should have relation helper field
490
+ expect(ts).toContain("tags?: Array<{ id: string | number; path: string; __type: \"relation\"; data?: any }>;");
78
491
  });
79
492
  });
80
493
 
81
- describe("generateSDK", () => {
82
- it("returns an array of generated files including database.types.ts and README.md", () => {
83
- const files = generateSDK([authorsCollection]);
84
- expect(files.length).toBe(2);
494
+ describe("generateSDK configurations", () => {
495
+ it("does not generate README.md if includeReadme is false", () => {
496
+ const files = generateSDK([authorsCollection], { includeReadme: false });
497
+ expect(files.length).toBe(1);
85
498
  expect(files[0].path).toBe("database.types.ts");
86
- expect(files[1].path).toBe("README.md");
87
499
  });
88
500
  });