@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,357 @@
|
|
|
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", parent_db: "my_app_db", created_at: now, size_bytes: 1048576 },
|
|
246
|
+
{ name: "preview", parent_db: "my_app_db", created_at: now, size_bytes: null },
|
|
247
|
+
],
|
|
248
|
+
} as never);
|
|
249
|
+
|
|
250
|
+
const result = await service.listBranches();
|
|
251
|
+
|
|
252
|
+
expect(result).toHaveLength(2);
|
|
253
|
+
|
|
254
|
+
expect(result[0].name).toBe("staging");
|
|
255
|
+
expect(result[0].parentDatabase).toBe("my_app_db");
|
|
256
|
+
expect(result[0].createdAt).toBeInstanceOf(Date);
|
|
257
|
+
expect(result[0].sizeBytes).toBe(1048576);
|
|
258
|
+
|
|
259
|
+
expect(result[1].name).toBe("preview");
|
|
260
|
+
expect(result[1].sizeBytes).toBeUndefined();
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// -----------------------------------------------------------------------
|
|
265
|
+
// getBranchInfo
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
describe("getBranchInfo", () => {
|
|
268
|
+
it("should return branch info when found", async () => {
|
|
269
|
+
const now = new Date().toISOString();
|
|
270
|
+
db.execute
|
|
271
|
+
.mockResolvedValueOnce({
|
|
272
|
+
rows: [{ name: "staging", parent_db: "my_app_db", created_at: now }],
|
|
273
|
+
} as never)
|
|
274
|
+
.mockResolvedValueOnce({
|
|
275
|
+
rows: [{ size_bytes: 2097152 }],
|
|
276
|
+
} as never);
|
|
277
|
+
|
|
278
|
+
const result = await service.getBranchInfo("staging");
|
|
279
|
+
|
|
280
|
+
expect(result).toBeDefined();
|
|
281
|
+
expect(result!.name).toBe("staging");
|
|
282
|
+
expect(result!.parentDatabase).toBe("my_app_db");
|
|
283
|
+
expect(result!.sizeBytes).toBe(2097152);
|
|
284
|
+
expect(result!.createdAt).toBeInstanceOf(Date);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("should return undefined when branch is not found", async () => {
|
|
288
|
+
db.execute.mockResolvedValueOnce({ rows: [] } as never);
|
|
289
|
+
|
|
290
|
+
const result = await service.getBranchInfo("nonexistent");
|
|
291
|
+
|
|
292
|
+
expect(result).toBeUndefined();
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("should gracefully handle size-fetch failure (externally dropped DB)", async () => {
|
|
296
|
+
const now = new Date().toISOString();
|
|
297
|
+
db.execute
|
|
298
|
+
.mockResolvedValueOnce({
|
|
299
|
+
rows: [{ name: "staging", parent_db: "my_app_db", created_at: now }],
|
|
300
|
+
} as never)
|
|
301
|
+
.mockRejectedValueOnce(new Error("database does not exist")); // size query fails
|
|
302
|
+
|
|
303
|
+
const result = await service.getBranchInfo("staging");
|
|
304
|
+
|
|
305
|
+
expect(result).toBeDefined();
|
|
306
|
+
expect(result!.name).toBe("staging");
|
|
307
|
+
expect(result!.sizeBytes).toBeUndefined();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("should sanitize the branch name input", async () => {
|
|
311
|
+
db.execute.mockResolvedValueOnce({ rows: [] } as never);
|
|
312
|
+
|
|
313
|
+
await service.getBranchInfo("my-branch!");
|
|
314
|
+
|
|
315
|
+
// Should still call execute (with sanitized name)
|
|
316
|
+
expect(db.execute).toHaveBeenCalledTimes(1);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// -----------------------------------------------------------------------
|
|
321
|
+
// Name sanitization edge cases (exercised through public API)
|
|
322
|
+
// -----------------------------------------------------------------------
|
|
323
|
+
describe("branch name sanitization", () => {
|
|
324
|
+
it("should preserve underscores", async () => {
|
|
325
|
+
db.execute
|
|
326
|
+
.mockResolvedValueOnce({ rows: [] } as never)
|
|
327
|
+
.mockResolvedValueOnce(undefined as never)
|
|
328
|
+
.mockResolvedValueOnce(undefined as never);
|
|
329
|
+
|
|
330
|
+
const result = await service.createBranch("feature_auth_v2");
|
|
331
|
+
|
|
332
|
+
expect(result.name).toBe("feature_auth_v2");
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should preserve mixed-case alphanumerics", async () => {
|
|
336
|
+
db.execute
|
|
337
|
+
.mockResolvedValueOnce({ rows: [] } as never)
|
|
338
|
+
.mockResolvedValueOnce(undefined as never)
|
|
339
|
+
.mockResolvedValueOnce(undefined as never);
|
|
340
|
+
|
|
341
|
+
const result = await service.createBranch("MyBranch123");
|
|
342
|
+
|
|
343
|
+
expect(result.name).toBe("MyBranch123");
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it("should strip spaces, hyphens, dots, and other special chars", async () => {
|
|
347
|
+
db.execute
|
|
348
|
+
.mockResolvedValueOnce({ rows: [] } as never)
|
|
349
|
+
.mockResolvedValueOnce(undefined as never)
|
|
350
|
+
.mockResolvedValueOnce(undefined as never);
|
|
351
|
+
|
|
352
|
+
const result = await service.createBranch("my branch.v2-rc1");
|
|
353
|
+
|
|
354
|
+
expect(result.name).toBe("mybranchv2rc1");
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
});
|