@apso/cli 0.41.2 → 0.42.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.
@@ -11,6 +11,7 @@ const perf_hooks_1 = require("perf_hooks");
11
11
  const file_system_1 = require("../lib/utils/file-system");
12
12
  const git_hooks_1 = require("../lib/utils/git-hooks");
13
13
  const crud_deps_1 = require("../lib/utils/crud-deps");
14
+ const pglite_upgrade_1 = require("../lib/utils/pglite-upgrade");
14
15
  class Generate extends base_command_1.default {
15
16
  async run() {
16
17
  var _a, _b;
@@ -249,6 +250,10 @@ class Generate extends base_command_1.default {
249
250
  if (added.length > 0) {
250
251
  console.log(`[apso] Added ${added.join(", ")} to package.json (the generated code imports them). Run npm install before building.`);
251
252
  }
253
+ // The pre-deploy checks run on PGlite; give a stock v1 config support.
254
+ if ((0, pglite_upgrade_1.upgradeOrmConfigForPglite)(process.cwd())) {
255
+ console.log("[apso] Updated src/orm.config.ts so `apso migrate verify` and `apso test smoke` can run on a local PGlite database. Run npm install before building.");
256
+ }
252
257
  }
253
258
  const totalBuildTime = perf_hooks_1.performance.now() - totalBuildStart;
254
259
  console.log(`[apso] Finished building all entities in ${totalBuildTime.toFixed(2)} ms`);
@@ -71,6 +71,17 @@ export declare class ApsorcNamingError extends Error {
71
71
  * schema builder runs the same rules, so a schema that passes there passes here.
72
72
  */
73
73
  export declare const assertCodegenSafeNames: (apsorc: Pick<ApsorcType, "entities" | "relationships">) => void;
74
+ /**
75
+ * Drop indexes the database cannot create alongside another. TypeORM names an
76
+ * index from its table and columns only, so a plain and a unique index on the
77
+ * same columns get one name and the second CREATE fails, rolling back the
78
+ * schema (prod svc 52: Warehouse.manager_email). The unique index also serves
79
+ * lookups, so it is kept; exact duplicates are dropped too.
80
+ *
81
+ * @param {Entity} entity - An entity from .apsorc.
82
+ * @returns {Entity} The entity, with a filtered copy of its indexes when needed.
83
+ */
84
+ export declare const dedupeIndexes: (entity: Entity) => Entity;
74
85
  export declare const parseApsorcV2: (apsorc: ApsorcType) => ParsedApsorcData;
75
86
  export declare const parseApsorc: () => ParsedApsorc;
76
87
  /**
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.findConfigPath = exports.readApsorcFile = exports.parseApsorc = exports.parseApsorcV2 = exports.assertCodegenSafeNames = exports.ApsorcNamingError = exports.parseApsorcV1 = exports.ApiType = void 0;
3
+ exports.findConfigPath = exports.readApsorcFile = exports.parseApsorc = exports.parseApsorcV2 = exports.dedupeIndexes = exports.assertCodegenSafeNames = exports.ApsorcNamingError = exports.parseApsorcV1 = exports.ApiType = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
@@ -55,11 +55,46 @@ const assertCodegenSafeNames = (apsorc) => {
55
55
  throw new ApsorcNamingError(`${error.message}${more > 0 ? ` (${more} more error${more === 1 ? "" : "s"})` : ""} Run "apso schema lint" to see every issue, or "apso schema lint --fix" to apply the safe fixes.`, error.rule, error.entity || "", error.field);
56
56
  };
57
57
  exports.assertCodegenSafeNames = assertCodegenSafeNames;
58
+ const columns = (fields) => fields.join(",");
59
+ /**
60
+ * Drop indexes the database cannot create alongside another. TypeORM names an
61
+ * index from its table and columns only, so a plain and a unique index on the
62
+ * same columns get one name and the second CREATE fails, rolling back the
63
+ * schema (prod svc 52: Warehouse.manager_email). The unique index also serves
64
+ * lookups, so it is kept; exact duplicates are dropped too.
65
+ *
66
+ * @param {Entity} entity - An entity from .apsorc.
67
+ * @returns {Entity} The entity, with a filtered copy of its indexes when needed.
68
+ */
69
+ const dedupeIndexes = (entity) => {
70
+ var _a;
71
+ if (!((_a = entity.indexes) === null || _a === void 0 ? void 0 : _a.length))
72
+ return entity;
73
+ const uniqueColumns = new Set(entity.indexes
74
+ .filter((index) => index.unique)
75
+ .map((index) => columns(index.fields)));
76
+ const seen = new Set();
77
+ const indexes = entity.indexes.filter((index) => {
78
+ const key = `${columns(index.fields)}|${index.unique ? "unique" : ""}`;
79
+ if (seen.has(key) ||
80
+ (!index.unique && uniqueColumns.has(columns(index.fields))))
81
+ return false;
82
+ seen.add(key);
83
+ return true;
84
+ });
85
+ return indexes.length === entity.indexes.length
86
+ ? entity
87
+ : { ...entity, indexes };
88
+ };
89
+ exports.dedupeIndexes = dedupeIndexes;
58
90
  const parseApsorcV2 = (apsorc) => {
59
91
  const { entities, relationships: apsoRelationships } = apsorc;
60
92
  (0, exports.assertCodegenSafeNames)(apsorc);
61
93
  const relationshipMap = (0, relationships_1.parseRelationships)(apsoRelationships);
62
- return { entities, relationshipMap };
94
+ return {
95
+ entities: entities.map((entity) => (0, exports.dedupeIndexes)(entity)),
96
+ relationshipMap,
97
+ };
63
98
  };
64
99
  exports.parseApsorcV2 = parseApsorcV2;
65
100
  const parseRc = () => {
@@ -156,7 +191,10 @@ const readApsorcFile = () => {
156
191
  if (!configPath) {
157
192
  throw new Error("No .apsorc found in this directory or any parent.");
158
193
  }
159
- return { configPath, apsorc: JSON.parse(fs.readFileSync(configPath).toString()) };
194
+ return {
195
+ configPath,
196
+ apsorc: JSON.parse(fs.readFileSync(configPath).toString()),
197
+ };
160
198
  };
161
199
  exports.readApsorcFile = readApsorcFile;
162
200
  const findConfigPath = () => {
@@ -16,3 +16,11 @@ export declare const APSO_CRUD_DEPS: Record<string, string>;
16
16
  * @returns {string[]} Names added (empty when nothing changed or no package.json).
17
17
  */
18
18
  export declare function ensureCrudDeps(projectDir: string): string[];
19
+ /**
20
+ * Add the given dependencies to package.json where it doesn't declare them.
21
+ *
22
+ * @param {string} projectDir - Directory holding the project's package.json.
23
+ * @param {Record<string, string>} deps - Package names and version ranges.
24
+ * @returns {string[]} Names added (empty when nothing changed or no package.json).
25
+ */
26
+ export declare function addMissingDeps(projectDir: string, deps: Record<string, string>): string[];
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensureCrudDeps = exports.APSO_CRUD_DEPS = void 0;
3
+ exports.addMissingDeps = exports.ensureCrudDeps = exports.APSO_CRUD_DEPS = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const fs = tslib_1.__importStar(require("fs"));
6
6
  const path = tslib_1.__importStar(require("path"));
@@ -27,6 +27,17 @@ exports.APSO_CRUD_DEPS = {
27
27
  * @returns {string[]} Names added (empty when nothing changed or no package.json).
28
28
  */
29
29
  function ensureCrudDeps(projectDir) {
30
+ return addMissingDeps(projectDir, exports.APSO_CRUD_DEPS);
31
+ }
32
+ exports.ensureCrudDeps = ensureCrudDeps;
33
+ /**
34
+ * Add the given dependencies to package.json where it doesn't declare them.
35
+ *
36
+ * @param {string} projectDir - Directory holding the project's package.json.
37
+ * @param {Record<string, string>} deps - Package names and version ranges.
38
+ * @returns {string[]} Names added (empty when nothing changed or no package.json).
39
+ */
40
+ function addMissingDeps(projectDir, deps) {
30
41
  var _a;
31
42
  const pkgPath = path.join(projectDir, "package.json");
32
43
  if (!fs.existsSync(pkgPath))
@@ -34,14 +45,14 @@ function ensureCrudDeps(projectDir) {
34
45
  const raw = fs.readFileSync(pkgPath, "utf8");
35
46
  const pkg = JSON.parse(raw);
36
47
  const declared = { ...pkg.devDependencies, ...pkg.dependencies };
37
- const missing = Object.keys(exports.APSO_CRUD_DEPS).filter((name) => !declared[name]);
48
+ const missing = Object.keys(deps).filter((name) => !declared[name]);
38
49
  if (missing.length === 0)
39
50
  return [];
40
51
  pkg.dependencies = (_a = pkg.dependencies) !== null && _a !== void 0 ? _a : {};
41
52
  for (const name of missing)
42
- pkg.dependencies[name] = exports.APSO_CRUD_DEPS[name];
53
+ pkg.dependencies[name] = deps[name];
43
54
  pkg.dependencies = Object.fromEntries(Object.entries(pkg.dependencies).sort(([a], [b]) => a.localeCompare(b)));
44
55
  fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + (raw.endsWith("\n") ? "\n" : ""));
45
56
  return missing;
46
57
  }
47
- exports.ensureCrudDeps = ensureCrudDeps;
58
+ exports.addMissingDeps = addMissingDeps;
@@ -0,0 +1,22 @@
1
+ /**
2
+ * PGlite packages the upgraded orm.config.ts loads. Versions match the pinned
3
+ * service-template-ts tag (see TEMPLATE_REFS in ./template.ts).
4
+ */
5
+ export declare const PGLITE_DEPS: Record<string, string>;
6
+ /** src/orm.config.ts as the v1 template shipped it (no PGlite support). */
7
+ export declare const ORM_CONFIG_V1 = "import { ConfigModule, ConfigService } from '@nestjs/config';\nimport { DataSource } from 'typeorm';\nimport {\n TypeOrmModuleAsyncOptions,\n TypeOrmModuleOptions,\n} from '@nestjs/typeorm';\nimport { join } from 'path';\nimport AppConfig from './config';\n\nconst sslConfig = process.env.DB_SSL\n ? {\n ssl: true,\n extra: {\n ssl: {\n rejectUnauthorized: false,\n },\n },\n }\n : {};\n\nconst ormConfig = {\n type: AppConfig.database.type as any,\n host: AppConfig.database.host,\n port: parseInt(AppConfig.database.port, 10),\n username: AppConfig.database.username,\n database: AppConfig.database.database,\n password: AppConfig.database.password,\n schema: AppConfig.database.schema,\n entities: [join(__dirname, '**', '*.entity.js')],\n migrations: [join(__dirname, '**', 'migrations/*-migration.js')],\n synchronize: AppConfig.database.synchronize,\n logging: AppConfig.database.logging,\n ...sslConfig,\n};\n\nexport const typeOrmAsyncConfig: TypeOrmModuleAsyncOptions = {\n imports: [ConfigModule],\n inject: [ConfigService],\n useFactory: async (): Promise<TypeOrmModuleOptions> => {\n console.log('ORM CONFIG', ormConfig);\n return ormConfig;\n },\n};\n\nexport default new DataSource(ormConfig);\n";
8
+ /**
9
+ * The v1 orm.config.ts with a PGlite branch for DATABASE_TYPE=pglite. Same
10
+ * exports and same behavior for every other database type.
11
+ */
12
+ export declare const ORM_CONFIG_V1_PGLITE = "import { ConfigModule, ConfigService } from '@nestjs/config';\nimport { DataSource } from 'typeorm';\nimport {\n TypeOrmModuleAsyncOptions,\n TypeOrmModuleOptions,\n} from '@nestjs/typeorm';\nimport { join } from 'path';\nimport AppConfig from './config';\n\n// DATABASE_TYPE=pglite runs on an in-process Postgres (PGlite) for local\n// checks: apso migrate verify, apso test smoke, and the in-browser editor.\n// PGLITE_DATA_DIR keeps the database on disk between commands.\nconst usePGlite = process.env.DATABASE_TYPE === 'pglite';\n\nconst sslConfig = process.env.DB_SSL\n ? {\n ssl: true,\n extra: {\n ssl: {\n rejectUnauthorized: false,\n },\n },\n }\n : {};\n\nconst ormConfig = {\n ...(usePGlite\n ? {\n type: 'postgres' as const,\n // Loaded only for PGlite so production installs never need it.\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n driver: new (require('typeorm-pglite').PGliteDriver)({\n dataDir: process.env.PGLITE_DATA_DIR,\n }).driver,\n }\n : {\n type: AppConfig.database.type as any,\n host: AppConfig.database.host,\n port: parseInt(AppConfig.database.port, 10),\n username: AppConfig.database.username,\n database: AppConfig.database.database,\n password: AppConfig.database.password,\n schema: AppConfig.database.schema,\n ...sslConfig,\n }),\n entities: [join(__dirname, '**', '*.entity.js')],\n migrations: [join(__dirname, '**', 'migrations/*-migration.js')],\n synchronize: AppConfig.database.synchronize,\n logging: AppConfig.database.logging,\n};\n\nexport const typeOrmAsyncConfig: TypeOrmModuleAsyncOptions = {\n imports: [ConfigModule],\n inject: [ConfigService],\n useFactory: async (): Promise<TypeOrmModuleOptions> => {\n console.log('ORM CONFIG', { ...ormConfig, driver: usePGlite ? '[PGliteDriver]' : undefined });\n return ormConfig;\n },\n};\n\nexport default new DataSource(ormConfig);\n";
13
+ /**
14
+ * Give a v1-template project PGlite support so the pre-deploy checks (apso
15
+ * migrate verify, apso test smoke) can run. Replaces src/orm.config.ts only
16
+ * when it is still the stock v1 file, so a customized config is never
17
+ * overwritten, and declares the PGlite packages.
18
+ *
19
+ * @param {string} projectDir - The service project root.
20
+ * @returns {boolean} True when orm.config.ts was upgraded.
21
+ */
22
+ export declare function upgradeOrmConfigForPglite(projectDir: string): boolean;
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.upgradeOrmConfigForPglite = exports.ORM_CONFIG_V1_PGLITE = exports.ORM_CONFIG_V1 = exports.PGLITE_DEPS = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const fs = tslib_1.__importStar(require("fs"));
6
+ const path = tslib_1.__importStar(require("path"));
7
+ const crud_deps_1 = require("./crud-deps");
8
+ /**
9
+ * PGlite packages the upgraded orm.config.ts loads. Versions match the pinned
10
+ * service-template-ts tag (see TEMPLATE_REFS in ./template.ts).
11
+ */
12
+ exports.PGLITE_DEPS = {
13
+ "@electric-sql/pglite": "^0.5.2",
14
+ "typeorm-pglite": "^0.3.2",
15
+ };
16
+ /** src/orm.config.ts as the v1 template shipped it (no PGlite support). */
17
+ exports.ORM_CONFIG_V1 = `import { ConfigModule, ConfigService } from '@nestjs/config';
18
+ import { DataSource } from 'typeorm';
19
+ import {
20
+ TypeOrmModuleAsyncOptions,
21
+ TypeOrmModuleOptions,
22
+ } from '@nestjs/typeorm';
23
+ import { join } from 'path';
24
+ import AppConfig from './config';
25
+
26
+ const sslConfig = process.env.DB_SSL
27
+ ? {
28
+ ssl: true,
29
+ extra: {
30
+ ssl: {
31
+ rejectUnauthorized: false,
32
+ },
33
+ },
34
+ }
35
+ : {};
36
+
37
+ const ormConfig = {
38
+ type: AppConfig.database.type as any,
39
+ host: AppConfig.database.host,
40
+ port: parseInt(AppConfig.database.port, 10),
41
+ username: AppConfig.database.username,
42
+ database: AppConfig.database.database,
43
+ password: AppConfig.database.password,
44
+ schema: AppConfig.database.schema,
45
+ entities: [join(__dirname, '**', '*.entity.js')],
46
+ migrations: [join(__dirname, '**', 'migrations/*-migration.js')],
47
+ synchronize: AppConfig.database.synchronize,
48
+ logging: AppConfig.database.logging,
49
+ ...sslConfig,
50
+ };
51
+
52
+ export const typeOrmAsyncConfig: TypeOrmModuleAsyncOptions = {
53
+ imports: [ConfigModule],
54
+ inject: [ConfigService],
55
+ useFactory: async (): Promise<TypeOrmModuleOptions> => {
56
+ console.log('ORM CONFIG', ormConfig);
57
+ return ormConfig;
58
+ },
59
+ };
60
+
61
+ export default new DataSource(ormConfig);
62
+ `;
63
+ /**
64
+ * The v1 orm.config.ts with a PGlite branch for DATABASE_TYPE=pglite. Same
65
+ * exports and same behavior for every other database type.
66
+ */
67
+ exports.ORM_CONFIG_V1_PGLITE = `import { ConfigModule, ConfigService } from '@nestjs/config';
68
+ import { DataSource } from 'typeorm';
69
+ import {
70
+ TypeOrmModuleAsyncOptions,
71
+ TypeOrmModuleOptions,
72
+ } from '@nestjs/typeorm';
73
+ import { join } from 'path';
74
+ import AppConfig from './config';
75
+
76
+ // DATABASE_TYPE=pglite runs on an in-process Postgres (PGlite) for local
77
+ // checks: apso migrate verify, apso test smoke, and the in-browser editor.
78
+ // PGLITE_DATA_DIR keeps the database on disk between commands.
79
+ const usePGlite = process.env.DATABASE_TYPE === 'pglite';
80
+
81
+ const sslConfig = process.env.DB_SSL
82
+ ? {
83
+ ssl: true,
84
+ extra: {
85
+ ssl: {
86
+ rejectUnauthorized: false,
87
+ },
88
+ },
89
+ }
90
+ : {};
91
+
92
+ const ormConfig = {
93
+ ...(usePGlite
94
+ ? {
95
+ type: 'postgres' as const,
96
+ // Loaded only for PGlite so production installs never need it.
97
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
98
+ driver: new (require('typeorm-pglite').PGliteDriver)({
99
+ dataDir: process.env.PGLITE_DATA_DIR,
100
+ }).driver,
101
+ }
102
+ : {
103
+ type: AppConfig.database.type as any,
104
+ host: AppConfig.database.host,
105
+ port: parseInt(AppConfig.database.port, 10),
106
+ username: AppConfig.database.username,
107
+ database: AppConfig.database.database,
108
+ password: AppConfig.database.password,
109
+ schema: AppConfig.database.schema,
110
+ ...sslConfig,
111
+ }),
112
+ entities: [join(__dirname, '**', '*.entity.js')],
113
+ migrations: [join(__dirname, '**', 'migrations/*-migration.js')],
114
+ synchronize: AppConfig.database.synchronize,
115
+ logging: AppConfig.database.logging,
116
+ };
117
+
118
+ export const typeOrmAsyncConfig: TypeOrmModuleAsyncOptions = {
119
+ imports: [ConfigModule],
120
+ inject: [ConfigService],
121
+ useFactory: async (): Promise<TypeOrmModuleOptions> => {
122
+ console.log('ORM CONFIG', { ...ormConfig, driver: usePGlite ? '[PGliteDriver]' : undefined });
123
+ return ormConfig;
124
+ },
125
+ };
126
+
127
+ export default new DataSource(ormConfig);
128
+ `;
129
+ const squash = (s) => s.replace(/\s+/g, "");
130
+ /**
131
+ * Give a v1-template project PGlite support so the pre-deploy checks (apso
132
+ * migrate verify, apso test smoke) can run. Replaces src/orm.config.ts only
133
+ * when it is still the stock v1 file, so a customized config is never
134
+ * overwritten, and declares the PGlite packages.
135
+ *
136
+ * @param {string} projectDir - The service project root.
137
+ * @returns {boolean} True when orm.config.ts was upgraded.
138
+ */
139
+ function upgradeOrmConfigForPglite(projectDir) {
140
+ const file = path.join(projectDir, "src", "orm.config.ts");
141
+ if (!fs.existsSync(file))
142
+ return false;
143
+ const current = fs.readFileSync(file, "utf8");
144
+ if (squash(current) === squash(exports.ORM_CONFIG_V1)) {
145
+ fs.writeFileSync(file, exports.ORM_CONFIG_V1_PGLITE);
146
+ (0, crud_deps_1.addMissingDeps)(projectDir, exports.PGLITE_DEPS);
147
+ return true;
148
+ }
149
+ // typeorm-pglite 0.3.4 takes an options object; given the data dir as a
150
+ // string it runs in memory, so each command starts from an empty database.
151
+ // { dataDir } works with 0.3.2 too.
152
+ const fixed = current.replace(/new PGliteDriver\(\s*process\.env\.PGLITE_DATA_DIR(?:\s*\|\|\s*undefined)?\s*\)/g, "new PGliteDriver({ dataDir: process.env.PGLITE_DATA_DIR })");
153
+ if (fixed === current)
154
+ return false;
155
+ fs.writeFileSync(file, fixed);
156
+ return true;
157
+ }
158
+ exports.upgradeOrmConfigForPglite = upgradeOrmConfigForPglite;
@@ -173,11 +173,13 @@ async function verifyMigration(projectDir, baselineApsorc, migrationFile, cliBin
173
173
  try {
174
174
  if (!done(await buildBaseline(projectDir, baselineApsorc, dataDir, cliBin)))
175
175
  return { ok: false, steps };
176
- if (!skipBuild && !done(step("build", await build(projectDir))))
177
- return { ok: false, steps };
178
176
  const compiled = path
179
177
  .join("dist", path.relative("src", migrationFile))
180
178
  .replace(/\.ts$/, ".js");
179
+ // --skip-build still builds when the migration isn't compiled yet.
180
+ const needsBuild = !skipBuild || !fs.existsSync(path.join(projectDir, compiled));
181
+ if (needsBuild && !done(step("build", await build(projectDir))))
182
+ return { ok: false, steps };
181
183
  const migrate = await run(process.execPath, ["-e", RUN_ONE_MIGRATION, compiled], projectDir, (0, exports.pgliteEnv)(dataDir));
182
184
  if (!done({ ...step("migrate", migrate), output: tail(migrate.output) }))
183
185
  return { ok: false, steps };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.41.2",
3
+ "version": "0.42.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@apso/cli",
9
- "version": "0.41.2",
9
+ "version": "0.42.1",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@apso/schema-tools": "^0.2.0",
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.41.2",
2
+ "version": "0.42.1",
3
3
  "commands": {
4
4
  "config": {
5
5
  "id": "config",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@apso/cli",
3
- "version": "0.41.2",
3
+ "version": "0.42.1",
4
4
  "mcpName": "io.github.apsoai/apso",
5
5
  "description": "Apso CLI",
6
6
  "author": "Apso by Mavric - @mavric",