@apso/cli 0.14.0 → 0.15.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/dist/commands/config.js +3 -3
- package/dist/commands/dev.d.ts +55 -2
- package/dist/commands/dev.js +220 -32
- package/dist/commands/init.js +4 -14
- package/dist/commands/link.js +1 -1
- package/dist/commands/login.js +1 -1
- package/dist/commands/mcp/serve.js +8 -1
- package/dist/commands/migrate.js +1 -0
- package/dist/commands/projects.js +1 -1
- package/dist/commands/schema/diff.js +3 -3
- package/dist/lib/config/types.js +2 -2
- package/dist/lib/doctor/checks.js +2 -2
- package/dist/lib/generators/go.js +1 -4
- package/dist/lib/generators/python.js +1 -1
- package/dist/lib/migrate/sandbox.js +134 -9
- package/dist/lib/migrate/snapshot.d.ts +1 -7
- package/dist/lib/migrate/snapshot.js +5 -1
- package/dist/lib/templates/entities/entity-col-bigint.eta +1 -1
- package/dist/lib/templates/entities/entity-col-decimal.eta +1 -1
- package/dist/lib/templates/entities/entity-col-double.eta +1 -1
- package/dist/lib/templates/entities/entity-col-float.eta +1 -1
- package/dist/lib/templates/entities/entity-col-integer.eta +1 -1
- package/dist/lib/templates/entities/entity-col-money.eta +1 -1
- package/dist/lib/templates/entities/entity-col-numeric.eta +1 -1
- package/dist/lib/templates/entities/entity-col-real.eta +1 -1
- package/dist/lib/templates/entities/entity-col-smallint.eta +1 -1
- package/dist/lib/templates/go/utils/query.eta +37 -9
- package/dist/lib/templates/python/utils/query.eta +38 -10
- package/dist/lib/utils/schema-convert.js +1 -1
- package/dist/lib/utils/spinner.js +2 -1
- package/dist/lib/utils/template.js +1 -1
- package/npm-shrinkwrap.json +5 -4
- package/oclif.manifest.json +12 -7
- package/package.json +3 -2
package/dist/commands/config.js
CHANGED
|
@@ -13,11 +13,11 @@ const VALID_KEYS = [
|
|
|
13
13
|
"telemetryDisabled",
|
|
14
14
|
"defaultWorkspace",
|
|
15
15
|
];
|
|
16
|
-
const BOOLEAN_KEYS = [
|
|
16
|
+
const BOOLEAN_KEYS = new Set([
|
|
17
17
|
"verbose",
|
|
18
18
|
"noColor",
|
|
19
19
|
"telemetryDisabled",
|
|
20
|
-
];
|
|
20
|
+
]);
|
|
21
21
|
function parseBoolean(value) {
|
|
22
22
|
const lower = value.toLowerCase();
|
|
23
23
|
if (lower === "true" || lower === "1")
|
|
@@ -62,7 +62,7 @@ class Config extends base_command_1.default {
|
|
|
62
62
|
if (!VALID_KEYS.includes(key)) {
|
|
63
63
|
this.error(`Unknown key: ${key}\nValid keys: ${VALID_KEYS.join(", ")}`);
|
|
64
64
|
}
|
|
65
|
-
if (BOOLEAN_KEYS.
|
|
65
|
+
if (BOOLEAN_KEYS.has(key)) {
|
|
66
66
|
const boolVal = parseBoolean(value);
|
|
67
67
|
if (boolVal === null) {
|
|
68
68
|
this.error(`Invalid boolean value: ${value}. Use true/false or 1/0.`);
|
package/dist/commands/dev.d.ts
CHANGED
|
@@ -3,13 +3,66 @@ export default class Dev extends BaseCommand {
|
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static flags: {
|
|
6
|
+
docker: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
6
7
|
detach: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
7
8
|
build: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
8
9
|
};
|
|
10
|
+
private children;
|
|
11
|
+
private cleanup;
|
|
9
12
|
run(): Promise<void>;
|
|
10
13
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
15
|
+
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
16
|
+
*/
|
|
17
|
+
private runTypeScript;
|
|
18
|
+
/**
|
|
19
|
+
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
20
|
+
*/
|
|
21
|
+
private runPython;
|
|
22
|
+
/**
|
|
23
|
+
* Go: if pglite, start the pglite-server sidecar first, then go run.
|
|
24
|
+
*/
|
|
25
|
+
private runGo;
|
|
26
|
+
/**
|
|
27
|
+
* Fallback: run docker compose up (the old behavior).
|
|
28
|
+
*/
|
|
29
|
+
private runDocker;
|
|
30
|
+
/**
|
|
31
|
+
* Detect the project language from .apsorc.
|
|
32
|
+
* Falls back to typescript if not specified.
|
|
33
|
+
*/
|
|
34
|
+
private detectLanguage;
|
|
35
|
+
/**
|
|
36
|
+
* Read DATABASE_TYPE from .env. Defaults to pglite.
|
|
37
|
+
*/
|
|
38
|
+
private readDatabaseType;
|
|
39
|
+
/**
|
|
40
|
+
* Read a single value from .env.
|
|
41
|
+
*/
|
|
42
|
+
private readEnvValue;
|
|
43
|
+
/**
|
|
44
|
+
* Find .apsorc by walking up from cwd.
|
|
45
|
+
*/
|
|
46
|
+
private findApsorc;
|
|
47
|
+
/**
|
|
48
|
+
* Run `npm install` if node_modules doesn't exist.
|
|
49
|
+
* Needed for the pglite-server sidecar in Python/Go projects.
|
|
50
|
+
*/
|
|
51
|
+
private ensureNodeModules;
|
|
52
|
+
/**
|
|
53
|
+
* Wait for a TCP port to become reachable.
|
|
54
|
+
*/
|
|
55
|
+
private waitForPort;
|
|
56
|
+
/**
|
|
57
|
+
* Spawn a process in the background and track it for cleanup.
|
|
58
|
+
*/
|
|
59
|
+
private spawnBackground;
|
|
60
|
+
/**
|
|
61
|
+
* Spawn a process and wait for it to exit.
|
|
62
|
+
*/
|
|
63
|
+
private spawnAndWait;
|
|
64
|
+
/**
|
|
65
|
+
* Detect whether to use `docker compose` (v2) or `docker-compose` (v1).
|
|
13
66
|
*/
|
|
14
67
|
private detectComposeVariant;
|
|
15
68
|
}
|
package/dist/commands/dev.js
CHANGED
|
@@ -4,61 +4,247 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const child_process_1 = require("child_process");
|
|
6
6
|
const fs = tslib_1.__importStar(require("fs"));
|
|
7
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
8
|
const shelljs_1 = tslib_1.__importDefault(require("shelljs"));
|
|
8
9
|
const base_command_1 = tslib_1.__importDefault(require("../lib/base-command"));
|
|
9
10
|
class Dev extends base_command_1.default {
|
|
11
|
+
constructor() {
|
|
12
|
+
super(...arguments);
|
|
13
|
+
this.children = [];
|
|
14
|
+
this.cleanup = () => {
|
|
15
|
+
for (const child of this.children) {
|
|
16
|
+
child.kill("SIGTERM");
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
}
|
|
10
20
|
async run() {
|
|
11
21
|
const { flags } = await this.parse(Dev);
|
|
12
|
-
|
|
22
|
+
if (flags.docker) {
|
|
23
|
+
return this.runDocker(flags);
|
|
24
|
+
}
|
|
25
|
+
const language = this.detectLanguage();
|
|
26
|
+
const databaseType = this.readDatabaseType();
|
|
27
|
+
this.log(`Detected language: ${language}`);
|
|
28
|
+
this.log(`Database type: ${databaseType}`);
|
|
29
|
+
// Set up signal forwarding for clean shutdown
|
|
30
|
+
process.on("SIGINT", this.cleanup);
|
|
31
|
+
process.on("SIGTERM", this.cleanup);
|
|
32
|
+
try {
|
|
33
|
+
switch (language) {
|
|
34
|
+
case "typescript":
|
|
35
|
+
await this.runTypeScript();
|
|
36
|
+
break;
|
|
37
|
+
case "python":
|
|
38
|
+
await this.runPython(databaseType);
|
|
39
|
+
break;
|
|
40
|
+
case "go":
|
|
41
|
+
await this.runGo(databaseType);
|
|
42
|
+
break;
|
|
43
|
+
default:
|
|
44
|
+
this.error(`Unsupported language: ${language}. Supported: typescript, python, go.`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
process.removeListener("SIGINT", this.cleanup);
|
|
49
|
+
process.removeListener("SIGTERM", this.cleanup);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* TypeScript: run `npm run dev` which reads DATABASE_TYPE from .env.
|
|
54
|
+
* PGlite is handled natively by the NestJS app -- no sidecar needed.
|
|
55
|
+
*/
|
|
56
|
+
async runTypeScript() {
|
|
57
|
+
this.log("Starting TypeScript dev server...");
|
|
58
|
+
await this.spawnAndWait("npm", ["run", "dev"]);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Python: if pglite, start the pglite-server sidecar first, then uvicorn.
|
|
62
|
+
*/
|
|
63
|
+
async runPython(databaseType) {
|
|
64
|
+
if (databaseType === "pglite") {
|
|
65
|
+
this.log("Starting PGlite sidecar...");
|
|
66
|
+
await this.ensureNodeModules();
|
|
67
|
+
this.spawnBackground("npm", ["run", "db"]);
|
|
68
|
+
// Give the sidecar a moment to bind the port
|
|
69
|
+
await this.waitForPort(5432, 10000);
|
|
70
|
+
}
|
|
71
|
+
this.log("Starting Python dev server...");
|
|
72
|
+
await this.spawnAndWait("uvicorn", [
|
|
73
|
+
"app.main:app",
|
|
74
|
+
"--reload",
|
|
75
|
+
"--host",
|
|
76
|
+
"0.0.0.0",
|
|
77
|
+
"--port",
|
|
78
|
+
this.readEnvValue("PORT") || "3000",
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Go: if pglite, start the pglite-server sidecar first, then go run.
|
|
83
|
+
*/
|
|
84
|
+
async runGo(databaseType) {
|
|
85
|
+
if (databaseType === "pglite") {
|
|
86
|
+
this.log("Starting PGlite sidecar...");
|
|
87
|
+
await this.ensureNodeModules();
|
|
88
|
+
this.spawnBackground("npm", ["run", "db"]);
|
|
89
|
+
await this.waitForPort(5432, 10000);
|
|
90
|
+
}
|
|
91
|
+
this.log("Starting Go dev server...");
|
|
92
|
+
await this.spawnAndWait("go", ["run", "cmd/main.go"]);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Fallback: run docker compose up (the old behavior).
|
|
96
|
+
*/
|
|
97
|
+
async runDocker(flags) {
|
|
13
98
|
if (!shelljs_1.default.which("docker")) {
|
|
14
99
|
this.error("Docker is required but not found. Please install Docker and try again.\nhttps://docs.docker.com/get-docker/");
|
|
15
100
|
}
|
|
16
|
-
// Detect compose variant (v2 plugin vs v1 standalone)
|
|
17
101
|
const composeCmd = this.detectComposeVariant();
|
|
18
|
-
// Verify docker-compose.yml exists
|
|
19
102
|
if (!fs.existsSync("docker-compose.yml")) {
|
|
20
103
|
this.error("No docker-compose.yml found in the current directory.\nMake sure you are in an Apso project root.");
|
|
21
104
|
}
|
|
22
|
-
// Build the command args
|
|
23
105
|
const args = [...composeCmd, "up"];
|
|
24
|
-
if (flags.build)
|
|
106
|
+
if (flags.build)
|
|
25
107
|
args.push("--build");
|
|
26
|
-
|
|
27
|
-
if (flags.detach) {
|
|
108
|
+
if (flags.detach)
|
|
28
109
|
args.push("-d");
|
|
29
|
-
}
|
|
30
110
|
this.log(`Running: ${args.join(" ")}`);
|
|
31
|
-
|
|
111
|
+
await this.spawnAndWait(args[0], args.slice(1), {
|
|
112
|
+
AUTH_DISABLED: "true",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// --- Helpers ---
|
|
116
|
+
/**
|
|
117
|
+
* Detect the project language from .apsorc.
|
|
118
|
+
* Falls back to typescript if not specified.
|
|
119
|
+
*/
|
|
120
|
+
detectLanguage() {
|
|
121
|
+
const apsorcPath = this.findApsorc();
|
|
122
|
+
if (!apsorcPath) {
|
|
123
|
+
this.log("No .apsorc found, defaulting to typescript.");
|
|
124
|
+
return "typescript";
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer -- TS types JSON.parse as string-only
|
|
128
|
+
const content = fs.readFileSync(apsorcPath, "utf-8");
|
|
129
|
+
const config = JSON.parse(content);
|
|
130
|
+
return config.language || "typescript";
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
this.log("Could not parse .apsorc, defaulting to typescript.");
|
|
134
|
+
return "typescript";
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Read DATABASE_TYPE from .env. Defaults to pglite.
|
|
139
|
+
*/
|
|
140
|
+
readDatabaseType() {
|
|
141
|
+
return this.readEnvValue("DATABASE_TYPE") || "pglite";
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Read a single value from .env.
|
|
145
|
+
*/
|
|
146
|
+
readEnvValue(key) {
|
|
147
|
+
const envPath = path.resolve(".env");
|
|
148
|
+
if (!fs.existsSync(envPath))
|
|
149
|
+
return undefined;
|
|
150
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
151
|
+
for (const line of content.split("\n")) {
|
|
152
|
+
const trimmed = line.trim();
|
|
153
|
+
if (trimmed.startsWith("#") || !trimmed.includes("="))
|
|
154
|
+
continue;
|
|
155
|
+
const [k, ...rest] = trimmed.split("=");
|
|
156
|
+
if (k.trim() === key)
|
|
157
|
+
return rest.join("=").trim();
|
|
158
|
+
}
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Find .apsorc by walking up from cwd.
|
|
163
|
+
*/
|
|
164
|
+
findApsorc() {
|
|
165
|
+
let dir = process.cwd();
|
|
166
|
+
while (dir !== path.parse(dir).root) {
|
|
167
|
+
const candidate = path.join(dir, ".apsorc");
|
|
168
|
+
if (fs.existsSync(candidate))
|
|
169
|
+
return candidate;
|
|
170
|
+
dir = path.dirname(dir);
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Run `npm install` if node_modules doesn't exist.
|
|
176
|
+
* Needed for the pglite-server sidecar in Python/Go projects.
|
|
177
|
+
*/
|
|
178
|
+
async ensureNodeModules() {
|
|
179
|
+
if (!fs.existsSync("node_modules")) {
|
|
180
|
+
this.log("Installing pglite-server dependencies...");
|
|
181
|
+
await this.spawnAndWait("npm", ["install"]);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Wait for a TCP port to become reachable.
|
|
186
|
+
*/
|
|
187
|
+
waitForPort(port, timeoutMs) {
|
|
188
|
+
return new Promise((resolve, reject) => {
|
|
189
|
+
const start = Date.now();
|
|
190
|
+
const check = () => {
|
|
191
|
+
const net = require("net");
|
|
192
|
+
const sock = net.createConnection({ port, host: "127.0.0.1" }, () => {
|
|
193
|
+
sock.destroy();
|
|
194
|
+
resolve();
|
|
195
|
+
});
|
|
196
|
+
sock.on("error", () => {
|
|
197
|
+
sock.destroy();
|
|
198
|
+
if (Date.now() - start > timeoutMs) {
|
|
199
|
+
reject(new Error(`Port ${port} did not become available within ${timeoutMs}ms`));
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
setTimeout(check, 250);
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
};
|
|
206
|
+
check();
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Spawn a process in the background and track it for cleanup.
|
|
211
|
+
*/
|
|
212
|
+
spawnBackground(cmd, args) {
|
|
213
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
32
214
|
stdio: "inherit",
|
|
33
|
-
env: {
|
|
34
|
-
...process.env,
|
|
35
|
-
AUTH_DISABLED: "true",
|
|
36
|
-
},
|
|
215
|
+
env: { ...process.env },
|
|
37
216
|
});
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
|
|
217
|
+
this.children.push(child);
|
|
218
|
+
child.on("error", (err) => {
|
|
219
|
+
this.warn(`Background process ${cmd} failed: ${err.message}`);
|
|
220
|
+
});
|
|
221
|
+
return child;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Spawn a process and wait for it to exit.
|
|
225
|
+
*/
|
|
226
|
+
spawnAndWait(cmd, args, extraEnv) {
|
|
44
227
|
return new Promise((resolve, reject) => {
|
|
228
|
+
const child = (0, child_process_1.spawn)(cmd, args, {
|
|
229
|
+
stdio: "inherit",
|
|
230
|
+
env: { ...process.env, ...extraEnv },
|
|
231
|
+
});
|
|
232
|
+
this.children.push(child);
|
|
45
233
|
child.on("close", (code) => {
|
|
46
|
-
|
|
47
|
-
process.removeListener("SIGTERM", forwardSignal);
|
|
234
|
+
this.children = this.children.filter((c) => c !== child);
|
|
48
235
|
if (code !== 0 && code !== null) {
|
|
49
|
-
reject(new Error(
|
|
236
|
+
reject(new Error(`${cmd} exited with code ${code}`));
|
|
50
237
|
return;
|
|
51
238
|
}
|
|
52
239
|
resolve();
|
|
53
240
|
});
|
|
54
241
|
child.on("error", (err) => {
|
|
55
|
-
this.error(`Failed to start
|
|
242
|
+
this.error(`Failed to start ${cmd}: ${err.message}`);
|
|
56
243
|
});
|
|
57
244
|
});
|
|
58
245
|
}
|
|
59
246
|
/**
|
|
60
|
-
* Detect whether to use `docker compose` (v2
|
|
61
|
-
* Returns the command parts as an array.
|
|
247
|
+
* Detect whether to use `docker compose` (v2) or `docker-compose` (v1).
|
|
62
248
|
*/
|
|
63
249
|
detectComposeVariant() {
|
|
64
250
|
try {
|
|
@@ -66,7 +252,6 @@ class Dev extends base_command_1.default {
|
|
|
66
252
|
return ["docker", "compose"];
|
|
67
253
|
}
|
|
68
254
|
catch {
|
|
69
|
-
// Fall back to v1
|
|
70
255
|
if (shelljs_1.default.which("docker-compose")) {
|
|
71
256
|
return ["docker-compose"];
|
|
72
257
|
}
|
|
@@ -74,21 +259,24 @@ class Dev extends base_command_1.default {
|
|
|
74
259
|
}
|
|
75
260
|
}
|
|
76
261
|
}
|
|
77
|
-
Dev.description = "Start the local development server
|
|
262
|
+
Dev.description = "Start the local development server. Detects project language and database type from .apsorc and .env.";
|
|
78
263
|
Dev.examples = [
|
|
79
264
|
`$ apso dev`,
|
|
80
|
-
`$ apso dev --
|
|
81
|
-
`$ apso dev --
|
|
82
|
-
`$ apso dev --build --detach`,
|
|
265
|
+
`$ apso dev --docker`,
|
|
266
|
+
`$ apso dev --docker --build`,
|
|
83
267
|
];
|
|
84
268
|
Dev.flags = {
|
|
269
|
+
docker: core_1.Flags.boolean({
|
|
270
|
+
description: "Use Docker Compose instead of the native dev stack",
|
|
271
|
+
default: false,
|
|
272
|
+
}),
|
|
85
273
|
detach: core_1.Flags.boolean({
|
|
86
274
|
char: "d",
|
|
87
|
-
description: "Run containers in the background",
|
|
275
|
+
description: "Run containers in the background (Docker mode only)",
|
|
88
276
|
default: false,
|
|
89
277
|
}),
|
|
90
278
|
build: core_1.Flags.boolean({
|
|
91
|
-
description: "Rebuild images before starting",
|
|
279
|
+
description: "Rebuild images before starting (Docker mode only)",
|
|
92
280
|
default: false,
|
|
93
281
|
}),
|
|
94
282
|
};
|
package/dist/commands/init.js
CHANGED
|
@@ -24,12 +24,7 @@ class Init extends base_command_1.default {
|
|
|
24
24
|
this.error("An Apso project already exists in this directory. Run commands from a different directory or remove .apsorc / .apso/link.json first.");
|
|
25
25
|
}
|
|
26
26
|
const isAuthenticated = !flags["skip-platform"] && config_1.credentials.isValid();
|
|
27
|
-
|
|
28
|
-
await this.runAuthenticated(flags);
|
|
29
|
-
}
|
|
30
|
-
else {
|
|
31
|
-
await this.runOffline(flags);
|
|
32
|
-
}
|
|
27
|
+
await (isAuthenticated ? this.runAuthenticated(flags) : this.runOffline(flags));
|
|
33
28
|
}
|
|
34
29
|
async runAuthenticated(flags) {
|
|
35
30
|
// Ask: create new or clone existing
|
|
@@ -44,18 +39,13 @@ class Init extends base_command_1.default {
|
|
|
44
39
|
],
|
|
45
40
|
},
|
|
46
41
|
]);
|
|
47
|
-
|
|
48
|
-
await this.cloneExisting();
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
await this.createNew(flags);
|
|
52
|
-
}
|
|
42
|
+
await (action === "clone" ? this.cloneExisting() : this.createNew(flags));
|
|
53
43
|
}
|
|
54
44
|
async cloneExisting() {
|
|
55
45
|
// Select workspace
|
|
56
46
|
const workspaces = await services_1.workspacesApi.list();
|
|
57
47
|
if (workspaces.length === 0) {
|
|
58
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
48
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
59
49
|
}
|
|
60
50
|
const { workspace } = await inquirer_1.default.prompt([
|
|
61
51
|
{
|
|
@@ -170,7 +160,7 @@ class Init extends base_command_1.default {
|
|
|
170
160
|
// Select workspace
|
|
171
161
|
const workspaces = await services_1.workspacesApi.list();
|
|
172
162
|
if (workspaces.length === 0) {
|
|
173
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
163
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
174
164
|
}
|
|
175
165
|
const { workspace } = await inquirer_1.default.prompt([
|
|
176
166
|
{
|
package/dist/commands/link.js
CHANGED
|
@@ -44,7 +44,7 @@ class Link extends base_command_1.default {
|
|
|
44
44
|
else {
|
|
45
45
|
const workspaces = await services_1.workspacesApi.list();
|
|
46
46
|
if (workspaces.length === 0) {
|
|
47
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
47
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
48
48
|
}
|
|
49
49
|
const response = await inquirer_1.default.prompt([
|
|
50
50
|
{
|
package/dist/commands/login.js
CHANGED
|
@@ -4,7 +4,9 @@ const tslib_1 = require("tslib");
|
|
|
4
4
|
const core_1 = require("@oclif/core");
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
// eslint-disable-next-line node/no-missing-import -- eslint-plugin-node cannot resolve package "exports" subpaths
|
|
7
8
|
const mcp_js_1 = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
9
|
+
// eslint-disable-next-line node/no-missing-import -- eslint-plugin-node cannot resolve package "exports" subpaths
|
|
8
10
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
9
11
|
const zod_1 = require("zod");
|
|
10
12
|
const base_command_1 = tslib_1.__importDefault(require("../../lib/base-command"));
|
|
@@ -233,7 +235,7 @@ class McpServe extends base_command_1.default {
|
|
|
233
235
|
.boolean()
|
|
234
236
|
.optional()
|
|
235
237
|
.describe("Skip code formatting after generation (default: false)"),
|
|
236
|
-
}, async ({ language
|
|
238
|
+
}, async ({ language }) => {
|
|
237
239
|
try {
|
|
238
240
|
const configPath = (0, apsorc_parser_1.findConfigPath)();
|
|
239
241
|
if (!configPath) {
|
|
@@ -301,12 +303,14 @@ class McpServe extends base_command_1.default {
|
|
|
301
303
|
const enumFiles = await generator.generateEnums(entities, lowerCaseApiType);
|
|
302
304
|
for (const file of enumFiles) {
|
|
303
305
|
const fullPath = path.join(autogenPath, file.path);
|
|
306
|
+
// eslint-disable-next-line no-await-in-loop
|
|
304
307
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
305
308
|
filesGenerated.push(file.path);
|
|
306
309
|
}
|
|
307
310
|
// Generate per-entity files
|
|
308
311
|
for (const entity of entities) {
|
|
309
312
|
const entityRelationships = relationshipMap[entity.name] || [];
|
|
313
|
+
// eslint-disable-next-line no-await-in-loop
|
|
310
314
|
const allFiles = await Promise.all([
|
|
311
315
|
generator.generateEntity({
|
|
312
316
|
entity,
|
|
@@ -344,6 +348,7 @@ class McpServe extends base_command_1.default {
|
|
|
344
348
|
for (const files of allFiles) {
|
|
345
349
|
for (const file of files) {
|
|
346
350
|
const fullPath = path.join(autogenPath, file.path);
|
|
351
|
+
// eslint-disable-next-line no-await-in-loop
|
|
347
352
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
348
353
|
filesGenerated.push(file.path);
|
|
349
354
|
}
|
|
@@ -353,6 +358,7 @@ class McpServe extends base_command_1.default {
|
|
|
353
358
|
const guardFiles = await generator.generateGuards(entities, auth);
|
|
354
359
|
for (const file of guardFiles) {
|
|
355
360
|
const fullPath = path.join(autogenPath, file.path);
|
|
361
|
+
// eslint-disable-next-line no-await-in-loop
|
|
356
362
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
357
363
|
filesGenerated.push(file.path);
|
|
358
364
|
}
|
|
@@ -360,6 +366,7 @@ class McpServe extends base_command_1.default {
|
|
|
360
366
|
const indexFiles = await generator.generateIndexModule(entities, lowerCaseApiType);
|
|
361
367
|
for (const file of indexFiles) {
|
|
362
368
|
const fullPath = path.join(autogenPath, file.path);
|
|
369
|
+
// eslint-disable-next-line no-await-in-loop
|
|
363
370
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
364
371
|
filesGenerated.push(file.path);
|
|
365
372
|
}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -75,6 +75,7 @@ class Migrate extends base_command_1.default {
|
|
|
75
75
|
});
|
|
76
76
|
for (const file of migrationFiles) {
|
|
77
77
|
const fullPath = path.join(process.cwd(), file.path);
|
|
78
|
+
// eslint-disable-next-line no-await-in-loop
|
|
78
79
|
await (0, file_system_1.createFile)(fullPath, file.content);
|
|
79
80
|
this.log(`Generated migration file: ${file.path}`);
|
|
80
81
|
}
|
|
@@ -28,7 +28,7 @@ class Projects extends base_command_1.default {
|
|
|
28
28
|
// Prompt
|
|
29
29
|
const workspaces = await services_1.workspacesApi.list();
|
|
30
30
|
if (workspaces.length === 0) {
|
|
31
|
-
this.error("No workspaces found. Create a workspace at https://app.apso.
|
|
31
|
+
this.error("No workspaces found. Create a workspace at https://app.apso.cloud first.");
|
|
32
32
|
}
|
|
33
33
|
const response = await inquirer_1.default.prompt([
|
|
34
34
|
{
|
|
@@ -8,9 +8,9 @@ const apsorc_parser_1 = require("../../lib/apsorc-parser");
|
|
|
8
8
|
const schema_convert_1 = require("../../lib/utils/schema-convert");
|
|
9
9
|
// Inline ANSI color helpers (no dependency needed)
|
|
10
10
|
const useColor = () => !config_1.globalConfig.read().noColor;
|
|
11
|
-
const green = (s) => useColor() ? `\
|
|
12
|
-
const red = (s) => useColor() ? `\
|
|
13
|
-
const yellow = (s) => useColor() ? `\
|
|
11
|
+
const green = (s) => useColor() ? `\u001B[32m${s}\u001B[0m` : s;
|
|
12
|
+
const red = (s) => useColor() ? `\u001B[31m${s}\u001B[0m` : s;
|
|
13
|
+
const yellow = (s) => useColor() ? `\u001B[33m${s}\u001B[0m` : s;
|
|
14
14
|
class SchemaDiff extends base_command_1.default {
|
|
15
15
|
async run() {
|
|
16
16
|
// Auth guard
|
package/dist/lib/config/types.js
CHANGED
|
@@ -11,8 +11,8 @@ exports.CONFIG_ENV_VARS = exports.DEFAULT_GLOBAL_CONFIG = void 0;
|
|
|
11
11
|
* Default values for global configuration
|
|
12
12
|
*/
|
|
13
13
|
exports.DEFAULT_GLOBAL_CONFIG = {
|
|
14
|
-
apiUrl: "https://api.apso.
|
|
15
|
-
webUrl: "https://app.apso.
|
|
14
|
+
apiUrl: "https://api.apso.dev",
|
|
15
|
+
webUrl: "https://app.apso.cloud",
|
|
16
16
|
verbose: false,
|
|
17
17
|
noColor: false,
|
|
18
18
|
telemetryDisabled: false,
|
|
@@ -40,7 +40,7 @@ function checkPkStrategy(ctx) {
|
|
|
40
40
|
}
|
|
41
41
|
const content = fs.readFileSync(entityPath, "utf-8");
|
|
42
42
|
// The correct pattern is @PrimaryGeneratedColumn('uuid')
|
|
43
|
-
const hasPrimaryGenerated = /PrimaryGeneratedColumn\s*\(\s*['
|
|
43
|
+
const hasPrimaryGenerated = /PrimaryGeneratedColumn\s*\(\s*["']uuid["']\s*\)/.test(content);
|
|
44
44
|
// Wrong patterns: @PrimaryColumn() or @PrimaryColumn({ type: 'uuid' }) without generation
|
|
45
45
|
const hasPrimaryColumnOnly = /PrimaryColumn\s*\(/.test(content) && !hasPrimaryGenerated;
|
|
46
46
|
if (hasPrimaryColumnOnly) {
|
|
@@ -87,7 +87,7 @@ function checkUnusedImports(ctx) {
|
|
|
87
87
|
const lines = content.split("\n");
|
|
88
88
|
// Find import lines from typeorm
|
|
89
89
|
for (const line of lines) {
|
|
90
|
-
const importMatch = line.match(/import\s
|
|
90
|
+
const importMatch = line.match(/import\s*{([^}]+)}\s*from\s*["']typeorm["']/);
|
|
91
91
|
if (!importMatch)
|
|
92
92
|
continue;
|
|
93
93
|
const imports = importMatch[1]
|
|
@@ -285,7 +285,7 @@ class GoGenerator extends base_1.BaseGenerator {
|
|
|
285
285
|
}
|
|
286
286
|
const timestamp = new Date()
|
|
287
287
|
.toISOString()
|
|
288
|
-
.replace(/[
|
|
288
|
+
.replace(/[:T-]/g, "")
|
|
289
289
|
.slice(0, 14);
|
|
290
290
|
const name = migrationName || "auto";
|
|
291
291
|
const filename = `${timestamp}_${name}.sql`;
|
|
@@ -346,9 +346,6 @@ ${downStatements}
|
|
|
346
346
|
entities: entitiesData,
|
|
347
347
|
});
|
|
348
348
|
// Generate model registry for AutoMigrate support
|
|
349
|
-
const modelImports = entities
|
|
350
|
-
.map((e) => `\t// ${e.name} model`)
|
|
351
|
-
.join("\n");
|
|
352
349
|
const modelRefs = entities
|
|
353
350
|
.map((e) => `\t\t&${(0, casing_1.pascalCase)(e.name)}{},`)
|
|
354
351
|
.join("\n");
|
|
@@ -213,7 +213,7 @@ class PythonGenerator extends base_1.BaseGenerator {
|
|
|
213
213
|
}
|
|
214
214
|
const timestamp = new Date()
|
|
215
215
|
.toISOString()
|
|
216
|
-
.replace(/[
|
|
216
|
+
.replace(/[:T-]/g, "")
|
|
217
217
|
.slice(0, 14);
|
|
218
218
|
const revision = timestamp;
|
|
219
219
|
const name = migrationName || "auto";
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.resetSandbox = exports.applyMigration = exports.runMigrationSandbox = void 0;
|
|
14
14
|
const tslib_1 = require("tslib");
|
|
15
|
+
/* eslint-disable new-cap -- TypeORM decorator factories (Entity, Column, ...) are applied programmatically */
|
|
15
16
|
const fs = tslib_1.__importStar(require("fs"));
|
|
16
17
|
const path = tslib_1.__importStar(require("path"));
|
|
17
18
|
const paths_1 = require("../config/paths");
|
|
18
19
|
const snapshot_1 = require("./snapshot");
|
|
19
20
|
const field_1 = require("../utils/field");
|
|
20
21
|
const casing_1 = require("../utils/casing");
|
|
22
|
+
const relationships_1 = require("../utils/relationships");
|
|
21
23
|
/**
|
|
22
24
|
* Get the sandbox data directory for PGlite persistence.
|
|
23
25
|
*/
|
|
@@ -57,11 +59,12 @@ async function resetPGliteSingleton() {
|
|
|
57
59
|
* Clears TypeORM's global metadata storage first to prevent cross-contamination
|
|
58
60
|
* between runs.
|
|
59
61
|
*/
|
|
60
|
-
function buildEntityClasses(entities) {
|
|
61
|
-
var _a, _b;
|
|
62
|
+
function buildEntityClasses(entities, relationshipMap = {}) {
|
|
63
|
+
var _a, _b, _c;
|
|
62
64
|
clearTypeOrmMetadata();
|
|
63
65
|
const typeorm = require("typeorm");
|
|
64
66
|
const classes = [];
|
|
67
|
+
const classMap = new Map();
|
|
65
68
|
for (const entityDef of entities) {
|
|
66
69
|
const className = entityDef.name;
|
|
67
70
|
const tableName = (0, casing_1.snakeCase)(entityDef.name);
|
|
@@ -108,7 +111,7 @@ function buildEntityClasses(entities) {
|
|
|
108
111
|
if (field.type === "enum" && field.values) {
|
|
109
112
|
colOpts.enum = field.values;
|
|
110
113
|
}
|
|
111
|
-
if (field.length)
|
|
114
|
+
if (((_c = field.length) !== null && _c !== void 0 ? _c : 0) > 0)
|
|
112
115
|
colOpts.length = field.length;
|
|
113
116
|
if (field.precision)
|
|
114
117
|
colOpts.precision = field.precision;
|
|
@@ -122,9 +125,125 @@ function buildEntityClasses(entities) {
|
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
127
|
classes.push(EntityClass);
|
|
128
|
+
classMap.set(entityDef.name, EntityClass);
|
|
129
|
+
}
|
|
130
|
+
for (const entityDef of entities) {
|
|
131
|
+
const EntityClass = classMap.get(entityDef.name);
|
|
132
|
+
if (!EntityClass)
|
|
133
|
+
continue;
|
|
134
|
+
const relationships = (0, relationships_1.getRelationshipForTemplate)(entityDef.name, relationshipMap[entityDef.name] || [], entities);
|
|
135
|
+
for (const relationship of relationships) {
|
|
136
|
+
const targetClass = classMap.get(relationship.name);
|
|
137
|
+
if (!targetClass)
|
|
138
|
+
continue;
|
|
139
|
+
const inversePropertyName = relationship.inverseSidePropertyName ||
|
|
140
|
+
relationship.inversePropertyName ||
|
|
141
|
+
entityDef.name;
|
|
142
|
+
const relationOptions = {};
|
|
143
|
+
if (relationship.nullable !== undefined) {
|
|
144
|
+
relationOptions.nullable = relationship.nullable;
|
|
145
|
+
}
|
|
146
|
+
if (relationship.cascadeDelete) {
|
|
147
|
+
relationOptions.onDelete = "CASCADE";
|
|
148
|
+
}
|
|
149
|
+
if (relationship.type === "ManyToOne") {
|
|
150
|
+
if (relationship.index) {
|
|
151
|
+
typeorm.Index()(EntityClass.prototype, relationship.relationshipName);
|
|
152
|
+
}
|
|
153
|
+
const inverseSide = relationship.biDirectional
|
|
154
|
+
? (target) => target[inversePropertyName]
|
|
155
|
+
: undefined;
|
|
156
|
+
typeorm.ManyToOne(() => targetClass, inverseSide, relationOptions)(EntityClass.prototype, relationship.relationshipName);
|
|
157
|
+
typeorm.JoinColumn({ name: relationship.camelCasedId })(EntityClass.prototype, relationship.relationshipName);
|
|
158
|
+
}
|
|
159
|
+
if (relationship.type === "OneToMany" && relationship.biDirectional) {
|
|
160
|
+
typeorm.OneToMany(() => targetClass, (target) => target[inversePropertyName])(EntityClass.prototype, relationship.pluralizedRelationshipName);
|
|
161
|
+
}
|
|
162
|
+
if (relationship.type === "ManyToMany") {
|
|
163
|
+
const inverseSide = relationship.biDirectional
|
|
164
|
+
? (target) => target[inversePropertyName]
|
|
165
|
+
: undefined;
|
|
166
|
+
typeorm.ManyToMany(() => targetClass, inverseSide)(EntityClass.prototype, relationship.pluralizedRelationshipName);
|
|
167
|
+
if (relationship.joinTable) {
|
|
168
|
+
const joinTableOptions = {};
|
|
169
|
+
if (relationship.joinTableName) {
|
|
170
|
+
joinTableOptions.name = relationship.joinTableName;
|
|
171
|
+
}
|
|
172
|
+
if (relationship.joinColumnName) {
|
|
173
|
+
joinTableOptions.joinColumn = {
|
|
174
|
+
name: relationship.joinColumnName,
|
|
175
|
+
referencedColumnName: "id",
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
if (relationship.inverseJoinColumnName) {
|
|
179
|
+
joinTableOptions.inverseJoinColumn = {
|
|
180
|
+
name: relationship.inverseJoinColumnName,
|
|
181
|
+
referencedColumnName: "id",
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
typeorm.JoinTable(Object.keys(joinTableOptions).length > 0
|
|
185
|
+
? joinTableOptions
|
|
186
|
+
: undefined)(EntityClass.prototype, relationship.pluralizedRelationshipName);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (relationship.type === "OneToOne") {
|
|
190
|
+
const inverseSide = relationship.biDirectional
|
|
191
|
+
? (target) => target[inversePropertyName]
|
|
192
|
+
: undefined;
|
|
193
|
+
typeorm.OneToOne(() => targetClass, inverseSide, relationOptions)(EntityClass.prototype, relationship.relationshipName);
|
|
194
|
+
if (relationship.joinTable) {
|
|
195
|
+
typeorm.JoinColumn()(EntityClass.prototype, relationship.relationshipName);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
125
199
|
}
|
|
126
200
|
return classes;
|
|
127
201
|
}
|
|
202
|
+
function snapshotRelationships(relationshipMap) {
|
|
203
|
+
return Object.entries(relationshipMap)
|
|
204
|
+
.flatMap(([from, relationships]) => relationships.map((relationship) => {
|
|
205
|
+
var _a;
|
|
206
|
+
return ({
|
|
207
|
+
from,
|
|
208
|
+
to: relationship.name,
|
|
209
|
+
type: relationship.type,
|
|
210
|
+
to_name: (_a = relationship.referenceName) !== null && _a !== void 0 ? _a : undefined,
|
|
211
|
+
nullable: relationship.nullable,
|
|
212
|
+
bi_directional: relationship.biDirectional,
|
|
213
|
+
cascadeDelete: relationship.cascadeDelete,
|
|
214
|
+
index: relationship.index,
|
|
215
|
+
join: relationship.join,
|
|
216
|
+
joinTableName: relationship.joinTableName,
|
|
217
|
+
joinColumnName: relationship.joinColumnName,
|
|
218
|
+
inverseJoinColumnName: relationship.inverseJoinColumnName,
|
|
219
|
+
inverseReferenceName: relationship.inverseReferenceName,
|
|
220
|
+
});
|
|
221
|
+
}))
|
|
222
|
+
.sort((a, b) => `${a.from}:${a.to}:${a.type}:${a.to_name || ""}`.localeCompare(`${b.from}:${b.to}:${b.type}:${b.to_name || ""}`));
|
|
223
|
+
}
|
|
224
|
+
function relationshipMapFromSnapshot(relationships = []) {
|
|
225
|
+
const relationshipMap = {};
|
|
226
|
+
for (const relationship of relationships) {
|
|
227
|
+
relationshipMap[relationship.from] = [
|
|
228
|
+
...(relationshipMap[relationship.from] || []),
|
|
229
|
+
{
|
|
230
|
+
name: relationship.to,
|
|
231
|
+
type: relationship.type,
|
|
232
|
+
referenceName: relationship.to_name,
|
|
233
|
+
nullable: relationship.nullable,
|
|
234
|
+
biDirectional: relationship.bi_directional,
|
|
235
|
+
cascadeDelete: relationship.cascadeDelete,
|
|
236
|
+
index: relationship.index,
|
|
237
|
+
join: relationship.join,
|
|
238
|
+
joinTableName: relationship.joinTableName,
|
|
239
|
+
joinColumnName: relationship.joinColumnName,
|
|
240
|
+
inverseJoinColumnName: relationship.inverseJoinColumnName,
|
|
241
|
+
inverseReferenceName: relationship.inverseReferenceName,
|
|
242
|
+
},
|
|
243
|
+
];
|
|
244
|
+
}
|
|
245
|
+
return relationshipMap;
|
|
246
|
+
}
|
|
128
247
|
/**
|
|
129
248
|
* Create a TypeORM DataSource configured for PGlite with in-memory entity classes.
|
|
130
249
|
*/
|
|
@@ -135,10 +254,10 @@ async function createPGliteDataSource(entityClasses) {
|
|
|
135
254
|
const pgliteModule = await Promise.resolve().then(() => tslib_1.__importStar(require("typeorm-pglite")));
|
|
136
255
|
PGliteDriver = pgliteModule.PGliteDriver;
|
|
137
256
|
}
|
|
138
|
-
catch (
|
|
257
|
+
catch (error) {
|
|
139
258
|
throw new Error("Migration sandbox requires typeorm-pglite and @electric-sql/pglite.\n" +
|
|
140
259
|
"Install them with: npm install typeorm @electric-sql/pglite typeorm-pglite\n" +
|
|
141
|
-
`Original error: ${
|
|
260
|
+
`Original error: ${error instanceof Error ? error.message : error}`);
|
|
142
261
|
}
|
|
143
262
|
// Create a driver. typeorm-pglite uses a singleton PGliteInstance
|
|
144
263
|
// so multiple DataSources share the same in-process database.
|
|
@@ -185,6 +304,7 @@ async function runMigrationSandbox(current, projectDir) {
|
|
|
185
304
|
scopeBy: e.scopeBy,
|
|
186
305
|
});
|
|
187
306
|
}),
|
|
307
|
+
relationships: snapshotRelationships(current.relationshipMap),
|
|
188
308
|
};
|
|
189
309
|
// Quick check: has anything changed?
|
|
190
310
|
if (snapshot && !schemaChanged(currentSnapshot, snapshot)) {
|
|
@@ -221,7 +341,7 @@ async function runMigrationSandbox(current, projectDir) {
|
|
|
221
341
|
: [];
|
|
222
342
|
// Step 1: Establish baseline with previous schema
|
|
223
343
|
if (previousEntities.length > 0) {
|
|
224
|
-
const prevClasses = buildEntityClasses(previousEntities);
|
|
344
|
+
const prevClasses = buildEntityClasses(previousEntities, relationshipMapFromSnapshot(snapshot === null || snapshot === void 0 ? void 0 : snapshot.relationships));
|
|
225
345
|
const prevDs = await createPGliteDataSource(prevClasses);
|
|
226
346
|
await prevDs.initialize();
|
|
227
347
|
await prevDs.synchronize();
|
|
@@ -230,7 +350,7 @@ async function runMigrationSandbox(current, projectDir) {
|
|
|
230
350
|
// The PGlite in-memory database persists in the singleton.
|
|
231
351
|
}
|
|
232
352
|
// Step 2: Compute diff with current schema
|
|
233
|
-
const currentClasses = buildEntityClasses(current.entities);
|
|
353
|
+
const currentClasses = buildEntityClasses(current.entities, current.relationshipMap);
|
|
234
354
|
const currentDs = await createPGliteDataSource(currentClasses);
|
|
235
355
|
await currentDs.initialize();
|
|
236
356
|
try {
|
|
@@ -275,7 +395,9 @@ async function runMigrationSandbox(current, projectDir) {
|
|
|
275
395
|
try {
|
|
276
396
|
await resetPGliteSingleton();
|
|
277
397
|
}
|
|
278
|
-
catch {
|
|
398
|
+
catch {
|
|
399
|
+
/* ignore */
|
|
400
|
+
}
|
|
279
401
|
return {
|
|
280
402
|
needed: true,
|
|
281
403
|
success: false,
|
|
@@ -314,6 +436,7 @@ function applyMigration(current, projectDir) {
|
|
|
314
436
|
scopeBy: e.scopeBy,
|
|
315
437
|
});
|
|
316
438
|
}),
|
|
439
|
+
relationships: snapshotRelationships(current.relationshipMap),
|
|
317
440
|
};
|
|
318
441
|
(0, snapshot_1.writeSnapshot)(snapshot, projectDir);
|
|
319
442
|
}
|
|
@@ -333,5 +456,7 @@ exports.resetSandbox = resetSandbox;
|
|
|
333
456
|
* Simple deep comparison between current and snapshot schemas.
|
|
334
457
|
*/
|
|
335
458
|
function schemaChanged(current, snapshot) {
|
|
336
|
-
return (JSON.stringify(current.entities) !== JSON.stringify(snapshot.entities)
|
|
459
|
+
return (JSON.stringify(current.entities) !== JSON.stringify(snapshot.entities) ||
|
|
460
|
+
JSON.stringify(current.relationships || []) !==
|
|
461
|
+
JSON.stringify(snapshot.relationships || []));
|
|
337
462
|
}
|
|
@@ -57,6 +57,7 @@ export interface ComparableSchema {
|
|
|
57
57
|
updated_at?: boolean;
|
|
58
58
|
scopeBy?: string | string[];
|
|
59
59
|
}>;
|
|
60
|
+
relationships?: ApsorcSnapshot["relationships"];
|
|
60
61
|
}
|
|
61
62
|
/**
|
|
62
63
|
* Read the saved schema snapshot, or null if none exists.
|
|
@@ -70,11 +71,4 @@ export declare function writeSnapshot(schema: ApsorcSnapshot, projectDir?: strin
|
|
|
70
71
|
* Delete the snapshot and sandbox directory contents (for --reset).
|
|
71
72
|
*/
|
|
72
73
|
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
74
|
export declare function hasChanges(current: ComparableSchema, snapshot: ApsorcSnapshot | null): boolean;
|
|
@@ -27,6 +27,7 @@ function readSnapshot(projectDir) {
|
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
try {
|
|
30
|
+
// eslint-disable-next-line unicorn/prefer-json-parse-buffer -- TS types JSON.parse as string-only
|
|
30
31
|
const content = fs.readFileSync(snapshotPath, "utf8");
|
|
31
32
|
return JSON.parse(content);
|
|
32
33
|
}
|
|
@@ -62,11 +63,11 @@ exports.resetSnapshot = resetSnapshot;
|
|
|
62
63
|
* deterministic JSON serialization. Returns true if any change is detected,
|
|
63
64
|
* or if no snapshot exists (first run).
|
|
64
65
|
*/
|
|
66
|
+
const normalize = (obj) => JSON.stringify(obj);
|
|
65
67
|
function hasChanges(current, snapshot) {
|
|
66
68
|
if (!snapshot) {
|
|
67
69
|
return true;
|
|
68
70
|
}
|
|
69
|
-
const normalize = (obj) => JSON.stringify(obj);
|
|
70
71
|
// Compare entity count
|
|
71
72
|
if (current.entities.length !== snapshot.entities.length) {
|
|
72
73
|
return true;
|
|
@@ -95,6 +96,9 @@ function hasChanges(current, snapshot) {
|
|
|
95
96
|
return true;
|
|
96
97
|
}
|
|
97
98
|
}
|
|
99
|
+
if (normalize(current.relationships || []) !== normalize(snapshot.relationships || [])) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
98
102
|
return false;
|
|
99
103
|
}
|
|
100
104
|
exports.hasChanges = hasChanges;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "bigint"
|
|
3
|
+
@Column({ "type": "bigint", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "decimal", precision: <%= field.precision || 10 %>, scale: <%= field.scale || 2 %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
3
|
+
@Column({ "type": "decimal", precision: <%= field.precision || 10 %>, scale: <%= field.scale || 2 %>, nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "float8"
|
|
3
|
+
@Column({ "type": "float8", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "decimal"
|
|
3
|
+
@Column({ "type": "decimal", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "int"
|
|
3
|
+
@Column({ "type": "int", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "money"
|
|
3
|
+
@Column({ "type": "money", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "numeric", precision: <%= field.precision || 10 %>, scale: <%= field.scale || 2 %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
3
|
+
@Column({ "type": "numeric", precision: <%= field.precision || 10 %>, scale: <%= field.scale || 2 %>, nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "real"
|
|
3
|
+
@Column({ "type": "real", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<% const {field} = it; %>
|
|
2
2
|
|
|
3
|
-
@Column({ "type": "smallint"
|
|
3
|
+
@Column({ "type": "smallint", nullable: <%= Boolean(field.nullable) %><% if (field.default !== null) {%>, default: <%= field.default %><% } %> })
|
|
4
4
|
<% if (field.index ) {%>
|
|
5
5
|
@Index()
|
|
6
6
|
<% } %>
|
|
@@ -222,19 +222,21 @@ func applyOperator(query *gorm.DB, field, op, value string, useOr bool) *gorm.DB
|
|
|
222
222
|
where = query.Or
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
cv := coerceValue(value)
|
|
226
|
+
|
|
225
227
|
switch op {
|
|
226
228
|
case "$eq":
|
|
227
|
-
return where(fmt.Sprintf("%s = ?", field),
|
|
229
|
+
return where(fmt.Sprintf("%s = ?", field), cv)
|
|
228
230
|
case "$ne":
|
|
229
|
-
return where(fmt.Sprintf("%s != ?", field),
|
|
231
|
+
return where(fmt.Sprintf("%s != ?", field), cv)
|
|
230
232
|
case "$gt":
|
|
231
|
-
return where(fmt.Sprintf("%s > ?", field),
|
|
233
|
+
return where(fmt.Sprintf("%s > ?", field), cv)
|
|
232
234
|
case "$gte":
|
|
233
|
-
return where(fmt.Sprintf("%s >= ?", field),
|
|
235
|
+
return where(fmt.Sprintf("%s >= ?", field), cv)
|
|
234
236
|
case "$lt":
|
|
235
|
-
return where(fmt.Sprintf("%s < ?", field),
|
|
237
|
+
return where(fmt.Sprintf("%s < ?", field), cv)
|
|
236
238
|
case "$lte":
|
|
237
|
-
return where(fmt.Sprintf("%s <= ?", field),
|
|
239
|
+
return where(fmt.Sprintf("%s <= ?", field), cv)
|
|
238
240
|
case "$starts":
|
|
239
241
|
return where(fmt.Sprintf("%s LIKE ?", field), value+"%")
|
|
240
242
|
case "$ends":
|
|
@@ -244,9 +246,9 @@ func applyOperator(query *gorm.DB, field, op, value string, useOr bool) *gorm.DB
|
|
|
244
246
|
case "$excl":
|
|
245
247
|
return where(fmt.Sprintf("%s NOT LIKE ?", field), "%"+value+"%")
|
|
246
248
|
case "$in":
|
|
247
|
-
return where(fmt.Sprintf("%s IN ?", field), splitValue(value))
|
|
249
|
+
return where(fmt.Sprintf("%s IN ?", field), coerceValues(splitValue(value)))
|
|
248
250
|
case "$notin":
|
|
249
|
-
return where(fmt.Sprintf("%s NOT IN ?", field), splitValue(value))
|
|
251
|
+
return where(fmt.Sprintf("%s NOT IN ?", field), coerceValues(splitValue(value)))
|
|
250
252
|
case "$isnull":
|
|
251
253
|
return where(fmt.Sprintf("%s IS NULL", field))
|
|
252
254
|
case "$notnull":
|
|
@@ -254,7 +256,7 @@ func applyOperator(query *gorm.DB, field, op, value string, useOr bool) *gorm.DB
|
|
|
254
256
|
case "$between":
|
|
255
257
|
vals := splitValue(value)
|
|
256
258
|
if len(vals) >= 2 {
|
|
257
|
-
return where(fmt.Sprintf("%s BETWEEN ? AND ?", field), vals[0], vals[1])
|
|
259
|
+
return where(fmt.Sprintf("%s BETWEEN ? AND ?", field), coerceValue(vals[0]), coerceValue(vals[1]))
|
|
258
260
|
}
|
|
259
261
|
return query
|
|
260
262
|
case "$eqL":
|
|
@@ -299,3 +301,29 @@ func splitValue(value string) []string {
|
|
|
299
301
|
}
|
|
300
302
|
return result
|
|
301
303
|
}
|
|
304
|
+
|
|
305
|
+
// coerceValue converts a string query parameter to a typed value.
|
|
306
|
+
// Booleans and numbers are converted so the database driver sends the
|
|
307
|
+
// correct wire type instead of a raw string.
|
|
308
|
+
func coerceValue(value string) interface{} {
|
|
309
|
+
low := strings.ToLower(value)
|
|
310
|
+
if low == "true" || low == "false" || low == "yes" || low == "no" || low == "1" || low == "0" {
|
|
311
|
+
return low == "true" || low == "yes" || low == "1"
|
|
312
|
+
}
|
|
313
|
+
if v, err := strconv.ParseInt(value, 10, 64); err == nil {
|
|
314
|
+
return v
|
|
315
|
+
}
|
|
316
|
+
if v, err := strconv.ParseFloat(value, 64); err == nil {
|
|
317
|
+
return v
|
|
318
|
+
}
|
|
319
|
+
return value
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// coerceValues applies coerceValue to each element in a slice.
|
|
323
|
+
func coerceValues(values []string) []interface{} {
|
|
324
|
+
result := make([]interface{}, len(values))
|
|
325
|
+
for i, v := range values {
|
|
326
|
+
result[i] = coerceValue(v)
|
|
327
|
+
}
|
|
328
|
+
return result
|
|
329
|
+
}
|
|
@@ -6,7 +6,7 @@ import math
|
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any, Dict, List, Optional, Sequence, Tuple, Type, TypeVar
|
|
8
8
|
|
|
9
|
-
from sqlalchemy import asc, desc, func, or_
|
|
9
|
+
from sqlalchemy import Boolean, Date, DateTime, Float, Integer, Numeric, asc, desc, func, or_
|
|
10
10
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
11
11
|
from sqlalchemy.orm import selectinload
|
|
12
12
|
from sqlalchemy.sql import Select, select
|
|
@@ -65,30 +65,58 @@ class PaginatedResult:
|
|
|
65
65
|
# Operator registry
|
|
66
66
|
# ---------------------------------------------------------------------------
|
|
67
67
|
|
|
68
|
+
def _coerce_value(col: Any, value: str) -> Any:
|
|
69
|
+
"""Coerce a string query param value to match the column's SQL type."""
|
|
70
|
+
try:
|
|
71
|
+
col_type = col.property.columns[0].type
|
|
72
|
+
except (AttributeError, IndexError):
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
if isinstance(col_type, Boolean):
|
|
76
|
+
return value.lower() in ("true", "1", "yes")
|
|
77
|
+
if isinstance(col_type, (Integer,)):
|
|
78
|
+
try:
|
|
79
|
+
return int(value)
|
|
80
|
+
except (ValueError, TypeError):
|
|
81
|
+
return value
|
|
82
|
+
if isinstance(col_type, (Float, Numeric)):
|
|
83
|
+
try:
|
|
84
|
+
return float(value)
|
|
85
|
+
except (ValueError, TypeError):
|
|
86
|
+
return value
|
|
87
|
+
return value
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _coerce_list(col: Any, values: List[str]) -> List[Any]:
|
|
91
|
+
"""Coerce a list of string values to match the column's SQL type."""
|
|
92
|
+
return [_coerce_value(col, v) for v in values]
|
|
93
|
+
|
|
94
|
+
|
|
68
95
|
def _apply_operator(model: Any, field_name: str, op: str, value: str) -> Any:
|
|
69
96
|
"""Return a SQLAlchemy filter expression for the given operator."""
|
|
70
97
|
col = getattr(model, field_name, None)
|
|
71
98
|
if col is None:
|
|
72
99
|
return None
|
|
73
100
|
|
|
101
|
+
typed_value = _coerce_value(col, value)
|
|
74
102
|
lower_col = func.lower(col)
|
|
75
103
|
|
|
76
104
|
operators: Dict[str, Any] = {
|
|
77
|
-
"$eq": lambda: col ==
|
|
78
|
-
"$ne": lambda: col !=
|
|
79
|
-
"$gt": lambda: col >
|
|
80
|
-
"$gte": lambda: col >=
|
|
81
|
-
"$lt": lambda: col <
|
|
82
|
-
"$lte": lambda: col <=
|
|
105
|
+
"$eq": lambda: col == typed_value,
|
|
106
|
+
"$ne": lambda: col != typed_value,
|
|
107
|
+
"$gt": lambda: col > typed_value,
|
|
108
|
+
"$gte": lambda: col >= typed_value,
|
|
109
|
+
"$lt": lambda: col < typed_value,
|
|
110
|
+
"$lte": lambda: col <= typed_value,
|
|
83
111
|
"$starts": lambda: col.like(f"{value}%"),
|
|
84
112
|
"$ends": lambda: col.like(f"%{value}"),
|
|
85
113
|
"$cont": lambda: col.like(f"%{value}%"),
|
|
86
114
|
"$excl": lambda: ~col.like(f"%{value}%"),
|
|
87
|
-
"$in": lambda: col.in_(_split_value(value)),
|
|
88
|
-
"$notin": lambda: ~col.in_(_split_value(value)),
|
|
115
|
+
"$in": lambda: col.in_(_coerce_list(col, _split_value(value))),
|
|
116
|
+
"$notin": lambda: ~col.in_(_coerce_list(col, _split_value(value))),
|
|
89
117
|
"$isnull": lambda: col.is_(None),
|
|
90
118
|
"$notnull": lambda: col.isnot(None),
|
|
91
|
-
"$between": lambda: col.between(*_split_value(value)[:2]),
|
|
119
|
+
"$between": lambda: col.between(*_coerce_list(col, _split_value(value)[:2])),
|
|
92
120
|
"$eqL": lambda: lower_col == value.lower(),
|
|
93
121
|
"$neL": lambda: lower_col != value.lower(),
|
|
94
122
|
"$startsL": lambda: lower_col.like(f"{value.lower()}%"),
|
|
@@ -147,7 +147,7 @@ exports.serviceSchemaToApsorc = serviceSchemaToApsorc;
|
|
|
147
147
|
*/
|
|
148
148
|
function sortKeys(value) {
|
|
149
149
|
if (Array.isArray(value)) {
|
|
150
|
-
return value.map(sortKeys);
|
|
150
|
+
return value.map((item) => sortKeys(item));
|
|
151
151
|
}
|
|
152
152
|
if (value !== null && typeof value === "object") {
|
|
153
153
|
const sorted = {};
|
|
@@ -39,7 +39,8 @@ async function createSpinner(text) {
|
|
|
39
39
|
const config = config_1.globalConfig.read();
|
|
40
40
|
try {
|
|
41
41
|
// Dynamic import to keep ora optional at startup
|
|
42
|
-
const
|
|
42
|
+
const oraModule = await Promise.resolve().then(() => __importStar(require("ora")));
|
|
43
|
+
const ora = oraModule.default;
|
|
43
44
|
const spinner = ora({
|
|
44
45
|
text,
|
|
45
46
|
color: config.noColor ? undefined : "cyan",
|
|
@@ -6,7 +6,7 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
6
6
|
const path = tslib_1.__importStar(require("path"));
|
|
7
7
|
const shelljs_1 = tslib_1.__importDefault(require("shelljs"));
|
|
8
8
|
exports.TEMPLATE_REPOS = {
|
|
9
|
-
typescript: "https://github.com/apsoai/service-template.git",
|
|
9
|
+
typescript: "https://github.com/apsoai/service-template-ts.git",
|
|
10
10
|
python: "https://github.com/apsoai/service-template-python.git",
|
|
11
11
|
go: "https://github.com/apsoai/service-template-go.git",
|
|
12
12
|
};
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@apso/cli",
|
|
9
|
-
"version": "0.
|
|
10
|
-
"license": "
|
|
9
|
+
"version": "0.15.1",
|
|
10
|
+
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@electric-sql/pglite": "^0.2.17",
|
|
13
13
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"rc": "^1.2.8",
|
|
23
23
|
"shelljs": "^0.8.5",
|
|
24
24
|
"typeorm": "^0.3.20",
|
|
25
|
-
"typeorm-pglite": "^0.3.4"
|
|
25
|
+
"typeorm-pglite": "^0.3.4",
|
|
26
|
+
"zod": "^4.4.3"
|
|
26
27
|
},
|
|
27
28
|
"bin": {
|
|
28
29
|
"apso": "bin/run"
|
package/oclif.manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
2
|
+
"version": "0.15.1",
|
|
3
3
|
"commands": {
|
|
4
4
|
"config": {
|
|
5
5
|
"id": "config",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
},
|
|
74
74
|
"dev": {
|
|
75
75
|
"id": "dev",
|
|
76
|
-
"description": "Start the local development server
|
|
76
|
+
"description": "Start the local development server. Detects project language and database type from .apsorc and .env.",
|
|
77
77
|
"strict": true,
|
|
78
78
|
"pluginName": "@apso/cli",
|
|
79
79
|
"pluginAlias": "@apso/cli",
|
|
@@ -81,22 +81,27 @@
|
|
|
81
81
|
"aliases": [],
|
|
82
82
|
"examples": [
|
|
83
83
|
"$ apso dev",
|
|
84
|
-
"$ apso dev --
|
|
85
|
-
"$ apso dev --
|
|
86
|
-
"$ apso dev --build --detach"
|
|
84
|
+
"$ apso dev --docker",
|
|
85
|
+
"$ apso dev --docker --build"
|
|
87
86
|
],
|
|
88
87
|
"flags": {
|
|
88
|
+
"docker": {
|
|
89
|
+
"name": "docker",
|
|
90
|
+
"type": "boolean",
|
|
91
|
+
"description": "Use Docker Compose instead of the native dev stack",
|
|
92
|
+
"allowNo": false
|
|
93
|
+
},
|
|
89
94
|
"detach": {
|
|
90
95
|
"name": "detach",
|
|
91
96
|
"type": "boolean",
|
|
92
97
|
"char": "d",
|
|
93
|
-
"description": "Run containers in the background",
|
|
98
|
+
"description": "Run containers in the background (Docker mode only)",
|
|
94
99
|
"allowNo": false
|
|
95
100
|
},
|
|
96
101
|
"build": {
|
|
97
102
|
"name": "build",
|
|
98
103
|
"type": "boolean",
|
|
99
|
-
"description": "Rebuild images before starting",
|
|
104
|
+
"description": "Rebuild images before starting (Docker mode only)",
|
|
100
105
|
"allowNo": false
|
|
101
106
|
}
|
|
102
107
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@apso/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
4
4
|
"mcpName": "io.github.apsoai/apso",
|
|
5
5
|
"description": "Apso CLI",
|
|
6
6
|
"author": "Apso by Mavric - @mavric",
|
|
@@ -37,7 +37,8 @@
|
|
|
37
37
|
"rc": "^1.2.8",
|
|
38
38
|
"shelljs": "^0.8.5",
|
|
39
39
|
"typeorm": "^0.3.20",
|
|
40
|
-
"typeorm-pglite": "^0.3.4"
|
|
40
|
+
"typeorm-pglite": "^0.3.4",
|
|
41
|
+
"zod": "^4.4.3"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
44
|
"@jest/globals": "^29.5.0",
|