@rebasepro/server-postgresql 0.0.1-canary.4d4fb3e
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/LICENSE +6 -0
- package/README.md +106 -0
- package/build-errors.txt +37 -0
- package/dist/common/src/collections/CollectionRegistry.d.ts +48 -0
- package/dist/common/src/collections/index.d.ts +1 -0
- package/dist/common/src/data/buildRebaseData.d.ts +14 -0
- package/dist/common/src/index.d.ts +3 -0
- package/dist/common/src/util/builders.d.ts +57 -0
- package/dist/common/src/util/callbacks.d.ts +6 -0
- package/dist/common/src/util/collections.d.ts +11 -0
- package/dist/common/src/util/common.d.ts +2 -0
- package/dist/common/src/util/conditions.d.ts +26 -0
- package/dist/common/src/util/entities.d.ts +36 -0
- package/dist/common/src/util/enums.d.ts +3 -0
- package/dist/common/src/util/index.d.ts +16 -0
- package/dist/common/src/util/navigation_from_path.d.ts +34 -0
- package/dist/common/src/util/navigation_utils.d.ts +20 -0
- package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
- package/dist/common/src/util/paths.d.ts +14 -0
- package/dist/common/src/util/permissions.d.ts +5 -0
- package/dist/common/src/util/references.d.ts +2 -0
- package/dist/common/src/util/relations.d.ts +12 -0
- package/dist/common/src/util/resolutions.d.ts +72 -0
- package/dist/common/src/util/storage.d.ts +24 -0
- package/dist/index.es.js +10635 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +10643 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +112 -0
- package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
- package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
- package/dist/server-postgresql/src/auth/services.d.ts +188 -0
- package/dist/server-postgresql/src/cli.d.ts +1 -0
- package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
- package/dist/server-postgresql/src/connection.d.ts +7 -0
- package/dist/server-postgresql/src/data-transformer.d.ts +36 -0
- package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
- package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
- package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
- package/dist/server-postgresql/src/index.d.ts +13 -0
- package/dist/server-postgresql/src/interfaces.d.ts +18 -0
- package/dist/server-postgresql/src/schema/auth-schema.d.ts +767 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
- package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
- package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
- package/dist/server-postgresql/src/services/EntityFetchService.d.ts +195 -0
- package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
- package/dist/server-postgresql/src/services/RelationService.d.ts +92 -0
- package/dist/server-postgresql/src/services/entity-helpers.d.ts +24 -0
- package/dist/server-postgresql/src/services/entityService.d.ts +102 -0
- package/dist/server-postgresql/src/services/index.d.ts +4 -0
- package/dist/server-postgresql/src/services/realtimeService.d.ts +186 -0
- package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
- package/dist/server-postgresql/src/websocket.d.ts +5 -0
- package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
- package/dist/types/src/controllers/auth.d.ts +117 -0
- package/dist/types/src/controllers/client.d.ts +58 -0
- package/dist/types/src/controllers/collection_registry.d.ts +44 -0
- package/dist/types/src/controllers/customization_controller.d.ts +54 -0
- package/dist/types/src/controllers/data.d.ts +141 -0
- package/dist/types/src/controllers/data_driver.d.ts +168 -0
- package/dist/types/src/controllers/database_admin.d.ts +11 -0
- package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
- package/dist/types/src/controllers/effective_role.d.ts +4 -0
- package/dist/types/src/controllers/index.d.ts +17 -0
- package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
- package/dist/types/src/controllers/navigation.d.ts +213 -0
- package/dist/types/src/controllers/registry.d.ts +51 -0
- package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
- package/dist/types/src/controllers/side_entity_controller.d.ts +89 -0
- package/dist/types/src/controllers/snackbar.d.ts +24 -0
- package/dist/types/src/controllers/storage.d.ts +173 -0
- package/dist/types/src/index.d.ts +4 -0
- package/dist/types/src/rebase_context.d.ts +101 -0
- package/dist/types/src/types/backend.d.ts +533 -0
- package/dist/types/src/types/builders.d.ts +14 -0
- package/dist/types/src/types/chips.d.ts +5 -0
- package/dist/types/src/types/collections.d.ts +812 -0
- package/dist/types/src/types/data_source.d.ts +64 -0
- package/dist/types/src/types/entities.d.ts +145 -0
- package/dist/types/src/types/entity_actions.d.ts +98 -0
- package/dist/types/src/types/entity_callbacks.d.ts +173 -0
- package/dist/types/src/types/entity_link_builder.d.ts +7 -0
- package/dist/types/src/types/entity_overrides.d.ts +9 -0
- package/dist/types/src/types/entity_views.d.ts +61 -0
- package/dist/types/src/types/export_import.d.ts +21 -0
- package/dist/types/src/types/index.d.ts +22 -0
- package/dist/types/src/types/locales.d.ts +4 -0
- package/dist/types/src/types/modify_collections.d.ts +5 -0
- package/dist/types/src/types/plugins.d.ts +225 -0
- package/dist/types/src/types/properties.d.ts +1091 -0
- package/dist/types/src/types/property_config.d.ts +70 -0
- package/dist/types/src/types/relations.d.ts +336 -0
- package/dist/types/src/types/slots.d.ts +228 -0
- package/dist/types/src/types/translations.d.ts +826 -0
- package/dist/types/src/types/user_management_delegate.d.ts +120 -0
- package/dist/types/src/types/websockets.d.ts +78 -0
- package/dist/types/src/users/index.d.ts +2 -0
- package/dist/types/src/users/roles.d.ts +22 -0
- package/dist/types/src/users/user.d.ts +46 -0
- package/jest-all.log +3128 -0
- package/jest.log +49 -0
- package/package.json +93 -0
- package/src/PostgresBackendDriver.ts +1024 -0
- package/src/PostgresBootstrapper.ts +232 -0
- package/src/auth/ensure-tables.ts +309 -0
- package/src/auth/services.ts +740 -0
- package/src/cli.ts +347 -0
- package/src/collections/PostgresCollectionRegistry.ts +96 -0
- package/src/connection.ts +62 -0
- package/src/data-transformer.ts +569 -0
- package/src/databasePoolManager.ts +84 -0
- package/src/history/HistoryService.ts +257 -0
- package/src/history/ensure-history-table.ts +45 -0
- package/src/index.ts +13 -0
- package/src/interfaces.ts +60 -0
- package/src/schema/auth-schema.ts +146 -0
- package/src/schema/generate-drizzle-schema-logic.ts +618 -0
- package/src/schema/generate-drizzle-schema.ts +151 -0
- package/src/services/BranchService.ts +237 -0
- package/src/services/EntityFetchService.ts +1447 -0
- package/src/services/EntityPersistService.ts +351 -0
- package/src/services/RelationService.ts +1012 -0
- package/src/services/entity-helpers.ts +121 -0
- package/src/services/entityService.ts +209 -0
- package/src/services/index.ts +13 -0
- package/src/services/realtimeService.ts +1005 -0
- package/src/utils/drizzle-conditions.ts +999 -0
- package/src/websocket.ts +487 -0
- package/test/auth-services.test.ts +569 -0
- package/test/branchService.test.ts +357 -0
- package/test/drizzle-conditions.test.ts +895 -0
- package/test/entityService.errors.test.ts +352 -0
- package/test/entityService.relations.test.ts +912 -0
- package/test/entityService.subcollection-search.test.ts +516 -0
- package/test/entityService.test.ts +977 -0
- package/test/generate-drizzle-schema.test.ts +795 -0
- package/test/historyService.test.ts +126 -0
- package/test/postgresDataDriver.test.ts +556 -0
- package/test/realtimeService.test.ts +276 -0
- package/test/relations.test.ts +662 -0
- package/test_drizzle_mock.js +3 -0
- package/test_find_changed.mjs +30 -0
- package/test_output.txt +3145 -0
- package/tsconfig.json +49 -0
- package/tsconfig.prod.json +20 -0
- package/vite.config.ts +82 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
import { EntityCollection, Relation } from "@rebasepro/types";
|
|
2
|
+
import { generateSchema } from "../src/schema/generate-drizzle-schema-logic";
|
|
3
|
+
import { sanitizeRelation } from "@rebasepro/common";
|
|
4
|
+
|
|
5
|
+
const mockAuthorCollection: EntityCollection = {
|
|
6
|
+
name: "Author",
|
|
7
|
+
slug: "author",
|
|
8
|
+
table: "authors",
|
|
9
|
+
properties: {
|
|
10
|
+
id: {
|
|
11
|
+
type: "number"
|
|
12
|
+
},
|
|
13
|
+
name: {
|
|
14
|
+
type: "string"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
idField: "id"
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const mockPostCollection: EntityCollection = {
|
|
21
|
+
name: "Post",
|
|
22
|
+
slug: "posts",
|
|
23
|
+
table: "posts",
|
|
24
|
+
properties: {
|
|
25
|
+
id: {
|
|
26
|
+
type: "number"
|
|
27
|
+
},
|
|
28
|
+
title: {
|
|
29
|
+
type: "string"
|
|
30
|
+
},
|
|
31
|
+
author_id: {
|
|
32
|
+
type: "number"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
idField: "id"
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const mockTagCollection: EntityCollection = {
|
|
39
|
+
name: "Tag",
|
|
40
|
+
slug: "tags",
|
|
41
|
+
table: "tags",
|
|
42
|
+
properties: {
|
|
43
|
+
id: {
|
|
44
|
+
type: "string"
|
|
45
|
+
},
|
|
46
|
+
name: {
|
|
47
|
+
type: "string"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
idField: "id"
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
describe("sanitizeRelation", () => {
|
|
54
|
+
|
|
55
|
+
it("should generate a default relationName if not provided", () => {
|
|
56
|
+
const relation: Partial<Relation> = {
|
|
57
|
+
target: () => mockPostCollection,
|
|
58
|
+
cardinality: "one"
|
|
59
|
+
};
|
|
60
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
61
|
+
expect(normalized.relationName).toBe("posts");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// --- Belongs-To (cardinality: 'one', direction: 'owning') ---
|
|
65
|
+
describe("Belongs-To (one-to-one/many-to-one)", () => {
|
|
66
|
+
it("should generate default localKey for a simple belongs-to relation", () => {
|
|
67
|
+
const relation: Partial<Relation> = {
|
|
68
|
+
relationName: "post",
|
|
69
|
+
target: () => mockPostCollection,
|
|
70
|
+
cardinality: "one"
|
|
71
|
+
};
|
|
72
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
73
|
+
expect(normalized.localKey).toEqual("post_id");
|
|
74
|
+
expect(normalized.direction).toEqual("owning");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should use provided `localKey` for a belongs-to relation", () => {
|
|
78
|
+
const relation: Partial<Relation> = {
|
|
79
|
+
relationName: "post",
|
|
80
|
+
target: () => mockPostCollection,
|
|
81
|
+
cardinality: "one",
|
|
82
|
+
localKey: "custom_post_fk"
|
|
83
|
+
};
|
|
84
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
85
|
+
expect(normalized.localKey).toEqual("custom_post_fk");
|
|
86
|
+
expect(normalized.direction).toEqual("owning");
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// --- Inverse One-to-One (cardinality: 'one', direction: 'inverse') ---
|
|
91
|
+
describe("Inverse One-to-One", () => {
|
|
92
|
+
it("should generate default foreignKeyOnTarget for an inverse one-to-one relation", () => {
|
|
93
|
+
const relation: Partial<Relation> = {
|
|
94
|
+
relationName: "profile",
|
|
95
|
+
target: () => mockPostCollection,
|
|
96
|
+
cardinality: "one",
|
|
97
|
+
direction: "inverse"
|
|
98
|
+
};
|
|
99
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
100
|
+
expect(normalized.foreignKeyOnTarget).toEqual("author_id");
|
|
101
|
+
expect(normalized.direction).toEqual("inverse");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("should use provided `foreignKeyOnTarget` for an inverse one-to-one relation", () => {
|
|
105
|
+
const relation: Partial<Relation> = {
|
|
106
|
+
relationName: "profile",
|
|
107
|
+
target: () => mockPostCollection,
|
|
108
|
+
cardinality: "one",
|
|
109
|
+
direction: "inverse",
|
|
110
|
+
foreignKeyOnTarget: "custom_author_fk"
|
|
111
|
+
};
|
|
112
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
113
|
+
expect(normalized.foreignKeyOnTarget).toEqual("custom_author_fk");
|
|
114
|
+
expect(normalized.direction).toEqual("inverse");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("should work with inverseRelationName property", () => {
|
|
118
|
+
const relation: Partial<Relation> = {
|
|
119
|
+
relationName: "profile",
|
|
120
|
+
target: () => mockPostCollection,
|
|
121
|
+
cardinality: "one",
|
|
122
|
+
direction: "inverse",
|
|
123
|
+
inverseRelationName: "author"
|
|
124
|
+
};
|
|
125
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
126
|
+
expect(normalized.foreignKeyOnTarget).toEqual("author_id");
|
|
127
|
+
expect(normalized.inverseRelationName).toEqual("author");
|
|
128
|
+
expect(normalized.direction).toEqual("inverse");
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// --- Has-Many (cardinality: 'many', direction: 'inverse') ---
|
|
133
|
+
describe("Has-Many (one-to-many)", () => {
|
|
134
|
+
it("should generate default foreignKeyOnTarget for a simple has-many relation", () => {
|
|
135
|
+
const relation: Partial<Relation> = {
|
|
136
|
+
relationName: "posts",
|
|
137
|
+
target: () => mockPostCollection,
|
|
138
|
+
cardinality: "many",
|
|
139
|
+
direction: "inverse"
|
|
140
|
+
};
|
|
141
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
142
|
+
expect(normalized.foreignKeyOnTarget).toEqual("author_id");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("should use provided `foreignKeyOnTarget` for a has-many relation", () => {
|
|
146
|
+
const relation: Partial<Relation> = {
|
|
147
|
+
relationName: "posts",
|
|
148
|
+
target: () => mockPostCollection,
|
|
149
|
+
cardinality: "many",
|
|
150
|
+
direction: "inverse",
|
|
151
|
+
foreignKeyOnTarget: "writer_id"
|
|
152
|
+
};
|
|
153
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
154
|
+
expect(normalized.foreignKeyOnTarget).toEqual("writer_id");
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// --- Many-To-Many (cardinality: 'many', through) ---
|
|
159
|
+
describe("Many-To-Many", () => {
|
|
160
|
+
it("should use provided `through` for a many-to-many relation", () => {
|
|
161
|
+
const relation: Partial<Relation> = {
|
|
162
|
+
relationName: "tags",
|
|
163
|
+
target: () => mockTagCollection,
|
|
164
|
+
cardinality: "many",
|
|
165
|
+
through: {
|
|
166
|
+
table: "posts_tags",
|
|
167
|
+
sourceColumn: "post_id",
|
|
168
|
+
targetColumn: "tag_id"
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const normalized = sanitizeRelation(relation, mockPostCollection);
|
|
172
|
+
expect(normalized.through).toEqual({
|
|
173
|
+
table: "posts_tags",
|
|
174
|
+
sourceColumn: "post_id",
|
|
175
|
+
targetColumn: "tag_id"
|
|
176
|
+
});
|
|
177
|
+
expect(normalized.direction).toEqual("owning");
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// --- Fallback/Default Behavior ---
|
|
182
|
+
describe("Fallback Behavior", () => {
|
|
183
|
+
it("should fallback to has-many for ambiguous 'many' without direction or through", () => {
|
|
184
|
+
const relation: Partial<Relation> = {
|
|
185
|
+
relationName: "posts",
|
|
186
|
+
target: () => mockPostCollection,
|
|
187
|
+
cardinality: "many"
|
|
188
|
+
};
|
|
189
|
+
const normalized = sanitizeRelation(relation, mockAuthorCollection);
|
|
190
|
+
// Should default to has-many (inverse) behavior
|
|
191
|
+
expect(normalized.direction).toEqual("inverse");
|
|
192
|
+
expect(normalized.foreignKeyOnTarget).toEqual("author_id");
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("should handle 'one' with 'owning' direction", () => {
|
|
196
|
+
const relation: Partial<Relation> = {
|
|
197
|
+
relationName: "author",
|
|
198
|
+
target: () => mockAuthorCollection,
|
|
199
|
+
cardinality: "one",
|
|
200
|
+
direction: "owning" // Changed from "inverse"
|
|
201
|
+
};
|
|
202
|
+
const normalized = sanitizeRelation(relation, mockPostCollection);
|
|
203
|
+
expect(normalized.localKey).toEqual("author_id");
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
/**
|
|
208
|
+
* Comprehensive test suite for complex relation scenarios
|
|
209
|
+
* This tests all the production use cases developers might implement
|
|
210
|
+
*/
|
|
211
|
+
describe("Comprehensive Relations Test Suite", () => {
|
|
212
|
+
|
|
213
|
+
const cleanSchema = (schema: string) => {
|
|
214
|
+
return schema
|
|
215
|
+
.replace(/\/\/.*$/gm, '')
|
|
216
|
+
.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
217
|
+
.replace(/\n{2,}/g, '\n')
|
|
218
|
+
.replace(/\s+/g, " ")
|
|
219
|
+
.trim();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
describe("Many-to-Many Relations", () => {
|
|
223
|
+
it("should handle many-to-many with a through table", async () => {
|
|
224
|
+
const authorsCollection: EntityCollection = {
|
|
225
|
+
slug: "authors",
|
|
226
|
+
table: "authors",
|
|
227
|
+
name: "Authors",
|
|
228
|
+
properties: {
|
|
229
|
+
name: { type: "string" },
|
|
230
|
+
books: { type: "relation", relationName: "books" }
|
|
231
|
+
},
|
|
232
|
+
relations: [
|
|
233
|
+
{
|
|
234
|
+
relationName: "books",
|
|
235
|
+
target: () => booksCollection,
|
|
236
|
+
cardinality: "many",
|
|
237
|
+
direction: "owning",
|
|
238
|
+
through: {
|
|
239
|
+
table: "author_books",
|
|
240
|
+
sourceColumn: "author_id",
|
|
241
|
+
targetColumn: "book_id"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
]
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
const booksCollection: EntityCollection = {
|
|
248
|
+
slug: "books",
|
|
249
|
+
table: "books",
|
|
250
|
+
name: "Books",
|
|
251
|
+
properties: {
|
|
252
|
+
title: { type: "string" }
|
|
253
|
+
}
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = await generateSchema([authorsCollection, booksCollection]);
|
|
257
|
+
const cleanResult = cleanSchema(result);
|
|
258
|
+
|
|
259
|
+
// Should create junction table
|
|
260
|
+
expect(cleanResult).toContain(`export const authorBooks = pgTable("author_books"`);
|
|
261
|
+
expect(cleanResult).toContain(`author_id: varchar("author_id").notNull().references(() => authors.id, { onDelete: "cascade" })`);
|
|
262
|
+
expect(cleanResult).toContain(`book_id: varchar("book_id").notNull().references(() => books.id, { onDelete: "cascade" })`);
|
|
263
|
+
expect(cleanResult).toContain(`export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ books: many(authorBooks, { relationName: "books" }) }));`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should handle a 4-table many-to-many chain with joinPath", async () => {
|
|
267
|
+
const usersCollection: EntityCollection = {
|
|
268
|
+
slug: "users",
|
|
269
|
+
table: "users",
|
|
270
|
+
name: "Users",
|
|
271
|
+
properties: {
|
|
272
|
+
name: { type: "string" },
|
|
273
|
+
permissions: { type: "relation", relationName: "permissions" }
|
|
274
|
+
},
|
|
275
|
+
relations: [
|
|
276
|
+
{
|
|
277
|
+
relationName: "permissions",
|
|
278
|
+
target: () => permissionsCollection,
|
|
279
|
+
cardinality: "many",
|
|
280
|
+
joinPath: [
|
|
281
|
+
{ table: "user_roles", on: { from: "id", to: "user_id" } },
|
|
282
|
+
{ table: "roles", on: { from: "role_id", to: "id" } },
|
|
283
|
+
{ table: "role_permissions", on: { from: "id", to: "role_id" } },
|
|
284
|
+
{ table: "permissions", on: { from: "permission_id", to: "id" } }
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
]
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const rolesCollection: EntityCollection = {
|
|
291
|
+
slug: "roles",
|
|
292
|
+
table: "roles",
|
|
293
|
+
name: "Roles",
|
|
294
|
+
properties: { name: { type: "string" } }
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
const permissionsCollection: EntityCollection = {
|
|
298
|
+
slug: "permissions",
|
|
299
|
+
table: "permissions",
|
|
300
|
+
name: "Permissions",
|
|
301
|
+
properties: { name: { type: "string" } }
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const result = await generateSchema([usersCollection, rolesCollection, permissionsCollection]);
|
|
305
|
+
const cleanResult = cleanSchema(result);
|
|
306
|
+
|
|
307
|
+
// joinPath relations use existing user-controlled tables - no views or Drizzle relations generated
|
|
308
|
+
expect(cleanResult).not.toContain("view_users_to_permissions");
|
|
309
|
+
expect(cleanResult).not.toContain("viewUsersToPermissions");
|
|
310
|
+
|
|
311
|
+
// Should generate basic table definitions for users, roles, and permissions
|
|
312
|
+
expect(cleanResult).toContain("export const users = pgTable(\"users\"");
|
|
313
|
+
expect(cleanResult).toContain("export const roles = pgTable(\"roles\"");
|
|
314
|
+
expect(cleanResult).toContain("export const permissions = pgTable(\"permissions\"");
|
|
315
|
+
|
|
316
|
+
// No Drizzle relations generated for joinPath relations
|
|
317
|
+
expect(cleanResult).not.toContain("usersRelations");
|
|
318
|
+
|
|
319
|
+
// No SQL view generation comments
|
|
320
|
+
expect(result).not.toContain("CREATE OR REPLACE VIEW");
|
|
321
|
+
expect(result).not.toContain("SQL VIEWS FOR COMPLEX RELATIONS");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
describe("Owning Relations", () => {
|
|
326
|
+
it("should generate owning one-to-one relations", async () => {
|
|
327
|
+
const authorsCollection: EntityCollection = {
|
|
328
|
+
slug: "authors",
|
|
329
|
+
table: "authors",
|
|
330
|
+
name: "Authors",
|
|
331
|
+
properties: {
|
|
332
|
+
name: { type: "string" },
|
|
333
|
+
profile: { type: "relation", relationName: "profile" }
|
|
334
|
+
},
|
|
335
|
+
relations: [
|
|
336
|
+
{
|
|
337
|
+
relationName: "profile",
|
|
338
|
+
target: () => profilesCollection,
|
|
339
|
+
cardinality: "one",
|
|
340
|
+
direction: "inverse",
|
|
341
|
+
inverseRelationName: "author"
|
|
342
|
+
}
|
|
343
|
+
]
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const profilesCollection: EntityCollection = {
|
|
347
|
+
slug: "profiles",
|
|
348
|
+
table: "profiles",
|
|
349
|
+
name: "Profiles",
|
|
350
|
+
properties: {
|
|
351
|
+
bio: { type: "string" },
|
|
352
|
+
author: { type: "relation", relationName: "author" }
|
|
353
|
+
},
|
|
354
|
+
relations: [
|
|
355
|
+
{
|
|
356
|
+
relationName: "author",
|
|
357
|
+
target: () => authorsCollection,
|
|
358
|
+
cardinality: "one",
|
|
359
|
+
localKey: "author_id"
|
|
360
|
+
}
|
|
361
|
+
]
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const result = await generateSchema([authorsCollection, profilesCollection]);
|
|
365
|
+
const cleanResult = cleanSchema(result);
|
|
366
|
+
|
|
367
|
+
// Should create FK on profiles table
|
|
368
|
+
expect(cleanResult).toContain(`author_id: varchar("author_id").references(() => authors.id, { onDelete: "set null" })`);
|
|
369
|
+
|
|
370
|
+
// Should create owning relation on profiles
|
|
371
|
+
expect(cleanResult).toContain(`export const profilesRelations = drizzleRelations(profiles, ({ one, many }) => ({ author: one(authors, { fields: [profiles.author_id], references: [authors.id], relationName: "author" }) }));`);
|
|
372
|
+
|
|
373
|
+
// Should create inverse relation on authors (this was previously missing)
|
|
374
|
+
expect(cleanResult).toContain(`export const authorsRelations = drizzleRelations(authors, ({ one, many }) => ({ profile: one(profiles, { fields: [authors.id], references: [profiles.author_id], relationName: "profile" }) }));`);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
it("should generate owning one-to-many relations", async () => {
|
|
378
|
+
const categoriesCollection: EntityCollection = {
|
|
379
|
+
slug: "categories",
|
|
380
|
+
table: "categories",
|
|
381
|
+
name: "Categories",
|
|
382
|
+
properties: {
|
|
383
|
+
name: { type: "string" },
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
const postsCollection: EntityCollection = {
|
|
388
|
+
slug: "posts",
|
|
389
|
+
table: "posts",
|
|
390
|
+
name: "Posts",
|
|
391
|
+
properties: {
|
|
392
|
+
title: { type: "string" },
|
|
393
|
+
category: { type: "relation", relationName: "category" }
|
|
394
|
+
},
|
|
395
|
+
relations: [
|
|
396
|
+
{
|
|
397
|
+
relationName: "category",
|
|
398
|
+
target: () => categoriesCollection,
|
|
399
|
+
cardinality: "one",
|
|
400
|
+
localKey: "category_id"
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const result = await generateSchema([categoriesCollection, postsCollection]);
|
|
406
|
+
const cleanResult = cleanSchema(result);
|
|
407
|
+
|
|
408
|
+
// Should create FK on posts table
|
|
409
|
+
expect(cleanResult).toContain(`category_id: varchar("category_id").references(() => categories.id, { onDelete: "set null" })`);
|
|
410
|
+
// Should create owning relation on posts
|
|
411
|
+
expect(cleanResult).toContain(`export const postsRelations = drizzleRelations(posts, ({ one, many }) => ({ category: one(categories, { fields: [posts.category_id], references: [categories.id], relationName: "category" }) }));`);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
describe("Mixed Relation Types", () => {
|
|
416
|
+
it("should handle collections with multiple relations", async () => {
|
|
417
|
+
const authorsCollection: EntityCollection = {
|
|
418
|
+
slug: "authors",
|
|
419
|
+
table: "authors",
|
|
420
|
+
name: "Authors",
|
|
421
|
+
properties: {
|
|
422
|
+
name: { type: "string" },
|
|
423
|
+
publisher: { type: "relation", relationName: "publisher" },
|
|
424
|
+
},
|
|
425
|
+
relations: [
|
|
426
|
+
{
|
|
427
|
+
relationName: "publisher",
|
|
428
|
+
target: () => publishersCollection,
|
|
429
|
+
cardinality: "one",
|
|
430
|
+
localKey: "publisher_id"
|
|
431
|
+
}
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const publishersCollection: EntityCollection = {
|
|
436
|
+
slug: "publishers",
|
|
437
|
+
table: "publishers",
|
|
438
|
+
name: "Publishers",
|
|
439
|
+
properties: {
|
|
440
|
+
name: { type: "string" }
|
|
441
|
+
}
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const booksCollection: EntityCollection = {
|
|
445
|
+
slug: "books",
|
|
446
|
+
table: "books",
|
|
447
|
+
name: "Books",
|
|
448
|
+
properties: {
|
|
449
|
+
title: { type: "string" },
|
|
450
|
+
author: { type: "relation", relationName: "author" }
|
|
451
|
+
},
|
|
452
|
+
relations: [{
|
|
453
|
+
relationName: "author",
|
|
454
|
+
target: () => authorsCollection,
|
|
455
|
+
cardinality: "one",
|
|
456
|
+
localKey: "author_id"
|
|
457
|
+
}]
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const result = await generateSchema([authorsCollection, publishersCollection, booksCollection]);
|
|
461
|
+
const cleanResult = cleanSchema(result);
|
|
462
|
+
|
|
463
|
+
// Check owning relation from author to publisher
|
|
464
|
+
expect(cleanResult).toContain(`publisher_id: varchar("publisher_id").references(() => publishers.id, { onDelete: "set null" })`);
|
|
465
|
+
expect(cleanResult).toContain(`publisher: one(publishers, { fields: [authors.publisher_id], references: [publishers.id], relationName: "publisher" })`);
|
|
466
|
+
|
|
467
|
+
// Check owning relation from book to author
|
|
468
|
+
expect(cleanResult).toContain(`author_id: varchar("author_id").references(() => authors.id, { onDelete: "set null" })`);
|
|
469
|
+
expect(cleanResult).toContain(`author: one(authors, { fields: [books.author_id], references: [authors.id], relationName: "author" })`);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
describe("Complex Multi-Column Relations", () => {
|
|
474
|
+
it("should handle multi-column foreign keys", async () => {
|
|
475
|
+
const ordersCollection: EntityCollection = {
|
|
476
|
+
slug: "orders",
|
|
477
|
+
table: "orders",
|
|
478
|
+
name: "Orders",
|
|
479
|
+
properties: {
|
|
480
|
+
customer_code: { type: "string" },
|
|
481
|
+
region_id: { type: "number", validation: { integer: true } },
|
|
482
|
+
customer: { type: "relation", relationName: "customer" }
|
|
483
|
+
},
|
|
484
|
+
relations: [
|
|
485
|
+
{
|
|
486
|
+
relationName: "customer",
|
|
487
|
+
target: () => customersCollection,
|
|
488
|
+
cardinality: "many",
|
|
489
|
+
joinPath: [
|
|
490
|
+
{ table: "customers", on: { from: ["customer_code", "region_id"], to: ["code", "region_id"] } }
|
|
491
|
+
]
|
|
492
|
+
}
|
|
493
|
+
]
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const customersCollection: EntityCollection = {
|
|
497
|
+
slug: "customers",
|
|
498
|
+
table: "customers",
|
|
499
|
+
name: "Customers",
|
|
500
|
+
properties: {
|
|
501
|
+
code: { type: "string" },
|
|
502
|
+
region_id: { type: "number", validation: { integer: true } },
|
|
503
|
+
name: { type: "string" }
|
|
504
|
+
}
|
|
505
|
+
};
|
|
506
|
+
|
|
507
|
+
const result = await generateSchema([ordersCollection, customersCollection]);
|
|
508
|
+
const cleanResult = cleanSchema(result);
|
|
509
|
+
|
|
510
|
+
// joinPath relations use existing user-controlled tables - no views generated
|
|
511
|
+
expect(cleanResult).not.toContain("view_orders_to_customers");
|
|
512
|
+
expect(cleanResult).not.toContain("viewOrdersToCustomers");
|
|
513
|
+
|
|
514
|
+
// Should generate basic table definitions
|
|
515
|
+
expect(cleanResult).toContain("export const orders = pgTable(\"orders\"");
|
|
516
|
+
expect(cleanResult).toContain("export const customers = pgTable(\"customers\"");
|
|
517
|
+
|
|
518
|
+
// Should include the multi-column properties in the tables
|
|
519
|
+
expect(cleanResult).toContain("customer_code: varchar(\"customer_code\")");
|
|
520
|
+
expect(cleanResult).toContain("region_id: integer(\"region_id\")");
|
|
521
|
+
expect(cleanResult).toContain("code: varchar(\"code\")");
|
|
522
|
+
|
|
523
|
+
// No Drizzle relations generated for joinPath relations
|
|
524
|
+
expect(cleanResult).not.toContain("ordersRelations");
|
|
525
|
+
|
|
526
|
+
// No SQL view generation
|
|
527
|
+
expect(result).not.toContain("CREATE OR REPLACE VIEW");
|
|
528
|
+
expect(result).not.toContain("SQL VIEWS FOR COMPLEX RELATIONS");
|
|
529
|
+
});
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
describe("Edge Cases and Production Scenarios", () => {
|
|
533
|
+
it("should handle self-referencing many-to-many", async () => {
|
|
534
|
+
const usersCollection: EntityCollection = {
|
|
535
|
+
slug: "users",
|
|
536
|
+
table: "users",
|
|
537
|
+
name: "Users",
|
|
538
|
+
properties: {
|
|
539
|
+
name: { type: "string" },
|
|
540
|
+
friends: { type: "relation", relationName: "friends" }
|
|
541
|
+
},
|
|
542
|
+
relations: [
|
|
543
|
+
{
|
|
544
|
+
relationName: "friends",
|
|
545
|
+
target: () => usersCollection,
|
|
546
|
+
cardinality: "many",
|
|
547
|
+
direction: "owning",
|
|
548
|
+
through: {
|
|
549
|
+
table: "user_friends",
|
|
550
|
+
sourceColumn: "user_id",
|
|
551
|
+
targetColumn: "friend_id"
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
]
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const result = await generateSchema([usersCollection]);
|
|
558
|
+
const cleanResult = cleanSchema(result);
|
|
559
|
+
|
|
560
|
+
// Should handle self-referencing relations
|
|
561
|
+
expect(cleanResult).toContain("export const userFriends = pgTable(\"user_friends\"");
|
|
562
|
+
expect(cleanResult).toContain("user_id: varchar(\"user_id\").notNull().references(() => users.id, { onDelete: \"cascade\" })");
|
|
563
|
+
expect(cleanResult).toContain("friend_id: varchar(\"friend_id\").notNull().references(() => users.id, { onDelete: \"cascade\" })");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it("should handle mixed ID types in relations", async () => {
|
|
567
|
+
const productsCollection: EntityCollection = {
|
|
568
|
+
slug: "products",
|
|
569
|
+
table: "products",
|
|
570
|
+
name: "Products",
|
|
571
|
+
properties: {
|
|
572
|
+
sku: { type: "string", isId: true },
|
|
573
|
+
name: { type: "string" },
|
|
574
|
+
categories: { type: "relation", relationName: "categories" }
|
|
575
|
+
},
|
|
576
|
+
relations: [
|
|
577
|
+
{
|
|
578
|
+
relationName: "categories",
|
|
579
|
+
target: () => categoriesCollection,
|
|
580
|
+
cardinality: "many",
|
|
581
|
+
direction: "owning",
|
|
582
|
+
through: {
|
|
583
|
+
table: "product_categories",
|
|
584
|
+
sourceColumn: "product_sku",
|
|
585
|
+
targetColumn: "category_id"
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
]
|
|
589
|
+
};
|
|
590
|
+
|
|
591
|
+
const categoriesCollection: EntityCollection = {
|
|
592
|
+
slug: "categories",
|
|
593
|
+
table: "categories",
|
|
594
|
+
name: "Categories",
|
|
595
|
+
properties: {
|
|
596
|
+
name: { type: "string" }
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
const result = await generateSchema([productsCollection, categoriesCollection]);
|
|
601
|
+
const cleanResult = cleanSchema(result);
|
|
602
|
+
|
|
603
|
+
// The primary key should be sku
|
|
604
|
+
expect(cleanResult).toContain("sku: varchar(\"sku\").primaryKey()");
|
|
605
|
+
expect(cleanResult).not.toContain("id: serial(\"id\").primaryKey()");
|
|
606
|
+
expect(cleanResult).toContain("product_sku: varchar(\"product_sku\").notNull().references(() => products.sku, { onDelete: \"cascade\" })");
|
|
607
|
+
expect(cleanResult).toContain("category_id: varchar(\"category_id\").notNull().references(() => categories.id, { onDelete: \"cascade\" })");
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
it("should handle circular references", async () => {
|
|
611
|
+
const aCollection: EntityCollection = {
|
|
612
|
+
slug: "a_entities",
|
|
613
|
+
table: "a_entities",
|
|
614
|
+
name: "A Entities",
|
|
615
|
+
properties: {
|
|
616
|
+
name: { type: "string" },
|
|
617
|
+
b_entities: { type: "relation", relationName: "b_entities" }
|
|
618
|
+
},
|
|
619
|
+
relations: [
|
|
620
|
+
{
|
|
621
|
+
relationName: "b_entities",
|
|
622
|
+
target: () => bCollection,
|
|
623
|
+
cardinality: "many",
|
|
624
|
+
direction: "inverse",
|
|
625
|
+
foreignKeyOnTarget: "a_entity_id"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
const bCollection: EntityCollection = {
|
|
631
|
+
slug: "b_entities",
|
|
632
|
+
table: "b_entities",
|
|
633
|
+
name: "B Entities",
|
|
634
|
+
properties: {
|
|
635
|
+
name: { type: "string" },
|
|
636
|
+
a_entity: { type: "relation", relationName: "a_entity" }
|
|
637
|
+
},
|
|
638
|
+
relations: [
|
|
639
|
+
{
|
|
640
|
+
relationName: "a_entity",
|
|
641
|
+
target: () => aCollection,
|
|
642
|
+
cardinality: "one",
|
|
643
|
+
direction: "owning",
|
|
644
|
+
localKey: "a_entity_id"
|
|
645
|
+
}
|
|
646
|
+
]
|
|
647
|
+
};
|
|
648
|
+
|
|
649
|
+
const result = await generateSchema([aCollection, bCollection]);
|
|
650
|
+
const cleanResult = cleanSchema(result);
|
|
651
|
+
|
|
652
|
+
// Should handle circular references without infinite loops
|
|
653
|
+
// The 'owning' relation on bCollection should correctly generate the FK
|
|
654
|
+
expect(cleanResult).toContain("export const aEntities = pgTable(\"a_entities\"");
|
|
655
|
+
expect(cleanResult).toContain("export const bEntities = pgTable(\"b_entities\"");
|
|
656
|
+
expect(cleanResult).toContain("a_entity_id: varchar(\"a_entity_id\").references(() => aEntities.id, { onDelete: \"set null\" })");
|
|
657
|
+
// Check that both drizzle relations are generated
|
|
658
|
+
expect(cleanResult).toContain("export const aEntitiesRelations = drizzleRelations(aEntities, ({ one, many }) => ({ b_entities: many(bEntities, { relationName: \"b_entities\" }) }));");
|
|
659
|
+
expect(cleanResult).toContain("export const bEntitiesRelations = drizzleRelations(bEntities, ({ one, many }) => ({ a_entity: one(aEntities, { fields: [bEntities.a_entity_id], references: [aEntities.id], relationName: \"a_entity\" }) }));");
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
});
|