@rebasepro/server-postgresql 0.0.1-canary.09e5ec5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/LICENSE +6 -0
  2. package/README.md +106 -0
  3. package/build-errors.txt +37 -0
  4. package/dist/common/src/collections/CollectionRegistry.d.ts +56 -0
  5. package/dist/common/src/collections/index.d.ts +1 -0
  6. package/dist/common/src/data/buildRebaseData.d.ts +14 -0
  7. package/dist/common/src/index.d.ts +3 -0
  8. package/dist/common/src/util/builders.d.ts +57 -0
  9. package/dist/common/src/util/callbacks.d.ts +6 -0
  10. package/dist/common/src/util/collections.d.ts +11 -0
  11. package/dist/common/src/util/common.d.ts +2 -0
  12. package/dist/common/src/util/conditions.d.ts +26 -0
  13. package/dist/common/src/util/entities.d.ts +58 -0
  14. package/dist/common/src/util/enums.d.ts +3 -0
  15. package/dist/common/src/util/index.d.ts +16 -0
  16. package/dist/common/src/util/navigation_from_path.d.ts +34 -0
  17. package/dist/common/src/util/navigation_utils.d.ts +20 -0
  18. package/dist/common/src/util/parent_references_from_path.d.ts +6 -0
  19. package/dist/common/src/util/paths.d.ts +14 -0
  20. package/dist/common/src/util/permissions.d.ts +5 -0
  21. package/dist/common/src/util/references.d.ts +2 -0
  22. package/dist/common/src/util/relations.d.ts +22 -0
  23. package/dist/common/src/util/resolutions.d.ts +72 -0
  24. package/dist/common/src/util/storage.d.ts +24 -0
  25. package/dist/index.es.js +11298 -0
  26. package/dist/index.es.js.map +1 -0
  27. package/dist/index.umd.js +11306 -0
  28. package/dist/index.umd.js.map +1 -0
  29. package/dist/server-postgresql/src/PostgresBackendDriver.d.ts +100 -0
  30. package/dist/server-postgresql/src/PostgresBootstrapper.d.ts +40 -0
  31. package/dist/server-postgresql/src/auth/ensure-tables.d.ts +6 -0
  32. package/dist/server-postgresql/src/auth/services.d.ts +192 -0
  33. package/dist/server-postgresql/src/cli.d.ts +1 -0
  34. package/dist/server-postgresql/src/collections/PostgresCollectionRegistry.d.ts +43 -0
  35. package/dist/server-postgresql/src/connection.d.ts +40 -0
  36. package/dist/server-postgresql/src/data-transformer.d.ts +58 -0
  37. package/dist/server-postgresql/src/databasePoolManager.d.ts +20 -0
  38. package/dist/server-postgresql/src/history/HistoryService.d.ts +71 -0
  39. package/dist/server-postgresql/src/history/ensure-history-table.d.ts +7 -0
  40. package/dist/server-postgresql/src/index.d.ts +13 -0
  41. package/dist/server-postgresql/src/interfaces.d.ts +18 -0
  42. package/dist/server-postgresql/src/schema/auth-schema.d.ts +868 -0
  43. package/dist/server-postgresql/src/schema/doctor-cli.d.ts +2 -0
  44. package/dist/server-postgresql/src/schema/doctor.d.ts +43 -0
  45. package/dist/server-postgresql/src/schema/generate-drizzle-schema-logic.d.ts +2 -0
  46. package/dist/server-postgresql/src/schema/generate-drizzle-schema.d.ts +1 -0
  47. package/dist/server-postgresql/src/schema/introspect-db-logic.d.ts +82 -0
  48. package/dist/server-postgresql/src/schema/introspect-db.d.ts +1 -0
  49. package/dist/server-postgresql/src/schema/test-schema.d.ts +24 -0
  50. package/dist/server-postgresql/src/services/BranchService.d.ts +47 -0
  51. package/dist/server-postgresql/src/services/EntityFetchService.d.ts +209 -0
  52. package/dist/server-postgresql/src/services/EntityPersistService.d.ts +41 -0
  53. package/dist/server-postgresql/src/services/RelationService.d.ts +98 -0
  54. package/dist/server-postgresql/src/services/entity-helpers.d.ts +38 -0
  55. package/dist/server-postgresql/src/services/entityService.d.ts +104 -0
  56. package/dist/server-postgresql/src/services/index.d.ts +4 -0
  57. package/dist/server-postgresql/src/services/realtimeService.d.ts +188 -0
  58. package/dist/server-postgresql/src/utils/drizzle-conditions.d.ts +116 -0
  59. package/dist/server-postgresql/src/websocket.d.ts +5 -0
  60. package/dist/types/src/controllers/analytics_controller.d.ts +7 -0
  61. package/dist/types/src/controllers/auth.d.ts +119 -0
  62. package/dist/types/src/controllers/client.d.ts +170 -0
  63. package/dist/types/src/controllers/collection_registry.d.ts +45 -0
  64. package/dist/types/src/controllers/customization_controller.d.ts +60 -0
  65. package/dist/types/src/controllers/data.d.ts +168 -0
  66. package/dist/types/src/controllers/data_driver.d.ts +160 -0
  67. package/dist/types/src/controllers/database_admin.d.ts +11 -0
  68. package/dist/types/src/controllers/dialogs_controller.d.ts +36 -0
  69. package/dist/types/src/controllers/effective_role.d.ts +4 -0
  70. package/dist/types/src/controllers/email.d.ts +34 -0
  71. package/dist/types/src/controllers/index.d.ts +18 -0
  72. package/dist/types/src/controllers/local_config_persistence.d.ts +20 -0
  73. package/dist/types/src/controllers/navigation.d.ts +213 -0
  74. package/dist/types/src/controllers/registry.d.ts +54 -0
  75. package/dist/types/src/controllers/side_dialogs_controller.d.ts +67 -0
  76. package/dist/types/src/controllers/side_entity_controller.d.ts +90 -0
  77. package/dist/types/src/controllers/snackbar.d.ts +24 -0
  78. package/dist/types/src/controllers/storage.d.ts +171 -0
  79. package/dist/types/src/index.d.ts +4 -0
  80. package/dist/types/src/rebase_context.d.ts +105 -0
  81. package/dist/types/src/types/backend.d.ts +536 -0
  82. package/dist/types/src/types/builders.d.ts +15 -0
  83. package/dist/types/src/types/chips.d.ts +5 -0
  84. package/dist/types/src/types/collections.d.ts +856 -0
  85. package/dist/types/src/types/cron.d.ts +102 -0
  86. package/dist/types/src/types/data_source.d.ts +64 -0
  87. package/dist/types/src/types/entities.d.ts +145 -0
  88. package/dist/types/src/types/entity_actions.d.ts +98 -0
  89. package/dist/types/src/types/entity_callbacks.d.ts +173 -0
  90. package/dist/types/src/types/entity_link_builder.d.ts +7 -0
  91. package/dist/types/src/types/entity_overrides.d.ts +10 -0
  92. package/dist/types/src/types/entity_views.d.ts +61 -0
  93. package/dist/types/src/types/export_import.d.ts +21 -0
  94. package/dist/types/src/types/index.d.ts +23 -0
  95. package/dist/types/src/types/locales.d.ts +4 -0
  96. package/dist/types/src/types/modify_collections.d.ts +5 -0
  97. package/dist/types/src/types/plugins.d.ts +279 -0
  98. package/dist/types/src/types/properties.d.ts +1176 -0
  99. package/dist/types/src/types/property_config.d.ts +70 -0
  100. package/dist/types/src/types/relations.d.ts +336 -0
  101. package/dist/types/src/types/slots.d.ts +252 -0
  102. package/dist/types/src/types/translations.d.ts +870 -0
  103. package/dist/types/src/types/user_management_delegate.d.ts +121 -0
  104. package/dist/types/src/types/websockets.d.ts +78 -0
  105. package/dist/types/src/users/index.d.ts +2 -0
  106. package/dist/types/src/users/roles.d.ts +22 -0
  107. package/dist/types/src/users/user.d.ts +46 -0
  108. package/drizzle-test/0000_woozy_junta.sql +6 -0
  109. package/drizzle-test/0001_youthful_arachne.sql +1 -0
  110. package/drizzle-test/0002_lively_dragon_lord.sql +2 -0
  111. package/drizzle-test/0003_mean_king_cobra.sql +2 -0
  112. package/drizzle-test/meta/0000_snapshot.json +47 -0
  113. package/drizzle-test/meta/0001_snapshot.json +48 -0
  114. package/drizzle-test/meta/0002_snapshot.json +38 -0
  115. package/drizzle-test/meta/0003_snapshot.json +48 -0
  116. package/drizzle-test/meta/_journal.json +34 -0
  117. package/drizzle-test-out/0000_tan_trauma.sql +6 -0
  118. package/drizzle-test-out/0001_rapid_drax.sql +1 -0
  119. package/drizzle-test-out/meta/0000_snapshot.json +44 -0
  120. package/drizzle-test-out/meta/0001_snapshot.json +54 -0
  121. package/drizzle-test-out/meta/_journal.json +20 -0
  122. package/drizzle.test.config.ts +10 -0
  123. package/jest-all.log +3128 -0
  124. package/jest.log +49 -0
  125. package/package.json +92 -0
  126. package/scratch.ts +41 -0
  127. package/src/PostgresBackendDriver.ts +1008 -0
  128. package/src/PostgresBootstrapper.ts +231 -0
  129. package/src/auth/ensure-tables.ts +381 -0
  130. package/src/auth/services.ts +799 -0
  131. package/src/cli.ts +648 -0
  132. package/src/collections/PostgresCollectionRegistry.ts +96 -0
  133. package/src/connection.ts +84 -0
  134. package/src/data-transformer.ts +608 -0
  135. package/src/databasePoolManager.ts +85 -0
  136. package/src/history/HistoryService.ts +248 -0
  137. package/src/history/ensure-history-table.ts +45 -0
  138. package/src/index.ts +13 -0
  139. package/src/interfaces.ts +60 -0
  140. package/src/schema/auth-schema.ts +169 -0
  141. package/src/schema/doctor-cli.ts +47 -0
  142. package/src/schema/doctor.ts +595 -0
  143. package/src/schema/generate-drizzle-schema-logic.ts +765 -0
  144. package/src/schema/generate-drizzle-schema.ts +151 -0
  145. package/src/schema/introspect-db-logic.ts +542 -0
  146. package/src/schema/introspect-db.ts +211 -0
  147. package/src/schema/test-schema.ts +11 -0
  148. package/src/services/BranchService.ts +237 -0
  149. package/src/services/EntityFetchService.ts +1576 -0
  150. package/src/services/EntityPersistService.ts +349 -0
  151. package/src/services/RelationService.ts +1274 -0
  152. package/src/services/entity-helpers.ts +147 -0
  153. package/src/services/entityService.ts +211 -0
  154. package/src/services/index.ts +13 -0
  155. package/src/services/realtimeService.ts +1034 -0
  156. package/src/utils/drizzle-conditions.ts +1000 -0
  157. package/src/websocket.ts +518 -0
  158. package/test/auth-services.test.ts +661 -0
  159. package/test/batch-many-to-many-regression.test.ts +573 -0
  160. package/test/branchService.test.ts +367 -0
  161. package/test/data-transformer-hardening.test.ts +417 -0
  162. package/test/data-transformer.test.ts +175 -0
  163. package/test/doctor.test.ts +182 -0
  164. package/test/drizzle-conditions.test.ts +895 -0
  165. package/test/entityService.errors.test.ts +367 -0
  166. package/test/entityService.relations.test.ts +1008 -0
  167. package/test/entityService.subcollection-search.test.ts +566 -0
  168. package/test/entityService.test.ts +1035 -0
  169. package/test/generate-drizzle-schema.test.ts +988 -0
  170. package/test/historyService.test.ts +141 -0
  171. package/test/introspect-db-generation.test.ts +436 -0
  172. package/test/introspect-db-utils.test.ts +389 -0
  173. package/test/n-plus-one-regression.test.ts +314 -0
  174. package/test/postgresDataDriver.test.ts +648 -0
  175. package/test/realtimeService.test.ts +307 -0
  176. package/test/relation-pipeline-gaps.test.ts +637 -0
  177. package/test/relations.test.ts +1115 -0
  178. package/test/unmapped-tables-safety.test.ts +345 -0
  179. package/test-drizzle-bug.ts +18 -0
  180. package/test-drizzle-out/0000_cultured_freak.sql +7 -0
  181. package/test-drizzle-out/0001_tiresome_professor_monster.sql +1 -0
  182. package/test-drizzle-out/meta/0000_snapshot.json +55 -0
  183. package/test-drizzle-out/meta/0001_snapshot.json +63 -0
  184. package/test-drizzle-out/meta/_journal.json +20 -0
  185. package/test-drizzle-prompt.sh +2 -0
  186. package/test-policy-prompt.sh +3 -0
  187. package/test-programmatic.ts +30 -0
  188. package/test-programmatic2.ts +59 -0
  189. package/test-schema-no-policies.ts +12 -0
  190. package/test_drizzle_mock.js +3 -0
  191. package/test_find_changed.mjs +32 -0
  192. package/test_hash.js +14 -0
  193. package/test_output.txt +3145 -0
  194. package/tsconfig.json +49 -0
  195. package/tsconfig.prod.json +20 -0
  196. package/vite.config.ts +82 -0
