@apso/cli 0.8.5 → 0.10.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +221 -1653
- package/dist/commands/config.d.ts +13 -0
- package/dist/commands/config.js +114 -0
- package/dist/commands/deploy.d.ts +11 -0
- package/dist/commands/deploy.js +141 -0
- package/dist/commands/dev.d.ts +15 -0
- package/dist/commands/dev.js +95 -0
- package/dist/commands/generate.d.ts +12 -0
- package/dist/commands/generate.js +179 -0
- package/dist/commands/init.d.ts +16 -0
- package/dist/commands/init.js +313 -0
- package/dist/commands/link.d.ts +11 -0
- package/dist/commands/link.js +139 -0
- package/dist/commands/login.d.ts +22 -0
- package/dist/commands/login.js +345 -0
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +38 -0
- package/dist/commands/logs.d.ts +10 -0
- package/dist/commands/logs.js +64 -0
- package/dist/commands/mcp/serve.d.ts +11 -0
- package/dist/commands/mcp/serve.js +818 -0
- package/dist/commands/migrate.d.ts +11 -0
- package/dist/commands/migrate.js +137 -0
- package/dist/commands/open.d.ts +10 -0
- package/dist/commands/open.js +79 -0
- package/dist/commands/projects.d.ts +10 -0
- package/dist/commands/projects.js +106 -0
- package/dist/commands/schema/diff.d.ts +7 -0
- package/dist/commands/schema/diff.js +63 -0
- package/dist/commands/schema/pull.d.ts +9 -0
- package/dist/commands/schema/pull.js +81 -0
- package/dist/commands/schema/push.d.ts +9 -0
- package/dist/commands/schema/push.js +97 -0
- package/dist/commands/schema/validate.d.ts +9 -0
- package/dist/commands/schema/validate.js +90 -0
- package/dist/commands/server/new.d.ts +2 -19
- package/dist/commands/server/new.js +6 -192
- package/dist/commands/server/scaffold.d.ts +2 -25
- package/dist/commands/server/scaffold.js +6 -312
- package/dist/commands/status.d.ts +7 -0
- package/dist/commands/status.js +53 -0
- package/dist/commands/unlink.d.ts +9 -0
- package/dist/commands/unlink.js +47 -0
- package/dist/commands/whoami.d.ts +10 -0
- package/dist/commands/whoami.js +97 -0
- package/dist/lib/api/client.js +1 -1
- package/dist/lib/api/services.js +3 -3
- package/dist/lib/api/types.d.ts +9 -2
- package/dist/lib/apsorc-parser.d.ts +0 -19
- package/dist/lib/apsorc-parser.js +2 -73
- package/dist/lib/guards.d.ts +3 -20
- package/dist/lib/guards.js +1 -113
- package/dist/lib/index.d.ts +1 -9
- package/dist/lib/index.js +1 -18
- package/dist/lib/migrate/entity-generator.d.ts +14 -0
- package/dist/lib/migrate/entity-generator.js +7 -0
- package/dist/lib/migrate/sandbox.d.ts +42 -0
- package/dist/lib/migrate/sandbox.js +337 -0
- package/dist/lib/migrate/snapshot.d.ts +80 -0
- package/dist/lib/migrate/snapshot.js +100 -0
- package/dist/lib/templates/entities/entity-col-enum.eta +2 -2
- package/dist/lib/templates/python/models/model-col-datetime.eta +1 -1
- package/dist/lib/templates/python/models/model-col-string.eta +1 -1
- package/dist/lib/templates/python/models/model-col-text.eta +1 -1
- package/dist/lib/templates/python/models/model-col-varchar.eta +1 -1
- package/dist/lib/utils/field.d.ts +12 -0
- package/dist/lib/utils/field.js +80 -1
- package/dist/lib/utils/schema-convert.d.ts +71 -0
- package/dist/lib/utils/schema-convert.js +169 -0
- package/dist/lib/utils/spinner.d.ts +19 -0
- package/dist/lib/utils/spinner.js +87 -0
- package/dist/lib/utils/template.d.ts +12 -0
- package/dist/lib/utils/template.js +49 -0
- package/npm-shrinkwrap.json +18118 -0
- package/oclif.manifest.json +547 -41
- package/package.json +26 -5
- package/dist/lib/controller.d.ts +0 -10
- package/dist/lib/controller.js +0 -99
- package/dist/lib/dto.d.ts +0 -16
- package/dist/lib/dto.js +0 -92
- package/dist/lib/entity.d.ts +0 -19
- package/dist/lib/entity.js +0 -56
- package/dist/lib/enums.d.ts +0 -6
- package/dist/lib/enums.js +0 -28
- package/dist/lib/gql-dto.d.ts +0 -3
- package/dist/lib/gql-dto.js +0 -39
- package/dist/lib/index-module.d.ts +0 -13
- package/dist/lib/index-module.js +0 -26
- package/dist/lib/module.d.ts +0 -13
- package/dist/lib/module.js +0 -42
- package/dist/lib/service.d.ts +0 -2
- package/dist/lib/service.js +0 -61
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Migration Sandbox
|
|
4
|
+
*
|
|
5
|
+
* The core migration testing engine. Uses PGlite (in-process Postgres) to:
|
|
6
|
+
* 1. Establish a baseline schema from the previous snapshot
|
|
7
|
+
* 2. Compute the diff against the current schema
|
|
8
|
+
* 3. Generate and test migration SQL locally
|
|
9
|
+
*
|
|
10
|
+
* This validates migrations before anything touches a remote database.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.resetSandbox = exports.applyMigration = exports.runMigrationSandbox = void 0;
|
|
14
|
+
const tslib_1 = require("tslib");
|
|
15
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
16
|
+
const path = tslib_1.__importStar(require("path"));
|
|
17
|
+
const paths_1 = require("../config/paths");
|
|
18
|
+
const snapshot_1 = require("./snapshot");
|
|
19
|
+
const field_1 = require("../utils/field");
|
|
20
|
+
const casing_1 = require("../utils/casing");
|
|
21
|
+
/**
|
|
22
|
+
* Get the sandbox data directory for PGlite persistence.
|
|
23
|
+
*/
|
|
24
|
+
function getSandboxDataDir(projectDir) {
|
|
25
|
+
return path.join((0, paths_1.getProjectConfigDir)(projectDir), "sandbox", "pgdata");
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Clear TypeORM's global metadata storage.
|
|
29
|
+
* Required between sandbox runs to prevent metadata from previous
|
|
30
|
+
* entity classes leaking into subsequent DataSource instances.
|
|
31
|
+
*/
|
|
32
|
+
function clearTypeOrmMetadata() {
|
|
33
|
+
const typeorm = require("typeorm");
|
|
34
|
+
const storage = typeorm.getMetadataArgsStorage();
|
|
35
|
+
for (const key of Object.keys(storage)) {
|
|
36
|
+
if (Array.isArray(storage[key])) {
|
|
37
|
+
storage[key].length = 0;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Reset the PGlite singleton so a fresh instance is created on next use.
|
|
43
|
+
*/
|
|
44
|
+
async function resetPGliteSingleton() {
|
|
45
|
+
try {
|
|
46
|
+
const { PGliteInstance } = require("typeorm-pglite/dist/pglite-instance");
|
|
47
|
+
await PGliteInstance.close();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// ignore -- singleton may not exist yet
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build TypeORM entity classes programmatically from parsed entity definitions.
|
|
55
|
+
* Uses TypeORM decorators applied at runtime, so no file I/O or compilation needed.
|
|
56
|
+
*
|
|
57
|
+
* Clears TypeORM's global metadata storage first to prevent cross-contamination
|
|
58
|
+
* between runs.
|
|
59
|
+
*/
|
|
60
|
+
function buildEntityClasses(entities) {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
clearTypeOrmMetadata();
|
|
63
|
+
const typeorm = require("typeorm");
|
|
64
|
+
const classes = [];
|
|
65
|
+
for (const entityDef of entities) {
|
|
66
|
+
const className = entityDef.name;
|
|
67
|
+
const tableName = (0, casing_1.snakeCase)(entityDef.name);
|
|
68
|
+
const EntityClass = { [className]: class {
|
|
69
|
+
} }[className];
|
|
70
|
+
typeorm.Entity(tableName)(EntityClass);
|
|
71
|
+
const hasCustomPk = (_b = (_a = entityDef.fields) === null || _a === void 0 ? void 0 : _a.some((f) => f.primary)) !== null && _b !== void 0 ? _b : false;
|
|
72
|
+
const pkType = entityDef.primaryKeyType || "serial";
|
|
73
|
+
if (!hasCustomPk) {
|
|
74
|
+
if (pkType === "uuid") {
|
|
75
|
+
typeorm.PrimaryColumn({ type: "uuid" })(EntityClass.prototype, "id");
|
|
76
|
+
}
|
|
77
|
+
else if (pkType === "text") {
|
|
78
|
+
typeorm.PrimaryColumn({ type: "text" })(EntityClass.prototype, "id");
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
typeorm.PrimaryGeneratedColumn()(EntityClass.prototype, "id");
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (entityDef.created_at) {
|
|
85
|
+
typeorm.CreateDateColumn()(EntityClass.prototype, "created_at");
|
|
86
|
+
}
|
|
87
|
+
if (entityDef.updated_at) {
|
|
88
|
+
typeorm.UpdateDateColumn()(EntityClass.prototype, "updated_at");
|
|
89
|
+
}
|
|
90
|
+
const fields = entityDef.fields || [];
|
|
91
|
+
for (const field of fields) {
|
|
92
|
+
if (!hasCustomPk && field.name === "id")
|
|
93
|
+
continue;
|
|
94
|
+
if (entityDef.created_at && field.name === "created_at")
|
|
95
|
+
continue;
|
|
96
|
+
if (entityDef.updated_at && field.name === "updated_at")
|
|
97
|
+
continue;
|
|
98
|
+
const normalizedType = (0, field_1.normalizeFieldType)(field.type);
|
|
99
|
+
const colType = field_1.fieldTypeToColumnType[normalizedType] || "varchar";
|
|
100
|
+
const colOpts = { type: colType };
|
|
101
|
+
if (field.nullable)
|
|
102
|
+
colOpts.nullable = true;
|
|
103
|
+
if (field.unique)
|
|
104
|
+
colOpts.unique = true;
|
|
105
|
+
if (field.default !== undefined && field.default !== null) {
|
|
106
|
+
colOpts.default = field.default;
|
|
107
|
+
}
|
|
108
|
+
if (field.type === "enum" && field.values) {
|
|
109
|
+
colOpts.enum = field.values;
|
|
110
|
+
}
|
|
111
|
+
if (field.length)
|
|
112
|
+
colOpts.length = field.length;
|
|
113
|
+
if (field.precision)
|
|
114
|
+
colOpts.precision = field.precision;
|
|
115
|
+
if (field.scale !== undefined)
|
|
116
|
+
colOpts.scale = field.scale;
|
|
117
|
+
if (field.primary) {
|
|
118
|
+
typeorm.PrimaryColumn(colOpts)(EntityClass.prototype, field.name);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
typeorm.Column(colOpts)(EntityClass.prototype, field.name);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
classes.push(EntityClass);
|
|
125
|
+
}
|
|
126
|
+
return classes;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Create a TypeORM DataSource configured for PGlite with in-memory entity classes.
|
|
130
|
+
*/
|
|
131
|
+
async function createPGliteDataSource(entityClasses) {
|
|
132
|
+
const { DataSource } = await Promise.resolve().then(() => tslib_1.__importStar(require("typeorm")));
|
|
133
|
+
let PGliteDriver;
|
|
134
|
+
try {
|
|
135
|
+
const pgliteModule = await Promise.resolve().then(() => tslib_1.__importStar(require("typeorm-pglite")));
|
|
136
|
+
PGliteDriver = pgliteModule.PGliteDriver;
|
|
137
|
+
}
|
|
138
|
+
catch (e) {
|
|
139
|
+
throw new Error("Migration sandbox requires typeorm-pglite and @electric-sql/pglite.\n" +
|
|
140
|
+
"Install them with: npm install typeorm @electric-sql/pglite typeorm-pglite\n" +
|
|
141
|
+
`Original error: ${e instanceof Error ? e.message : e}`);
|
|
142
|
+
}
|
|
143
|
+
// Create a driver. typeorm-pglite uses a singleton PGliteInstance
|
|
144
|
+
// so multiple DataSources share the same in-process database.
|
|
145
|
+
const _driver = new PGliteDriver();
|
|
146
|
+
const dataSource = new DataSource({
|
|
147
|
+
type: "postgres",
|
|
148
|
+
driver: _driver.driver,
|
|
149
|
+
entities: entityClasses,
|
|
150
|
+
synchronize: false,
|
|
151
|
+
logging: false,
|
|
152
|
+
});
|
|
153
|
+
return dataSource;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Run the migration sandbox: detect changes, generate SQL, test execution.
|
|
157
|
+
*
|
|
158
|
+
* Uses PGlite's singleton pattern: all DataSources within a single
|
|
159
|
+
* runMigrationSandbox call share the same in-process Postgres instance.
|
|
160
|
+
* We reset the singleton at the start of each run for isolation.
|
|
161
|
+
*/
|
|
162
|
+
async function runMigrationSandbox(current, projectDir) {
|
|
163
|
+
var _a;
|
|
164
|
+
const snapshot = (0, snapshot_1.readSnapshot)(projectDir);
|
|
165
|
+
// Build a comparable representation of current schema for snapshot
|
|
166
|
+
const currentSnapshot = {
|
|
167
|
+
entities: current.entities.map((e) => {
|
|
168
|
+
var _a;
|
|
169
|
+
return ({
|
|
170
|
+
name: e.name,
|
|
171
|
+
fields: (_a = e.fields) === null || _a === void 0 ? void 0 : _a.map((f) => {
|
|
172
|
+
var _a;
|
|
173
|
+
return ({
|
|
174
|
+
name: f.name,
|
|
175
|
+
type: f.type,
|
|
176
|
+
nullable: f.nullable,
|
|
177
|
+
unique: f.unique,
|
|
178
|
+
default: (_a = f.default) !== null && _a !== void 0 ? _a : undefined,
|
|
179
|
+
index: f.index,
|
|
180
|
+
});
|
|
181
|
+
}),
|
|
182
|
+
primaryKeyType: e.primaryKeyType,
|
|
183
|
+
created_at: e.created_at,
|
|
184
|
+
updated_at: e.updated_at,
|
|
185
|
+
scopeBy: e.scopeBy,
|
|
186
|
+
});
|
|
187
|
+
}),
|
|
188
|
+
};
|
|
189
|
+
// Quick check: has anything changed?
|
|
190
|
+
if (snapshot && !schemaChanged(currentSnapshot, snapshot)) {
|
|
191
|
+
return {
|
|
192
|
+
needed: false,
|
|
193
|
+
success: true,
|
|
194
|
+
upSql: [],
|
|
195
|
+
downSql: [],
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
// Reset PGlite singleton to get a fresh in-memory database
|
|
200
|
+
await resetPGliteSingleton();
|
|
201
|
+
// Build previous entities from snapshot
|
|
202
|
+
const previousEntities = ((_a = snapshot === null || snapshot === void 0 ? void 0 : snapshot.entities) === null || _a === void 0 ? void 0 : _a.length)
|
|
203
|
+
? snapshot.entities.map((se) => {
|
|
204
|
+
var _a;
|
|
205
|
+
return ({
|
|
206
|
+
name: se.name,
|
|
207
|
+
fields: (_a = se.fields) === null || _a === void 0 ? void 0 : _a.map((f) => ({
|
|
208
|
+
name: f.name,
|
|
209
|
+
type: f.type,
|
|
210
|
+
nullable: f.nullable,
|
|
211
|
+
unique: f.unique,
|
|
212
|
+
default: f.default,
|
|
213
|
+
index: f.index,
|
|
214
|
+
})),
|
|
215
|
+
primaryKeyType: se.primaryKeyType,
|
|
216
|
+
created_at: se.created_at,
|
|
217
|
+
updated_at: se.updated_at,
|
|
218
|
+
scopeBy: se.scopeBy,
|
|
219
|
+
});
|
|
220
|
+
})
|
|
221
|
+
: [];
|
|
222
|
+
// Step 1: Establish baseline with previous schema
|
|
223
|
+
if (previousEntities.length > 0) {
|
|
224
|
+
const prevClasses = buildEntityClasses(previousEntities);
|
|
225
|
+
const prevDs = await createPGliteDataSource(prevClasses);
|
|
226
|
+
await prevDs.initialize();
|
|
227
|
+
await prevDs.synchronize();
|
|
228
|
+
// Don't call prevDs.destroy() -- it closes the PGlite singleton.
|
|
229
|
+
// Just disconnect TypeORM's connection manager.
|
|
230
|
+
// The PGlite in-memory database persists in the singleton.
|
|
231
|
+
}
|
|
232
|
+
// Step 2: Compute diff with current schema
|
|
233
|
+
const currentClasses = buildEntityClasses(current.entities);
|
|
234
|
+
const currentDs = await createPGliteDataSource(currentClasses);
|
|
235
|
+
await currentDs.initialize();
|
|
236
|
+
try {
|
|
237
|
+
const sqlInMemory = await currentDs.driver.createSchemaBuilder().log();
|
|
238
|
+
const upQueries = sqlInMemory.upQueries || [];
|
|
239
|
+
const downQueries = sqlInMemory.downQueries || [];
|
|
240
|
+
const upSql = upQueries.map((q) => q.query);
|
|
241
|
+
const downSql = downQueries.map((q) => q.query);
|
|
242
|
+
if (upSql.length === 0) {
|
|
243
|
+
await resetPGliteSingleton();
|
|
244
|
+
return {
|
|
245
|
+
needed: false,
|
|
246
|
+
success: true,
|
|
247
|
+
upSql: [],
|
|
248
|
+
downSql: [],
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// Step 3: Execute the migration to verify it works
|
|
252
|
+
await currentDs.synchronize();
|
|
253
|
+
await resetPGliteSingleton();
|
|
254
|
+
return {
|
|
255
|
+
needed: true,
|
|
256
|
+
success: true,
|
|
257
|
+
upSql,
|
|
258
|
+
downSql,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
catch (execError) {
|
|
262
|
+
await resetPGliteSingleton();
|
|
263
|
+
return {
|
|
264
|
+
needed: true,
|
|
265
|
+
success: false,
|
|
266
|
+
upSql: [],
|
|
267
|
+
downSql: [],
|
|
268
|
+
error: execError instanceof Error
|
|
269
|
+
? execError.message
|
|
270
|
+
: String(execError),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
catch (outerError) {
|
|
275
|
+
try {
|
|
276
|
+
await resetPGliteSingleton();
|
|
277
|
+
}
|
|
278
|
+
catch { /* ignore */ }
|
|
279
|
+
return {
|
|
280
|
+
needed: true,
|
|
281
|
+
success: false,
|
|
282
|
+
upSql: [],
|
|
283
|
+
downSql: [],
|
|
284
|
+
error: outerError instanceof Error
|
|
285
|
+
? outerError.message
|
|
286
|
+
: String(outerError),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
exports.runMigrationSandbox = runMigrationSandbox;
|
|
291
|
+
/**
|
|
292
|
+
* Apply a successful migration: update the snapshot to current state.
|
|
293
|
+
*/
|
|
294
|
+
function applyMigration(current, projectDir) {
|
|
295
|
+
const snapshot = {
|
|
296
|
+
entities: current.entities.map((e) => {
|
|
297
|
+
var _a;
|
|
298
|
+
return ({
|
|
299
|
+
name: e.name,
|
|
300
|
+
fields: (_a = e.fields) === null || _a === void 0 ? void 0 : _a.map((f) => {
|
|
301
|
+
var _a;
|
|
302
|
+
return ({
|
|
303
|
+
name: f.name,
|
|
304
|
+
type: f.type,
|
|
305
|
+
nullable: f.nullable,
|
|
306
|
+
unique: f.unique,
|
|
307
|
+
default: (_a = f.default) !== null && _a !== void 0 ? _a : undefined,
|
|
308
|
+
index: f.index,
|
|
309
|
+
});
|
|
310
|
+
}),
|
|
311
|
+
primaryKeyType: e.primaryKeyType,
|
|
312
|
+
created_at: e.created_at,
|
|
313
|
+
updated_at: e.updated_at,
|
|
314
|
+
scopeBy: e.scopeBy,
|
|
315
|
+
});
|
|
316
|
+
}),
|
|
317
|
+
};
|
|
318
|
+
(0, snapshot_1.writeSnapshot)(snapshot, projectDir);
|
|
319
|
+
}
|
|
320
|
+
exports.applyMigration = applyMigration;
|
|
321
|
+
/**
|
|
322
|
+
* Reset the sandbox: remove snapshot and PGlite data.
|
|
323
|
+
*/
|
|
324
|
+
function resetSandbox(projectDir) {
|
|
325
|
+
(0, snapshot_1.resetSnapshot)(projectDir);
|
|
326
|
+
const dataDir = getSandboxDataDir(projectDir);
|
|
327
|
+
if (fs.existsSync(dataDir)) {
|
|
328
|
+
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
exports.resetSandbox = resetSandbox;
|
|
332
|
+
/**
|
|
333
|
+
* Simple deep comparison between current and snapshot schemas.
|
|
334
|
+
*/
|
|
335
|
+
function schemaChanged(current, snapshot) {
|
|
336
|
+
return (JSON.stringify(current.entities) !== JSON.stringify(snapshot.entities));
|
|
337
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Snapshot Manager
|
|
3
|
+
*
|
|
4
|
+
* Manages the schema snapshot at .apso/sandbox/schema-snapshot.json.
|
|
5
|
+
* The snapshot stores the .apsorc content at the time of last successful migration.
|
|
6
|
+
* Comparing current .apsorc against the snapshot detects whether a migration is needed.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Shape of the stored snapshot (mirrors the parsed .apsorc structure)
|
|
10
|
+
*/
|
|
11
|
+
export interface ApsorcSnapshot {
|
|
12
|
+
entities: Array<{
|
|
13
|
+
name: string;
|
|
14
|
+
fields?: Array<{
|
|
15
|
+
name: string;
|
|
16
|
+
type: string;
|
|
17
|
+
nullable?: boolean;
|
|
18
|
+
unique?: boolean;
|
|
19
|
+
default?: unknown;
|
|
20
|
+
index?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
primaryKeyType?: string;
|
|
23
|
+
created_at?: boolean;
|
|
24
|
+
updated_at?: boolean;
|
|
25
|
+
scopeBy?: string | string[];
|
|
26
|
+
}>;
|
|
27
|
+
relationships?: Array<{
|
|
28
|
+
from: string;
|
|
29
|
+
to: string;
|
|
30
|
+
type: string;
|
|
31
|
+
to_name?: string;
|
|
32
|
+
nullable?: boolean;
|
|
33
|
+
bi_directional?: boolean;
|
|
34
|
+
cascadeDelete?: boolean;
|
|
35
|
+
}>;
|
|
36
|
+
version?: number;
|
|
37
|
+
rootFolder?: string;
|
|
38
|
+
apiType?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Subset of ParsedApsorc we use for comparison.
|
|
42
|
+
* Avoids importing the full ParsedApsorc type which carries runtime-only data (relationshipMap).
|
|
43
|
+
*/
|
|
44
|
+
export interface ComparableSchema {
|
|
45
|
+
entities: Array<{
|
|
46
|
+
name: string;
|
|
47
|
+
fields?: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
type: string;
|
|
50
|
+
nullable?: boolean;
|
|
51
|
+
unique?: boolean;
|
|
52
|
+
default?: unknown;
|
|
53
|
+
index?: boolean;
|
|
54
|
+
}>;
|
|
55
|
+
primaryKeyType?: string;
|
|
56
|
+
created_at?: boolean;
|
|
57
|
+
updated_at?: boolean;
|
|
58
|
+
scopeBy?: string | string[];
|
|
59
|
+
}>;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read the saved schema snapshot, or null if none exists.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readSnapshot(projectDir?: string): ApsorcSnapshot | null;
|
|
65
|
+
/**
|
|
66
|
+
* Write a schema snapshot to disk.
|
|
67
|
+
*/
|
|
68
|
+
export declare function writeSnapshot(schema: ApsorcSnapshot, projectDir?: string): void;
|
|
69
|
+
/**
|
|
70
|
+
* Delete the snapshot and sandbox directory contents (for --reset).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resetSnapshot(projectDir?: string): void;
|
|
73
|
+
/**
|
|
74
|
+
* Detect whether the current schema differs from the stored snapshot.
|
|
75
|
+
*
|
|
76
|
+
* Compares entity names, field definitions, and relationships using
|
|
77
|
+
* deterministic JSON serialization. Returns true if any change is detected,
|
|
78
|
+
* or if no snapshot exists (first run).
|
|
79
|
+
*/
|
|
80
|
+
export declare function hasChanges(current: ComparableSchema, snapshot: ApsorcSnapshot | null): boolean;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Schema Snapshot Manager
|
|
4
|
+
*
|
|
5
|
+
* Manages the schema snapshot at .apso/sandbox/schema-snapshot.json.
|
|
6
|
+
* The snapshot stores the .apsorc content at the time of last successful migration.
|
|
7
|
+
* Comparing current .apsorc against the snapshot detects whether a migration is needed.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.hasChanges = exports.resetSnapshot = exports.writeSnapshot = exports.readSnapshot = void 0;
|
|
11
|
+
const tslib_1 = require("tslib");
|
|
12
|
+
const fs = tslib_1.__importStar(require("fs"));
|
|
13
|
+
const path = tslib_1.__importStar(require("path"));
|
|
14
|
+
const paths_1 = require("../config/paths");
|
|
15
|
+
function getSandboxDir(projectDir) {
|
|
16
|
+
return path.join((0, paths_1.getProjectConfigDir)(projectDir), "sandbox");
|
|
17
|
+
}
|
|
18
|
+
function getSnapshotPath(projectDir) {
|
|
19
|
+
return path.join(getSandboxDir(projectDir), "schema-snapshot.json");
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Read the saved schema snapshot, or null if none exists.
|
|
23
|
+
*/
|
|
24
|
+
function readSnapshot(projectDir) {
|
|
25
|
+
const snapshotPath = getSnapshotPath(projectDir);
|
|
26
|
+
if (!fs.existsSync(snapshotPath)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
const content = fs.readFileSync(snapshotPath, "utf8");
|
|
31
|
+
return JSON.parse(content);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.readSnapshot = readSnapshot;
|
|
38
|
+
/**
|
|
39
|
+
* Write a schema snapshot to disk.
|
|
40
|
+
*/
|
|
41
|
+
function writeSnapshot(schema, projectDir) {
|
|
42
|
+
const sandboxDir = getSandboxDir(projectDir);
|
|
43
|
+
(0, paths_1.ensureDir)(sandboxDir);
|
|
44
|
+
const snapshotPath = getSnapshotPath(projectDir);
|
|
45
|
+
fs.writeFileSync(snapshotPath, JSON.stringify(schema, null, 2), "utf-8");
|
|
46
|
+
}
|
|
47
|
+
exports.writeSnapshot = writeSnapshot;
|
|
48
|
+
/**
|
|
49
|
+
* Delete the snapshot and sandbox directory contents (for --reset).
|
|
50
|
+
*/
|
|
51
|
+
function resetSnapshot(projectDir) {
|
|
52
|
+
const sandboxDir = getSandboxDir(projectDir);
|
|
53
|
+
if (fs.existsSync(sandboxDir)) {
|
|
54
|
+
fs.rmSync(sandboxDir, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
exports.resetSnapshot = resetSnapshot;
|
|
58
|
+
/**
|
|
59
|
+
* Detect whether the current schema differs from the stored snapshot.
|
|
60
|
+
*
|
|
61
|
+
* Compares entity names, field definitions, and relationships using
|
|
62
|
+
* deterministic JSON serialization. Returns true if any change is detected,
|
|
63
|
+
* or if no snapshot exists (first run).
|
|
64
|
+
*/
|
|
65
|
+
function hasChanges(current, snapshot) {
|
|
66
|
+
if (!snapshot) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
const normalize = (obj) => JSON.stringify(obj);
|
|
70
|
+
// Compare entity count
|
|
71
|
+
if (current.entities.length !== snapshot.entities.length) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Build maps for comparison
|
|
75
|
+
const currentMap = new Map(current.entities.map((e) => [e.name, e]));
|
|
76
|
+
const snapshotMap = new Map(snapshot.entities.map((e) => [e.name, e]));
|
|
77
|
+
// Check for added or removed entities
|
|
78
|
+
for (const name of currentMap.keys()) {
|
|
79
|
+
if (!snapshotMap.has(name)) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const name of snapshotMap.keys()) {
|
|
84
|
+
if (!currentMap.has(name)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Deep compare each entity
|
|
89
|
+
for (const [name, currentEntity] of currentMap) {
|
|
90
|
+
const snapshotEntity = snapshotMap.get(name);
|
|
91
|
+
if (!snapshotEntity) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
if (normalize(currentEntity) !== normalize(snapshotEntity)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
exports.hasChanges = hasChanges;
|
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
@Column({
|
|
4
4
|
type: 'enum',
|
|
5
5
|
enum: enums.<%= field.dataType%>,
|
|
6
|
-
default: enums.<%= field.dataType%>.<%= field.default %>
|
|
6
|
+
default: enums.<%= field.dataType%>.<%= field.default.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/^(\d)/, '_$1').replace(/_+/g, '_').replace(/_$/, '') %>
|
|
7
7
|
})
|
|
8
8
|
<% if (field.index ) {%>
|
|
9
9
|
@Index()
|
|
10
10
|
<% } %>
|
|
11
11
|
|
|
12
|
-
<%= field.name %>!: enums.<%= field.dataType%>;
|
|
12
|
+
<%= field.name %>!: enums.<%= field.dataType%>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%= it.field.name %>: Mapped[<% if (it.field.nullable !== false) { %>Optional[datetime]<% } else { %>datetime<% } %>] = mapped_column(
|
|
2
2
|
DateTime,
|
|
3
3
|
nullable=<%= it.field.nullable === false ? 'False' : 'True' %><% if (it.field.default !== undefined && it.field.default !== null) { %>,
|
|
4
|
-
default
|
|
4
|
+
default=<%= it.field.default === null ? 'None' : `"${it.field.default}"` %><% } %>
|
|
5
5
|
)
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
String(<%= it.field.length || 255 %>),
|
|
3
3
|
nullable=<%= it.field.nullable === false ? 'False' : 'True' %><% if (it.field.unique) { %>,
|
|
4
4
|
unique=True<% } %><% if (it.field.default !== undefined && it.field.default !== null) { %>,
|
|
5
|
-
default
|
|
5
|
+
default="<%= it.field.default %>"<% } %>
|
|
6
6
|
)
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
Text,
|
|
3
3
|
nullable=<%= it.field.nullable === false ? 'False' : 'True' %><% if (it.field.unique) { %>,
|
|
4
4
|
unique=True<% } %><% if (it.field.default !== undefined && it.field.default !== null) { %>,
|
|
5
|
-
default
|
|
5
|
+
default="<%= it.field.default %>"<% } %>
|
|
6
6
|
)
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
String(<%= it.field.length || 255 %>),
|
|
3
3
|
nullable=<%= it.field.nullable === false ? 'False' : 'True' %><% if (it.field.unique) { %>,
|
|
4
4
|
unique=True<% } %><% if (it.field.default !== undefined && it.field.default !== null) { %>,
|
|
5
|
-
default
|
|
5
|
+
default="<%= it.field.default %>"<% } %>
|
|
6
6
|
)
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
import { FieldType, Field, ComputedField, Entity } from "../types";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical mapping from .apsorc field types to Postgres column types.
|
|
4
|
+
* This is the single source of truth -- sandbox, entity-generator, and
|
|
5
|
+
* templates should all derive from this map.
|
|
6
|
+
*/
|
|
7
|
+
export declare const fieldTypeToColumnType: Record<string, string>;
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a field type to its template-compatible name.
|
|
10
|
+
* PostgreSQL has some types with spaces (e.g., "double precision")
|
|
11
|
+
* that need to be mapped to simpler names for template lookup.
|
|
12
|
+
*/
|
|
13
|
+
export declare const normalizeFieldType: (type: string) => string;
|
|
2
14
|
export declare const getJsTypeFromFieldType: (type: FieldType | string) => string;
|
|
3
15
|
export declare const getDefaultValueForField: (field: Field) => string | null;
|
|
4
16
|
export declare const getFieldForTemplate: (fields: Field[], entityName: string) => ComputedField[];
|
package/dist/lib/utils/field.js
CHANGED
|
@@ -1,7 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.validateDecimalField = exports.typeExistsInEntity = exports.fieldToEnumType = exports.getFieldForTemplate = exports.getDefaultValueForField = exports.getJsTypeFromFieldType = void 0;
|
|
3
|
+
exports.validateDecimalField = exports.typeExistsInEntity = exports.fieldToEnumType = exports.getFieldForTemplate = exports.getDefaultValueForField = exports.getJsTypeFromFieldType = exports.normalizeFieldType = exports.fieldTypeToColumnType = void 0;
|
|
4
4
|
const casing_1 = require("./casing");
|
|
5
|
+
/**
|
|
6
|
+
* Canonical mapping from .apsorc field types to Postgres column types.
|
|
7
|
+
* This is the single source of truth -- sandbox, entity-generator, and
|
|
8
|
+
* templates should all derive from this map.
|
|
9
|
+
*/
|
|
10
|
+
exports.fieldTypeToColumnType = {
|
|
11
|
+
text: "text",
|
|
12
|
+
string: "varchar",
|
|
13
|
+
varchar: "varchar",
|
|
14
|
+
char: "char",
|
|
15
|
+
boolean: "boolean",
|
|
16
|
+
integer: "integer",
|
|
17
|
+
int: "integer",
|
|
18
|
+
smallint: "smallint",
|
|
19
|
+
bigint: "bigint",
|
|
20
|
+
float: "float",
|
|
21
|
+
real: "real",
|
|
22
|
+
double: "double precision",
|
|
23
|
+
decimal: "decimal",
|
|
24
|
+
numeric: "numeric",
|
|
25
|
+
serial: "integer",
|
|
26
|
+
bigserial: "bigint",
|
|
27
|
+
smallserial: "smallint",
|
|
28
|
+
date: "date",
|
|
29
|
+
timestamp: "timestamp",
|
|
30
|
+
timestamptz: "timestamptz",
|
|
31
|
+
time: "time",
|
|
32
|
+
timetz: "timetz",
|
|
33
|
+
json: "json",
|
|
34
|
+
jsonb: "jsonb",
|
|
35
|
+
"json-plain": "json",
|
|
36
|
+
uuid: "uuid",
|
|
37
|
+
money: "money",
|
|
38
|
+
bytea: "bytea",
|
|
39
|
+
xml: "xml",
|
|
40
|
+
inet: "inet",
|
|
41
|
+
interval: "interval",
|
|
42
|
+
tsvector: "tsvector",
|
|
43
|
+
enum: "enum",
|
|
44
|
+
array: "text",
|
|
45
|
+
point: "point",
|
|
46
|
+
geometry: "geometry",
|
|
47
|
+
geography: "geography",
|
|
48
|
+
linestring: "geometry",
|
|
49
|
+
polygon: "geometry",
|
|
50
|
+
multipoint: "geometry",
|
|
51
|
+
multilinestring: "geometry",
|
|
52
|
+
multipolygon: "geometry",
|
|
53
|
+
geometrycollection: "geometry",
|
|
54
|
+
int4range: "int4range",
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Normalize PostgreSQL field types that have spaces or variants to their
|
|
58
|
+
* template-compatible names. This ensures field types like "double precision"
|
|
59
|
+
* map to the corresponding template file (entity-col-double.eta).
|
|
60
|
+
*/
|
|
61
|
+
const fieldTypeNormalizationMap = {
|
|
62
|
+
"double precision": "double",
|
|
63
|
+
"character varying": "varchar",
|
|
64
|
+
"bit varying": "varbit",
|
|
65
|
+
"time without time zone": "time",
|
|
66
|
+
"time with time zone": "timetz",
|
|
67
|
+
"timestamp without time zone": "timestamp",
|
|
68
|
+
"timestamp with time zone": "timestamptz",
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Normalize a field type to its template-compatible name.
|
|
72
|
+
* PostgreSQL has some types with spaces (e.g., "double precision")
|
|
73
|
+
* that need to be mapped to simpler names for template lookup.
|
|
74
|
+
*/
|
|
75
|
+
const normalizeFieldType = (type) => {
|
|
76
|
+
return fieldTypeNormalizationMap[type.toLowerCase()] || type;
|
|
77
|
+
};
|
|
78
|
+
exports.normalizeFieldType = normalizeFieldType;
|
|
5
79
|
const typeToJsType = {
|
|
6
80
|
array: "string[]",
|
|
7
81
|
boolean: "boolean",
|
|
@@ -9,6 +83,7 @@ const typeToJsType = {
|
|
|
9
83
|
float: "number",
|
|
10
84
|
real: "number",
|
|
11
85
|
double: "number",
|
|
86
|
+
"double precision": "number",
|
|
12
87
|
decimal: "number",
|
|
13
88
|
numeric: "number",
|
|
14
89
|
smallint: "number",
|
|
@@ -82,8 +157,12 @@ exports.getDefaultValueForField = getDefaultValueForField;
|
|
|
82
157
|
const getFieldForTemplate = (fields, entityName) => fields.map((field) => {
|
|
83
158
|
// Validate decimal/numeric fields
|
|
84
159
|
(0, exports.validateDecimalField)(field);
|
|
160
|
+
// Normalize field type for template lookup (e.g., "double precision" -> "double")
|
|
161
|
+
const normalizedType = (0, exports.normalizeFieldType)(field.type);
|
|
85
162
|
return {
|
|
86
163
|
...field,
|
|
164
|
+
// Use normalized type for template lookup
|
|
165
|
+
type: normalizedType,
|
|
87
166
|
default: (0, exports.getDefaultValueForField)(field),
|
|
88
167
|
dataType: field.type === "enum"
|
|
89
168
|
? (0, exports.fieldToEnumType)(field.name, entityName)
|