@@ -0,0 +1,367 @@
1
+ import { BranchService } from "../src/services/BranchService";
2
+ import { DatabasePoolManager } from "../src/databasePoolManager";
3
+ import { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /** Create a minimal mock DrizzleClient with a configurable `execute` spy. */
10
+ function createMockDb() {
11
+ return {
12
+ execute: jest.fn().mockResolvedValue({ rows: [] })
13
+ } as unknown as jest.Mocked<NodePgDatabase>;
14
+ }
15
+
16
+ /** Create a minimal mock DatabasePoolManager. */
17
+ function createMockPoolManager(defaultDbName = "my_app_db") {
18
+ return {
19
+ defaultDatabaseName: defaultDbName,
20
+ disconnectDatabase: jest.fn().mockResolvedValue(undefined),
21
+ getDrizzle: jest.fn(),
22
+ getPool: jest.fn(),
23
+ hasPool: jest.fn(),
24
+ shutdown: jest.fn().mockResolvedValue(undefined)
25
+ } as unknown as jest.Mocked<DatabasePoolManager>;
26
+ }
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Tests
30
+ // ---------------------------------------------------------------------------
31
+
32
+ describe("BranchService", () => {
33
+ let db: jest.Mocked<NodePgDatabase>;
34
+ let poolManager: jest.Mocked<DatabasePoolManager>;
35
+ let service: BranchService;
36
+
37
+ beforeEach(() => {
38
+ db = createMockDb();
39
+ poolManager = createMockPoolManager();
40
+ service = new BranchService(db, poolManager);
41
+ });
42
+
43
+ afterEach(() => {
44
+ jest.restoreAllMocks();
45
+ });
46
+
47
+ // -----------------------------------------------------------------------
48
+ // ensureBranchMetadataTable
49
+ // -----------------------------------------------------------------------
50
+ describe("ensureBranchMetadataTable", () => {
51
+ it("should execute CREATE SCHEMA and CREATE TABLE statements", async () => {
52
+ await service.ensureBranchMetadataTable();
53
+
54
+ // Two calls: one for the schema, one for the table
55
+ expect(db.execute).toHaveBeenCalledTimes(2);
56
+
57
+ const firstArg = (db.execute as jest.Mock).mock.calls[0][0];
58
+ expect(firstArg).toBeDefined();
59
+
60
+ const secondArg = (db.execute as jest.Mock).mock.calls[1][0];
61
+ expect(secondArg).toBeDefined();
62
+ });
63
+
64
+ it("should be idempotent (safe to call multiple times)", async () => {
65
+ await service.ensureBranchMetadataTable();
66
+ await service.ensureBranchMetadataTable();
67
+
68
+ // Each call issues 2 executes → total 4
69
+ expect(db.execute).toHaveBeenCalledTimes(4);
70
+ });
71
+ });
72
+
73
+ // -----------------------------------------------------------------------
74
+ // createBranch
75
+ // -----------------------------------------------------------------------
76
+ describe("createBranch", () => {
77
+ it("should create a branch database and record metadata", async () => {
78
+ // No existing branch
79
+ db.execute
80
+ .mockResolvedValueOnce({ rows: [] } as never) // existence check
81
+ .mockResolvedValueOnce(undefined as never) // disconnectDatabase (noop)
82
+ .mockResolvedValueOnce(undefined as never) // CREATE DATABASE
83
+ .mockResolvedValueOnce(undefined as never); // INSERT metadata
84
+
85
+ const result = await service.createBranch("staging");
86
+
87
+ expect(result.name).toBe("staging");
88
+ expect(result.parentDatabase).toBe("my_app_db");
89
+ expect(result.createdAt).toBeInstanceOf(Date);
90
+
91
+ // poolManager.disconnectDatabase should be called with the source db
92
+ expect(poolManager.disconnectDatabase).toHaveBeenCalledWith("my_app_db");
93
+
94
+ // Should have 4 execute calls: existence-check, disconnect (on poolManager), CREATE DB, INSERT
95
+ // The disconnect is on poolManager, not db, so db.execute has 3 calls
96
+ expect(db.execute).toHaveBeenCalledTimes(3);
97
+ });
98
+
99
+ it("should use a custom source database when provided", async () => {
100
+ db.execute
101
+ .mockResolvedValueOnce({ rows: [] } as never)
102
+ .mockResolvedValueOnce(undefined as never)
103
+ .mockResolvedValueOnce(undefined as never);
104
+
105
+ const result = await service.createBranch("preview", { source: "production_db" });
106
+
107
+ expect(result.parentDatabase).toBe("production_db");
108
+ expect(poolManager.disconnectDatabase).toHaveBeenCalledWith("production_db");
109
+ });
110
+
111
+ it("should sanitize the branch name — stripping special characters", async () => {
112
+ db.execute
113
+ .mockResolvedValueOnce({ rows: [] } as never)
114
+ .mockResolvedValueOnce(undefined as never)
115
+ .mockResolvedValueOnce(undefined as never);
116
+
117
+ const result = await service.createBranch("my-feature/branch!@#");
118
+
119
+ expect(result.name).toBe("myfeaturebranch");
120
+ });
121
+
122
+ it("should throw when the branch already exists in metadata", async () => {
123
+ db.execute.mockResolvedValueOnce({
124
+ rows: [{ name: "staging" }]
125
+ } as never);
126
+
127
+ await expect(service.createBranch("staging")).rejects.toThrow(
128
+ 'Branch "staging" already exists.'
129
+ );
130
+
131
+ // Should not attempt to create a DB
132
+ expect(poolManager.disconnectDatabase).not.toHaveBeenCalled();
133
+ });
134
+
135
+ it("should throw a helpful error when CREATE DATABASE fails due to existing DB", async () => {
136
+ db.execute
137
+ .mockResolvedValueOnce({ rows: [] } as never) // existence check
138
+ .mockRejectedValueOnce(new Error('database "rb_staging" already exists')); // CREATE DB fails
139
+
140
+ await expect(service.createBranch("staging")).rejects.toThrow(
141
+ 'Database "rb_staging" already exists on the server'
142
+ );
143
+ });
144
+
145
+ it("should throw a helpful error when CREATE DATABASE fails due to active connections", async () => {
146
+ db.execute
147
+ .mockResolvedValueOnce({ rows: [] } as never) // existence check
148
+ .mockRejectedValueOnce(new Error("source database is being accessed by other users"));
149
+
150
+ await expect(service.createBranch("staging")).rejects.toThrow(
151
+ "Cannot create branch"
152
+ );
153
+ });
154
+
155
+ it("should re-throw unknown CREATE DATABASE errors", async () => {
156
+ const unknownError = new Error("disk full");
157
+ db.execute
158
+ .mockResolvedValueOnce({ rows: [] } as never)
159
+ .mockRejectedValueOnce(unknownError);
160
+
161
+ await expect(service.createBranch("staging")).rejects.toThrow("disk full");
162
+ });
163
+
164
+ it("should throw when branch name is entirely special characters", async () => {
165
+ await expect(service.createBranch("---!!!")).rejects.toThrow(
166
+ "Branch name must contain at least one alphanumeric character"
167
+ );
168
+ });
169
+ });
170
+
171
+ // -----------------------------------------------------------------------
172
+ // deleteBranch
173
+ // -----------------------------------------------------------------------
174
+ describe("deleteBranch", () => {
175
+ it("should delete the branch database and remove metadata", async () => {
176
+ db.execute
177
+ .mockResolvedValueOnce({ rows: [{ db_name: "rb_staging" }] } as never) // existence check
178
+ .mockResolvedValueOnce(undefined as never) // DROP DATABASE
179
+ .mockResolvedValueOnce(undefined as never); // DELETE metadata
180
+
181
+ await service.deleteBranch("staging");
182
+
183
+ expect(poolManager.disconnectDatabase).toHaveBeenCalledWith("rb_staging");
184
+ // 3 execute calls: SELECT, DROP, DELETE
185
+ expect(db.execute).toHaveBeenCalledTimes(3);
186
+ });
187
+
188
+ it("should throw when trying to delete the main database", async () => {
189
+ // The branch name, after prefix, would need to match defaultDatabaseName.
190
+ // Use a pool manager where defaultDatabaseName = "rb_main"
191
+ const pm = createMockPoolManager("rb_main");
192
+ const svc = new BranchService(db, pm);
193
+
194
+ await expect(svc.deleteBranch("main")).rejects.toThrow(
195
+ "Cannot delete the main database"
196
+ );
197
+
198
+ // Should not query metadata at all
199
+ expect(db.execute).not.toHaveBeenCalled();
200
+ });
201
+
202
+ it("should throw when the branch is not found in metadata", async () => {
203
+ db.execute.mockResolvedValueOnce({ rows: [] } as never);
204
+
205
+ await expect(service.deleteBranch("nonexistent")).rejects.toThrow(
206
+ 'Branch "nonexistent" not found.'
207
+ );
208
+ });
209
+
210
+ it("should throw a helpful error when DROP DATABASE fails due to active connections", async () => {
211
+ db.execute
212
+ .mockResolvedValueOnce({ rows: [{ db_name: "rb_staging" }] } as never) // existence check
213
+ .mockRejectedValueOnce(new Error("database is being accessed by other users")); // DROP fails
214
+
215
+ await expect(service.deleteBranch("staging")).rejects.toThrow(
216
+ 'Cannot delete branch "staging"'
217
+ );
218
+ });
219
+
220
+ it("should re-throw unknown DROP DATABASE errors", async () => {
221
+ db.execute
222
+ .mockResolvedValueOnce({ rows: [{ db_name: "rb_staging" }] } as never)
223
+ .mockRejectedValueOnce(new Error("permission denied"));
224
+
225
+ await expect(service.deleteBranch("staging")).rejects.toThrow("permission denied");
226
+ });
227
+ });
228
+
229
+ // -----------------------------------------------------------------------
230
+ // listBranches
231
+ // -----------------------------------------------------------------------
232
+ describe("listBranches", () => {
233
+ it("should return an empty array when no branches exist", async () => {
234
+ db.execute.mockResolvedValueOnce({ rows: [] } as never);
235
+
236
+ const result = await service.listBranches();
237
+
238
+ expect(result).toEqual([]);
239
+ });
240
+
241
+ it("should map database rows to BranchInfo objects", async () => {
242
+ const now = new Date().toISOString();
243
+ db.execute.mockResolvedValueOnce({
244
+ rows: [
245
+ { name: "staging",
246
+ parent_db: "my_app_db",
247
+ created_at: now,
248
+ size_bytes: 1048576 },
249
+ { name: "preview",
250
+ parent_db: "my_app_db",
251
+ created_at: now,
252
+ size_bytes: null }
253
+ ]
254
+ } as never);
255
+
256
+ const result = await service.listBranches();
257
+
258
+ expect(result).toHaveLength(2);
259
+
260
+ expect(result[0].name).toBe("staging");
261
+ expect(result[0].parentDatabase).toBe("my_app_db");
262
+ expect(result[0].createdAt).toBeInstanceOf(Date);
263
+ expect(result[0].sizeBytes).toBe(1048576);
264
+
265
+ expect(result[1].name).toBe("preview");
266
+ expect(result[1].sizeBytes).toBeUndefined();
267
+ });
268
+ });
269
+
270
+ // -----------------------------------------------------------------------
271
+ // getBranchInfo
272
+ // -----------------------------------------------------------------------
273
+ describe("getBranchInfo", () => {
274
+ it("should return branch info when found", async () => {
275
+ const now = new Date().toISOString();
276
+ db.execute
277
+ .mockResolvedValueOnce({
278
+ rows: [{ name: "staging",
279
+ parent_db: "my_app_db",
280
+ created_at: now }]
281
+ } as never)
282
+ .mockResolvedValueOnce({
283
+ rows: [{ size_bytes: 2097152 }]
284
+ } as never);
285
+
286
+ const result = await service.getBranchInfo("staging");
287
+
288
+ expect(result).toBeDefined();
289
+ expect(result!.name).toBe("staging");
290
+ expect(result!.parentDatabase).toBe("my_app_db");
291
+ expect(result!.sizeBytes).toBe(2097152);
292
+ expect(result!.createdAt).toBeInstanceOf(Date);
293
+ });
294
+
295
+ it("should return undefined when branch is not found", async () => {
296
+ db.execute.mockResolvedValueOnce({ rows: [] } as never);
297
+
298
+ const result = await service.getBranchInfo("nonexistent");
299
+
300
+ expect(result).toBeUndefined();
301
+ });
302
+
303
+ it("should gracefully handle size-fetch failure (externally dropped DB)", async () => {
304
+ const now = new Date().toISOString();
305
+ db.execute
306
+ .mockResolvedValueOnce({
307
+ rows: [{ name: "staging",
308
+ parent_db: "my_app_db",
309
+ created_at: now }]
310
+ } as never)
311
+ .mockRejectedValueOnce(new Error("database does not exist")); // size query fails
312
+
313
+ const result = await service.getBranchInfo("staging");
314
+
315
+ expect(result).toBeDefined();
316
+ expect(result!.name).toBe("staging");
317
+ expect(result!.sizeBytes).toBeUndefined();
318
+ });
319
+
320
+ it("should sanitize the branch name input", async () => {
321
+ db.execute.mockResolvedValueOnce({ rows: [] } as never);
322
+
323
+ await service.getBranchInfo("my-branch!");
324
+
325
+ // Should still call execute (with sanitized name)
326
+ expect(db.execute).toHaveBeenCalledTimes(1);
327
+ });
328
+ });
329
+
330
+ // -----------------------------------------------------------------------
331
+ // Name sanitization edge cases (exercised through public API)
332
+ // -----------------------------------------------------------------------
333
+ describe("branch name sanitization", () => {
334
+ it("should preserve underscores", async () => {
335
+ db.execute
336
+ .mockResolvedValueOnce({ rows: [] } as never)
337
+ .mockResolvedValueOnce(undefined as never)
338
+ .mockResolvedValueOnce(undefined as never);
339
+
340
+ const result = await service.createBranch("feature_auth_v2");
341
+
342
+ expect(result.name).toBe("feature_auth_v2");
343
+ });
344
+
345
+ it("should preserve mixed-case alphanumerics", async () => {
346
+ db.execute
347
+ .mockResolvedValueOnce({ rows: [] } as never)
348
+ .mockResolvedValueOnce(undefined as never)
349
+ .mockResolvedValueOnce(undefined as never);
350
+
351
+ const result = await service.createBranch("MyBranch123");
352
+
353
+ expect(result.name).toBe("MyBranch123");
354
+ });
355
+
356
+ it("should strip spaces, hyphens, dots, and other special chars", async () => {
357
+ db.execute
358
+ .mockResolvedValueOnce({ rows: [] } as never)
359
+ .mockResolvedValueOnce(undefined as never)
360
+ .mockResolvedValueOnce(undefined as never);
361
+
362
+ const result = await service.createBranch("my branch.v2-rc1");
363
+
364
+ expect(result.name).toBe("mybranchv2rc1");
365
+ });
366
+ });
367
+ });