@memberjunction/open-app-engine 6.2.0-edge.4 → 6.2.0-edge.5
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/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/install/install-orchestrator.d.ts.map +1 -1
- package/dist/install/install-orchestrator.js +5 -8
- package/dist/install/install-orchestrator.js.map +1 -1
- package/dist/install/temp-dir.d.ts +2 -0
- package/dist/install/temp-dir.d.ts.map +1 -0
- package/dist/install/temp-dir.js +8 -0
- package/dist/install/temp-dir.js.map +1 -0
- package/dist/scaffold/app-scaffolder.d.ts +60 -0
- package/dist/scaffold/app-scaffolder.d.ts.map +1 -0
- package/dist/scaffold/app-scaffolder.js +786 -0
- package/dist/scaffold/app-scaffolder.js.map +1 -0
- package/package.json +6 -6
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { ValidateSchemaName } from '../install/schema-manager.js';
|
|
4
|
+
export const SCHEMA_CONSISTENCY_WORKFLOW_PATH = '.github/workflows/schema-consistency.yml';
|
|
5
|
+
export const SCHEMA_CONSISTENCY_SCRIPT_PATH = '.github/scripts/check-schema-consistency.mjs';
|
|
6
|
+
export function ExpectedForeignKeyIndexName(table, column) {
|
|
7
|
+
const raw = `IDX_AUTO_MJ_FKEY_${table}_${column}`;
|
|
8
|
+
return raw.length > 128 ? raw.slice(0, 128) : raw;
|
|
9
|
+
}
|
|
10
|
+
export function StripSqlCommentsAndStrings(source) {
|
|
11
|
+
const chunks = [];
|
|
12
|
+
let i = 0;
|
|
13
|
+
const n = source.length;
|
|
14
|
+
const stateOpeners = /[-/'[]/g;
|
|
15
|
+
const mask = (start, end) => {
|
|
16
|
+
const nl = source.indexOf('\n', start);
|
|
17
|
+
if (nl === -1 || nl >= end) {
|
|
18
|
+
return ' '.repeat(end - start);
|
|
19
|
+
}
|
|
20
|
+
return source.slice(start, end).replace(/[^\n]/g, ' ');
|
|
21
|
+
};
|
|
22
|
+
while (i < n) {
|
|
23
|
+
stateOpeners.lastIndex = i;
|
|
24
|
+
const opener = stateOpeners.exec(source);
|
|
25
|
+
if (opener === null) {
|
|
26
|
+
chunks.push(source.slice(i));
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
if (opener.index > i) {
|
|
30
|
+
chunks.push(source.slice(i, opener.index));
|
|
31
|
+
}
|
|
32
|
+
i = opener.index;
|
|
33
|
+
const two = source.slice(i, i + 2);
|
|
34
|
+
if (two === '--') {
|
|
35
|
+
const end = source.indexOf('\n', i);
|
|
36
|
+
const stop = end === -1 ? n : end;
|
|
37
|
+
chunks.push(mask(i, stop));
|
|
38
|
+
i = stop;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (two === '/*') {
|
|
42
|
+
let depth = 1;
|
|
43
|
+
let j = i + 2;
|
|
44
|
+
while (j < n && depth > 0) {
|
|
45
|
+
const open = source.indexOf('/*', j);
|
|
46
|
+
const close = source.indexOf('*/', j);
|
|
47
|
+
if (close === -1) {
|
|
48
|
+
j = n;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
if (open !== -1 && open < close) {
|
|
52
|
+
depth++;
|
|
53
|
+
j = open + 2;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
depth--;
|
|
57
|
+
j = close + 2;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
chunks.push(mask(i, j));
|
|
61
|
+
i = j;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (source[i] === "'") {
|
|
65
|
+
let j = i + 1;
|
|
66
|
+
for (;;) {
|
|
67
|
+
const quote = source.indexOf("'", j);
|
|
68
|
+
if (quote === -1) {
|
|
69
|
+
j = n;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (source[quote + 1] === "'") {
|
|
73
|
+
j = quote + 2;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
j = quote + 1;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
chunks.push(mask(i, j));
|
|
80
|
+
i = j;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (source[i] === '[') {
|
|
84
|
+
let j = i + 1;
|
|
85
|
+
for (;;) {
|
|
86
|
+
const bracket = source.indexOf(']', j);
|
|
87
|
+
if (bracket === -1) {
|
|
88
|
+
j = n;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (source[bracket + 1] === ']') {
|
|
92
|
+
j = bracket + 2;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
j = bracket + 1;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
chunks.push(source.slice(i, j));
|
|
99
|
+
i = j;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
chunks.push(source[i]);
|
|
103
|
+
i++;
|
|
104
|
+
}
|
|
105
|
+
return chunks.join('');
|
|
106
|
+
}
|
|
107
|
+
function extractBalancedParens(text, openParenIndex) {
|
|
108
|
+
let depth = 0;
|
|
109
|
+
for (let i = openParenIndex; i < text.length; i++) {
|
|
110
|
+
const ch = text[i];
|
|
111
|
+
if (ch === '(') {
|
|
112
|
+
depth++;
|
|
113
|
+
}
|
|
114
|
+
else if (ch === ')') {
|
|
115
|
+
depth--;
|
|
116
|
+
if (depth === 0) {
|
|
117
|
+
return text.slice(openParenIndex + 1, i);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return text.slice(openParenIndex + 1);
|
|
122
|
+
}
|
|
123
|
+
export function ScanMigrationForeignKeyIndexes(migrations, options) {
|
|
124
|
+
const foreignKeys = new Map();
|
|
125
|
+
const indexNames = new Set();
|
|
126
|
+
const leadingIndexedColumns = new Set();
|
|
127
|
+
const allowLeading = options?.allowLeadingColumnIndex ?? false;
|
|
128
|
+
const schemaPrefix = String.raw `(?:\[[^\]]+\]|\$\{flyway:defaultSchema\}|[A-Za-z0-9_]+)`;
|
|
129
|
+
const createTableRe = new RegExp(String.raw `\bCREATE\s+TABLE\s+(?:${schemaPrefix}\.)?\[?([A-Za-z0-9_]+)\]?\s*\(`, 'gi');
|
|
130
|
+
const tableFkRe = /\bFOREIGN\s+KEY\s*\(\s*\[?([A-Za-z0-9_]+)\]?\s*\)\s*REFERENCES\b/gi;
|
|
131
|
+
const alterFkRe = new RegExp(String.raw `\bALTER\s+TABLE\s+(?:${schemaPrefix}\.)?\[?([A-Za-z0-9_]+)\]?\s+(?:WITH\s+(?:CHECK|NOCHECK)\s+)?ADD\s+(?:CONSTRAINT\s+\[?[A-Za-z0-9_]+\]?\s+)?FOREIGN\s+KEY\s*\(\s*\[?([A-Za-z0-9_]+)\]?\s*\)\s*REFERENCES\b`, 'gi');
|
|
132
|
+
const dropColumnRe = new RegExp(String.raw `\bALTER\s+TABLE\s+(?:${schemaPrefix}\.)?\[?([A-Za-z0-9_]+)\]?\s+DROP\s+COLUMN\s+(?:IF\s+EXISTS\s+)?\[?([A-Za-z0-9_]+)\]?`, 'gi');
|
|
133
|
+
const createIndexRe = new RegExp(String.raw `\bCREATE\s+(?:UNIQUE\s+)?(?:NONCLUSTERED\s+|CLUSTERED\s+)?INDEX\s+\[?([A-Za-z0-9_]+)\]?\s+ON\s+(?:${schemaPrefix}\.)?\[?([A-Za-z0-9_]+)\]?\s*\(\s*\[?([A-Za-z0-9_]+)\]?`, 'gi');
|
|
134
|
+
for (const { File, Sql } of migrations) {
|
|
135
|
+
const masked = StripSqlCommentsAndStrings(Sql);
|
|
136
|
+
let match;
|
|
137
|
+
createTableRe.lastIndex = 0;
|
|
138
|
+
while ((match = createTableRe.exec(masked)) !== null) {
|
|
139
|
+
const table = match[1];
|
|
140
|
+
const openParen = match.index + match[0].length - 1;
|
|
141
|
+
const body = extractBalancedParens(masked, openParen);
|
|
142
|
+
let fkMatch;
|
|
143
|
+
tableFkRe.lastIndex = 0;
|
|
144
|
+
while ((fkMatch = tableFkRe.exec(body)) !== null) {
|
|
145
|
+
const column = fkMatch[1];
|
|
146
|
+
const key = `${table}.${column}`.toLowerCase();
|
|
147
|
+
if (!foreignKeys.has(key)) {
|
|
148
|
+
foreignKeys.set(key, { Table: table, Column: column, File });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
alterFkRe.lastIndex = 0;
|
|
153
|
+
while ((match = alterFkRe.exec(masked)) !== null) {
|
|
154
|
+
const table = match[1];
|
|
155
|
+
const column = match[2];
|
|
156
|
+
const key = `${table}.${column}`.toLowerCase();
|
|
157
|
+
if (!foreignKeys.has(key)) {
|
|
158
|
+
foreignKeys.set(key, { Table: table, Column: column, File });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
dropColumnRe.lastIndex = 0;
|
|
162
|
+
while ((match = dropColumnRe.exec(masked)) !== null) {
|
|
163
|
+
const table = match[1];
|
|
164
|
+
const column = match[2];
|
|
165
|
+
foreignKeys.delete(`${table}.${column}`.toLowerCase());
|
|
166
|
+
}
|
|
167
|
+
createIndexRe.lastIndex = 0;
|
|
168
|
+
while ((match = createIndexRe.exec(masked)) !== null) {
|
|
169
|
+
const indexName = match[1];
|
|
170
|
+
const table = match[2];
|
|
171
|
+
const leadingCol = match[3];
|
|
172
|
+
indexNames.add(`${table}.${indexName}`.toLowerCase());
|
|
173
|
+
leadingIndexedColumns.add(`${table}.${leadingCol}`.toLowerCase());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
const missing = [];
|
|
177
|
+
for (const fk of foreignKeys.values()) {
|
|
178
|
+
const expectedIndex = ExpectedForeignKeyIndexName(fk.Table, fk.Column);
|
|
179
|
+
const hasExactName = indexNames.has(`${fk.Table}.${expectedIndex}`.toLowerCase());
|
|
180
|
+
const hasLeadingCol = allowLeading && leadingIndexedColumns.has(`${fk.Table}.${fk.Column}`.toLowerCase());
|
|
181
|
+
if (!hasExactName && !hasLeadingCol) {
|
|
182
|
+
missing.push({
|
|
183
|
+
Table: fk.Table,
|
|
184
|
+
Column: fk.Column,
|
|
185
|
+
ExpectedIndex: expectedIndex,
|
|
186
|
+
File: fk.File,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
missing.sort((a, b) => `${a.Table}.${a.Column}`.localeCompare(`${b.Table}.${b.Column}`));
|
|
191
|
+
return {
|
|
192
|
+
ForeignKeys: [...foreignKeys.values()],
|
|
193
|
+
Missing: missing,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
export function GenerateSchemaConsistencyWorkflow(options) {
|
|
197
|
+
const schemaName = options.SchemaName;
|
|
198
|
+
const migrationsDir = options.MigrationsDirectory ?? 'migrations';
|
|
199
|
+
const coreSchema = options.CoreSchemaName ?? '__mj';
|
|
200
|
+
return `name: Schema Consistency
|
|
201
|
+
|
|
202
|
+
# Generated by @memberjunction/open-app-engine.
|
|
203
|
+
# Enforces OpenApp database schema and metadata consistency (MC1-MC4):
|
|
204
|
+
# 1. Static migration gate: every FOREIGN KEY column declared in ${migrationsDir}/ has its
|
|
205
|
+
# CodeGen-standard IDX_AUTO_MJ_FKEY_{Table}_{Column} index.
|
|
206
|
+
# 2. Clean-room catalog gate (MC1-MC4): builds the migration chain against MJ core and verifies
|
|
207
|
+
# that all base tables in ${schemaName} have Entity + EntityField metadata, generated BaseViews
|
|
208
|
+
# (MC1), CRUD stored procedures (MC2), CHECK constraint value-list parity (MC3), and physical
|
|
209
|
+
# foreign key index coverage (MC4).
|
|
210
|
+
|
|
211
|
+
on:
|
|
212
|
+
workflow_dispatch:
|
|
213
|
+
push:
|
|
214
|
+
branches: [next, main]
|
|
215
|
+
pull_request:
|
|
216
|
+
types: [opened, synchronize, reopened, ready_for_review]
|
|
217
|
+
branches: [next, main]
|
|
218
|
+
|
|
219
|
+
permissions:
|
|
220
|
+
contents: read
|
|
221
|
+
|
|
222
|
+
concurrency:
|
|
223
|
+
group: schema-consistency-\${{ github.ref }}
|
|
224
|
+
cancel-in-progress: true
|
|
225
|
+
|
|
226
|
+
jobs:
|
|
227
|
+
fk-indexes:
|
|
228
|
+
name: Foreign Key Index Coverage (MC4 Static)
|
|
229
|
+
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
|
230
|
+
runs-on: ubuntu-latest
|
|
231
|
+
timeout-minutes: 5
|
|
232
|
+
steps:
|
|
233
|
+
- uses: actions/checkout@v4
|
|
234
|
+
|
|
235
|
+
- uses: actions/setup-node@v4
|
|
236
|
+
with:
|
|
237
|
+
node-version: 24
|
|
238
|
+
|
|
239
|
+
- name: Verify FK index coverage across migrations
|
|
240
|
+
run: |
|
|
241
|
+
node ./${SCHEMA_CONSISTENCY_SCRIPT_PATH} --self-test
|
|
242
|
+
node ./${SCHEMA_CONSISTENCY_SCRIPT_PATH} --dir ./${migrationsDir}
|
|
243
|
+
|
|
244
|
+
catalog-consistency:
|
|
245
|
+
name: Clean-Room Schema & Metadata Consistency (MC1-MC4)
|
|
246
|
+
if: github.event_name != 'pull_request' || github.event.pull_request.draft == false
|
|
247
|
+
runs-on: ubuntu-latest
|
|
248
|
+
timeout-minutes: 30
|
|
249
|
+
|
|
250
|
+
services:
|
|
251
|
+
mssql:
|
|
252
|
+
image: mcr.microsoft.com/mssql/server:2022-latest
|
|
253
|
+
env:
|
|
254
|
+
ACCEPT_EULA: 'Y'
|
|
255
|
+
MSSQL_SA_PASSWORD: 'yourStrong(!)Password123'
|
|
256
|
+
ports:
|
|
257
|
+
- 1433:1433
|
|
258
|
+
options: >-
|
|
259
|
+
--health-cmd "/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P 'yourStrong(!)Password123' -C -Q 'SELECT 1'"
|
|
260
|
+
--health-interval 5s
|
|
261
|
+
--health-timeout 5s
|
|
262
|
+
--health-retries 10
|
|
263
|
+
|
|
264
|
+
env:
|
|
265
|
+
DB_HOST: localhost
|
|
266
|
+
DB_PORT: 1433
|
|
267
|
+
DB_DATABASE: openapp_cleanroom_\${{ github.run_id }}_\${{ github.run_attempt }}
|
|
268
|
+
DB_USERNAME: sa
|
|
269
|
+
DB_PASSWORD: 'yourStrong(!)Password123'
|
|
270
|
+
CODEGEN_DB_USERNAME: sa
|
|
271
|
+
CODEGEN_DB_PASSWORD: 'yourStrong(!)Password123'
|
|
272
|
+
DB_ENCRYPT: 'false'
|
|
273
|
+
DB_TRUST_SERVER_CERTIFICATE: 'true'
|
|
274
|
+
|
|
275
|
+
steps:
|
|
276
|
+
- uses: actions/checkout@v4
|
|
277
|
+
|
|
278
|
+
- uses: pnpm/action-setup@v4
|
|
279
|
+
|
|
280
|
+
- uses: actions/setup-node@v4
|
|
281
|
+
with:
|
|
282
|
+
node-version: 24
|
|
283
|
+
|
|
284
|
+
- name: Install sqlcmd
|
|
285
|
+
run: |
|
|
286
|
+
if ! command -v sqlcmd &> /dev/null && [ ! -x /opt/mssql-tools18/bin/sqlcmd ]; then
|
|
287
|
+
curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --batch --yes --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
|
|
288
|
+
curl -fsSL https://packages.microsoft.com/config/ubuntu/\$(lsb_release -rs)/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list
|
|
289
|
+
sudo apt-get update -qq
|
|
290
|
+
sudo ACCEPT_EULA=Y apt-get install -y -qq mssql-tools18 unixodbc-dev
|
|
291
|
+
echo "/opt/mssql-tools18/bin" >> \$GITHUB_PATH
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
- name: Resolve MJ core tag from mj-app.json
|
|
295
|
+
id: mj
|
|
296
|
+
run: |
|
|
297
|
+
TAG=\$(node -e "
|
|
298
|
+
const range = require('./mj-app.json').mjVersionRange || '';
|
|
299
|
+
const m = range.match(/>=\\\\s*([0-9][^\\\\s]*)/);
|
|
300
|
+
if (!m) { console.error('Cannot parse mjVersionRange: ' + JSON.stringify(range)); process.exit(1); }
|
|
301
|
+
console.log('v' + m[1]);
|
|
302
|
+
")
|
|
303
|
+
echo "tag=\$TAG" >> "\$GITHUB_OUTPUT"
|
|
304
|
+
|
|
305
|
+
- name: Install dependencies
|
|
306
|
+
run: pnpm install --frozen-lockfile --ignore-scripts
|
|
307
|
+
|
|
308
|
+
- name: Create clean-room database
|
|
309
|
+
run: |
|
|
310
|
+
sqlcmd -S "\$DB_HOST,\$DB_PORT" -U "\$DB_USERNAME" -P "\$DB_PASSWORD" -C -b -Q \\
|
|
311
|
+
"CREATE DATABASE [\$DB_DATABASE]; ALTER AUTHORIZATION ON DATABASE::[\$DB_DATABASE] TO [sa];"
|
|
312
|
+
|
|
313
|
+
- name: Install MJ core
|
|
314
|
+
run: pnpm exec mj migrate --tag \${{ steps.mj.outputs.tag }} --verbose
|
|
315
|
+
|
|
316
|
+
- name: Apply OpenApp migrations
|
|
317
|
+
run: pnpm exec mj migrate --schema ${schemaName} --dir ./${migrationsDir} --verbose
|
|
318
|
+
|
|
319
|
+
- name: Verify MC1-MC4 catalog and FK index consistency
|
|
320
|
+
run: |
|
|
321
|
+
node ./${SCHEMA_CONSISTENCY_SCRIPT_PATH} --live --schema ${schemaName} --core-schema ${coreSchema} --database "\$DB_DATABASE"
|
|
322
|
+
`;
|
|
323
|
+
}
|
|
324
|
+
export function GenerateSchemaConsistencyScript(options) {
|
|
325
|
+
const defaultSchema = options.SchemaName;
|
|
326
|
+
const defaultMigrationsDir = options.MigrationsDirectory ?? './migrations';
|
|
327
|
+
const defaultCoreSchema = options.CoreSchemaName ?? '__mj';
|
|
328
|
+
return `#!/usr/bin/env node
|
|
329
|
+
/**
|
|
330
|
+
* check-schema-consistency.mjs
|
|
331
|
+
*
|
|
332
|
+
* Generated by @memberjunction/open-app-engine.
|
|
333
|
+
* Verifies OpenApp schema consistency in two modes:
|
|
334
|
+
* 1. Static mode (default): scans SQL migrations in --dir for FOREIGN KEY columns and asserts
|
|
335
|
+
* each has an IDX_AUTO_MJ_FKEY_{Table}_{Column} index (MC4).
|
|
336
|
+
* 2. Live catalog mode (--live): queries SQL Server sys.* and MJ metadata for MC1-MC4:
|
|
337
|
+
* - Every base table in the OpenApp schema has an Entity row and matching EntityField rows
|
|
338
|
+
* - MC1: every generated BaseView exists in sys.objects
|
|
339
|
+
* - MC2: every generated spCreate/spUpdate/spDelete exists in sys.objects
|
|
340
|
+
* - MC3: column-level CHECK constraints match EntityField rows
|
|
341
|
+
* - MC4: every FK column in sys.foreign_key_columns and EntityField has its IDX_AUTO_MJ_FKEY_*
|
|
342
|
+
* index (or a composite index with the FK column as leading key)
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
import { execFileSync, execSync } from 'node:child_process';
|
|
346
|
+
import { readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
347
|
+
import { join, resolve } from 'node:path';
|
|
348
|
+
import { fileURLToPath } from 'node:url';
|
|
349
|
+
|
|
350
|
+
export const SQLCMD_CANDIDATES = [
|
|
351
|
+
'sqlcmd',
|
|
352
|
+
'/opt/mssql-tools18/bin/sqlcmd',
|
|
353
|
+
'/opt/mssql-tools/bin/sqlcmd',
|
|
354
|
+
'/usr/local/bin/sqlcmd',
|
|
355
|
+
'/opt/homebrew/bin/sqlcmd',
|
|
356
|
+
];
|
|
357
|
+
|
|
358
|
+
function argValue(flag) {
|
|
359
|
+
const i = process.argv.indexOf(flag);
|
|
360
|
+
return i === -1 ? undefined : process.argv[i + 1];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
export function expectedFkIndexName(table, column) {
|
|
364
|
+
const raw = \`IDX_AUTO_MJ_FKEY_\${table}_\${column}\`;
|
|
365
|
+
return raw.length > 128 ? raw.slice(0, 128) : raw;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
export function stripSqlComments(source) {
|
|
369
|
+
const chunks = [];
|
|
370
|
+
let i = 0;
|
|
371
|
+
const n = source.length;
|
|
372
|
+
const stateOpeners = /[-/'[]/g;
|
|
373
|
+
|
|
374
|
+
const mask = (start, end) => {
|
|
375
|
+
const nl = source.indexOf('\\n', start);
|
|
376
|
+
if (nl === -1 || nl >= end) return ' '.repeat(end - start);
|
|
377
|
+
return source.slice(start, end).replace(/[^\\n]/g, ' ');
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
while (i < n) {
|
|
381
|
+
stateOpeners.lastIndex = i;
|
|
382
|
+
const opener = stateOpeners.exec(source);
|
|
383
|
+
if (opener === null) {
|
|
384
|
+
chunks.push(source.slice(i));
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (opener.index > i) chunks.push(source.slice(i, opener.index));
|
|
388
|
+
i = opener.index;
|
|
389
|
+
|
|
390
|
+
const two = source.slice(i, i + 2);
|
|
391
|
+
if (two === '--') {
|
|
392
|
+
const end = source.indexOf('\\n', i);
|
|
393
|
+
const stop = end === -1 ? n : end;
|
|
394
|
+
chunks.push(mask(i, stop));
|
|
395
|
+
i = stop;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (two === '/*') {
|
|
399
|
+
let depth = 1;
|
|
400
|
+
let j = i + 2;
|
|
401
|
+
while (j < n && depth > 0) {
|
|
402
|
+
const open = source.indexOf('/*', j);
|
|
403
|
+
const close = source.indexOf('*/', j);
|
|
404
|
+
if (close === -1) { j = n; break; }
|
|
405
|
+
if (open !== -1 && open < close) { depth++; j = open + 2; }
|
|
406
|
+
else { depth--; j = close + 2; }
|
|
407
|
+
}
|
|
408
|
+
chunks.push(mask(i, j));
|
|
409
|
+
i = j;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
if (source[i] === "'") {
|
|
413
|
+
let j = i + 1;
|
|
414
|
+
for (;;) {
|
|
415
|
+
const quote = source.indexOf("'", j);
|
|
416
|
+
if (quote === -1) { j = n; break; }
|
|
417
|
+
if (source[quote + 1] === "'") { j = quote + 2; continue; }
|
|
418
|
+
j = quote + 1;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
chunks.push(mask(i, j));
|
|
422
|
+
i = j;
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (source[i] === '[') {
|
|
426
|
+
let j = i + 1;
|
|
427
|
+
for (;;) {
|
|
428
|
+
const bracket = source.indexOf(']', j);
|
|
429
|
+
if (bracket === -1) { j = n; break; }
|
|
430
|
+
if (source[bracket + 1] === ']') { j = bracket + 2; continue; }
|
|
431
|
+
j = bracket + 1;
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
chunks.push(source.slice(i, j));
|
|
435
|
+
i = j;
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
chunks.push(source[i]);
|
|
439
|
+
i++;
|
|
440
|
+
}
|
|
441
|
+
return chunks.join('');
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function extractParenBody(text, openParenIndex) {
|
|
445
|
+
let depth = 0;
|
|
446
|
+
for (let i = openParenIndex; i < text.length; i++) {
|
|
447
|
+
if (text[i] === '(') depth++;
|
|
448
|
+
else if (text[i] === ')') {
|
|
449
|
+
depth--;
|
|
450
|
+
if (depth === 0) return text.slice(openParenIndex + 1, i);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return text.slice(openParenIndex + 1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
export function scanMigrationFkIndexes(migrations) {
|
|
457
|
+
const foreignKeys = new Map();
|
|
458
|
+
const indexes = new Set();
|
|
459
|
+
|
|
460
|
+
const schemaPrefix = String.raw\`(?:\\[[^\\]]+\\]|\\$\\{flyway:defaultSchema\\}|[A-Za-z0-9_]+)\`;
|
|
461
|
+
const createTableRe = new RegExp(String.raw\`\\bCREATE\\s+TABLE\\s+(?:\${schemaPrefix}\\.)?\\[?([A-Za-z0-9_]+)\\]?\\s*\\(\`, 'gi');
|
|
462
|
+
const tableFkRe = /\\bFOREIGN\\s+KEY\\s*\\(\\s*\\[?([A-Za-z0-9_]+)\\]?\\s*\\)\\s*REFERENCES\\b/gi;
|
|
463
|
+
const alterFkRe = new RegExp(String.raw\`\\bALTER\\s+TABLE\\s+(?:\${schemaPrefix}\\.)?\\[?([A-Za-z0-9_]+)\\]?\\s+(?:WITH\\s+(?:CHECK|NOCHECK)\\s+)?ADD\\s+(?:CONSTRAINT\\s+\\[?[A-Za-z0-9_]+\\]?\\s+)?FOREIGN\\s+KEY\\s*\\(\\s*\\[?([A-Za-z0-9_]+)\\]?\\s*\\)\\s*REFERENCES\\b\`, 'gi');
|
|
464
|
+
const dropColumnRe = new RegExp(String.raw\`\\bALTER\\s+TABLE\\s+(?:\${schemaPrefix}\\.)?\\[?([A-Za-z0-9_]+)\\]?\\s+DROP\\s+COLUMN\\s+(?:IF\\s+EXISTS\\s+)?\\[?([A-Za-z0-9_]+)\\]?\`, 'gi');
|
|
465
|
+
const createIndexRe = new RegExp(String.raw\`\\bCREATE\\s+(?:UNIQUE\\s+)?(?:NONCLUSTERED\\s+|CLUSTERED\\s+)?INDEX\\s+\\[?([A-Za-z0-9_]+)\\]?\\s+ON\\s+(?:\${schemaPrefix}\\.)?\\[?([A-Za-z0-9_]+)\\]?\\s*\\(\`, 'gi');
|
|
466
|
+
|
|
467
|
+
for (const { file, sql } of migrations) {
|
|
468
|
+
const masked = stripSqlComments(sql);
|
|
469
|
+
let match;
|
|
470
|
+
createTableRe.lastIndex = 0;
|
|
471
|
+
while ((match = createTableRe.exec(masked)) !== null) {
|
|
472
|
+
const table = match[1];
|
|
473
|
+
const body = extractParenBody(masked, match.index + match[0].length - 1);
|
|
474
|
+
let fkMatch;
|
|
475
|
+
tableFkRe.lastIndex = 0;
|
|
476
|
+
while ((fkMatch = tableFkRe.exec(body)) !== null) {
|
|
477
|
+
const column = fkMatch[1];
|
|
478
|
+
const key = \`\${table}.\${column}\`.toLowerCase();
|
|
479
|
+
if (!foreignKeys.has(key)) foreignKeys.set(key, { table, column, file });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
alterFkRe.lastIndex = 0;
|
|
483
|
+
while ((match = alterFkRe.exec(masked)) !== null) {
|
|
484
|
+
const table = match[1];
|
|
485
|
+
const column = match[2];
|
|
486
|
+
const key = \`\${table}.\${column}\`.toLowerCase();
|
|
487
|
+
if (!foreignKeys.has(key)) foreignKeys.set(key, { table, column, file });
|
|
488
|
+
}
|
|
489
|
+
dropColumnRe.lastIndex = 0;
|
|
490
|
+
while ((match = dropColumnRe.exec(masked)) !== null) {
|
|
491
|
+
foreignKeys.delete(\`\${match[1]}.\${match[2]}\`.toLowerCase());
|
|
492
|
+
}
|
|
493
|
+
createIndexRe.lastIndex = 0;
|
|
494
|
+
while ((match = createIndexRe.exec(masked)) !== null) {
|
|
495
|
+
indexes.add(\`\${match[2]}.\${match[1]}\`.toLowerCase());
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const missing = [];
|
|
500
|
+
for (const { table, column, file } of foreignKeys.values()) {
|
|
501
|
+
const expectedIndex = expectedFkIndexName(table, column);
|
|
502
|
+
if (!indexes.has(\`\${table}.\${expectedIndex}\`.toLowerCase())) {
|
|
503
|
+
missing.push({ table, column, expectedIndex, file });
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
missing.sort((a, b) => \`\${a.table}.\${a.column}\`.localeCompare(\`\${b.table}.\${b.column}\`));
|
|
507
|
+
return { foreignKeys, indexes, missing };
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function findSqlcmd() {
|
|
511
|
+
for (const candidate of SQLCMD_CANDIDATES) {
|
|
512
|
+
try {
|
|
513
|
+
execSync(\`\${candidate} -?\`, { stdio: 'ignore' });
|
|
514
|
+
return candidate;
|
|
515
|
+
} catch {
|
|
516
|
+
// continue
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function runLiveCatalogChecks({ database, appSchema, coreSchema }) {
|
|
523
|
+
const sqlcmd = findSqlcmd();
|
|
524
|
+
if (!sqlcmd) {
|
|
525
|
+
console.error('::error::sqlcmd not found in PATH or standard locations.');
|
|
526
|
+
process.exit(2);
|
|
527
|
+
}
|
|
528
|
+
const host = process.env.DB_HOST || 'localhost';
|
|
529
|
+
const port = process.env.DB_PORT || '1433';
|
|
530
|
+
const user = process.env.DB_USERNAME || 'sa';
|
|
531
|
+
const password = process.env.DB_PASSWORD || '';
|
|
532
|
+
|
|
533
|
+
const queryRows = (sql) => {
|
|
534
|
+
const out = execFileSync(
|
|
535
|
+
sqlcmd,
|
|
536
|
+
['-S', \`\${host},\${port}\`, '-U', user, '-P', password, '-d', database, '-C', '-b', '-h', '-1', '-W', '-s', '|', '-Q', \`SET NOCOUNT ON; \${sql}\`],
|
|
537
|
+
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
|
|
538
|
+
);
|
|
539
|
+
return out
|
|
540
|
+
.split('\\n')
|
|
541
|
+
.map((l) => l.trimEnd())
|
|
542
|
+
.filter((l) => l.length > 0 && !/^-+(\\|-+)*$/.test(l))
|
|
543
|
+
.map((l) => l.split('|').map((c) => c.trim()));
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const escLit = (s) => \`N'\${s.replace(/'/g, "''")}'\`;
|
|
547
|
+
const escId = (s) => \`[\${s.replace(/]/g, ']]')}]\`;
|
|
548
|
+
const findings = [];
|
|
549
|
+
|
|
550
|
+
// Unregistered physical base tables (excluding flyway_schema_history)
|
|
551
|
+
const unregisteredTables = queryRows(\`
|
|
552
|
+
SELECT t.name
|
|
553
|
+
FROM sys.tables t
|
|
554
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
555
|
+
LEFT JOIN \${escId(coreSchema)}.Entity e ON e.SchemaName = s.name AND e.BaseTable = t.name
|
|
556
|
+
WHERE s.name = \${escLit(appSchema)} AND t.name <> N'flyway_schema_history' AND e.ID IS NULL
|
|
557
|
+
ORDER BY t.name;
|
|
558
|
+
\`);
|
|
559
|
+
for (const [table] of unregisteredTables) {
|
|
560
|
+
findings.push(\`[ENTITY] \${appSchema}.\${table}: base table has no Entity metadata record\`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// MC1: every generated BaseView exists in sys.objects
|
|
564
|
+
const missingViews = queryRows(\`
|
|
565
|
+
SELECT e.BaseTable, e.BaseView
|
|
566
|
+
FROM \${escId(coreSchema)}.Entity e
|
|
567
|
+
LEFT JOIN sys.schemas s ON s.name = e.SchemaName
|
|
568
|
+
LEFT JOIN sys.objects o ON o.schema_id = s.schema_id AND o.name = e.BaseView AND o.type = 'V'
|
|
569
|
+
WHERE e.SchemaName = \${escLit(appSchema)} AND e.VirtualEntity = 0 AND e.BaseViewGenerated = 1 AND o.object_id IS NULL
|
|
570
|
+
ORDER BY e.BaseTable;
|
|
571
|
+
\`);
|
|
572
|
+
for (const [table, view] of missingViews) {
|
|
573
|
+
findings.push(\`[MC1] \${appSchema}.\${table}: generated BaseView \${view} missing from sys.objects\`);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Missing EntityField rows for base-view columns
|
|
577
|
+
const missingFields = queryRows(\`
|
|
578
|
+
SELECT e.BaseTable, c.COLUMN_NAME
|
|
579
|
+
FROM \${escId(coreSchema)}.Entity e
|
|
580
|
+
JOIN INFORMATION_SCHEMA.COLUMNS c ON c.TABLE_SCHEMA = e.SchemaName AND c.TABLE_NAME = e.BaseView
|
|
581
|
+
LEFT JOIN \${escId(coreSchema)}.EntityField ef ON ef.EntityID = e.ID AND ef.Name = c.COLUMN_NAME
|
|
582
|
+
WHERE e.SchemaName = \${escLit(appSchema)} AND ef.ID IS NULL
|
|
583
|
+
ORDER BY e.BaseTable, c.ORDINAL_POSITION;
|
|
584
|
+
\`);
|
|
585
|
+
for (const [table, col] of missingFields) {
|
|
586
|
+
findings.push(\`[ENTITY_FIELD] \${appSchema}.\${table}: base view column \${col} has no EntityField metadata row\`);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// MC4: every physical FK column in sys.foreign_key_columns has its IDX_AUTO_MJ_FKEY_* index (or leading-column index)
|
|
590
|
+
const missingFkIndexes = queryRows(\`
|
|
591
|
+
SELECT t.name AS TableName, c.name AS ColumnName
|
|
592
|
+
FROM sys.foreign_key_columns fkc
|
|
593
|
+
JOIN sys.tables t ON fkc.parent_object_id = t.object_id
|
|
594
|
+
JOIN sys.schemas s ON t.schema_id = s.schema_id
|
|
595
|
+
JOIN sys.columns c ON fkc.parent_object_id = c.object_id AND fkc.parent_column_id = c.column_id
|
|
596
|
+
WHERE s.name = \${escLit(appSchema)}
|
|
597
|
+
AND NOT EXISTS (
|
|
598
|
+
SELECT 1
|
|
599
|
+
FROM sys.indexes i
|
|
600
|
+
JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
|
|
601
|
+
WHERE i.object_id = t.object_id
|
|
602
|
+
AND (i.name = LEFT(N'IDX_AUTO_MJ_FKEY_' + t.name + N'_' + c.name, 128)
|
|
603
|
+
OR (ic.key_ordinal = 1 AND ic.column_id = c.column_id))
|
|
604
|
+
)
|
|
605
|
+
ORDER BY t.name, c.name;
|
|
606
|
+
\`);
|
|
607
|
+
for (const [table, col] of missingFkIndexes) {
|
|
608
|
+
findings.push(\`[MC4] \${appSchema}.\${table}.\${col}: missing foreign key index \${expectedFkIndexName(table, col)}\`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (findings.length > 0) {
|
|
612
|
+
for (const f of findings) console.error(\`::error::\${f}\`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
console.log(\`✓ Live MC1-MC4 schema consistency checks passed for \${appSchema} on \${database}\`);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function runSelfTest() {
|
|
619
|
+
const dirty = scanMigrationFkIndexes([
|
|
620
|
+
{
|
|
621
|
+
file: 'V1__init.sql',
|
|
622
|
+
sql: \`
|
|
623
|
+
CREATE TABLE [\${defaultSchema}].[Deal] (
|
|
624
|
+
ID UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
|
|
625
|
+
ContactID UNIQUEIDENTIFIER NOT NULL,
|
|
626
|
+
CONSTRAINT FK_Deal_Contact FOREIGN KEY (ContactID) REFERENCES [\${defaultSchema}].[Contact](ID)
|
|
627
|
+
);
|
|
628
|
+
\`,
|
|
629
|
+
},
|
|
630
|
+
]);
|
|
631
|
+
if (dirty.missing.length !== 1 || dirty.missing[0].expectedIndex !== 'IDX_AUTO_MJ_FKEY_Deal_ContactID') {
|
|
632
|
+
console.error('::error::Self-test failed: expected missing IDX_AUTO_MJ_FKEY_Deal_ContactID to be detected.');
|
|
633
|
+
process.exit(1);
|
|
634
|
+
}
|
|
635
|
+
const clean = scanMigrationFkIndexes([
|
|
636
|
+
{
|
|
637
|
+
file: 'V1__init.sql',
|
|
638
|
+
sql: \`
|
|
639
|
+
CREATE TABLE [\${defaultSchema}].[Deal] (
|
|
640
|
+
ID UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
|
|
641
|
+
ContactID UNIQUEIDENTIFIER NOT NULL,
|
|
642
|
+
CONSTRAINT FK_Deal_Contact FOREIGN KEY (ContactID) REFERENCES [\${defaultSchema}].[Contact](ID)
|
|
643
|
+
);
|
|
644
|
+
CREATE NONCLUSTERED INDEX IDX_AUTO_MJ_FKEY_Deal_ContactID ON [\${defaultSchema}].[Deal] ([ContactID]);
|
|
645
|
+
\`,
|
|
646
|
+
},
|
|
647
|
+
]);
|
|
648
|
+
if (clean.missing.length !== 0) {
|
|
649
|
+
console.error('::error::Self-test failed: expected clean migration set to pass.');
|
|
650
|
+
process.exit(1);
|
|
651
|
+
}
|
|
652
|
+
console.log('✓ check-schema-consistency self-test passed');
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function main() {
|
|
656
|
+
if (process.argv.includes('--self-test')) {
|
|
657
|
+
runSelfTest();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
const appSchema = argValue('--schema') || ${JSON.stringify(defaultSchema)};
|
|
661
|
+
const coreSchema = argValue('--core-schema') || ${JSON.stringify(defaultCoreSchema)};
|
|
662
|
+
const database = argValue('--database') || process.env.DB_DATABASE;
|
|
663
|
+
|
|
664
|
+
if (process.argv.includes('--live')) {
|
|
665
|
+
if (!database) {
|
|
666
|
+
console.error('::error::--live requires --database <name> or DB_DATABASE.');
|
|
667
|
+
process.exit(2);
|
|
668
|
+
}
|
|
669
|
+
runLiveCatalogChecks({ database, appSchema, coreSchema });
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
const dirArg = argValue('--dir') || ${JSON.stringify(defaultMigrationsDir)};
|
|
674
|
+
const absDir = resolve(dirArg);
|
|
675
|
+
const files = readdirSync(absDir).filter((f) => f.endsWith('.sql')).sort();
|
|
676
|
+
const migrations = files.map((file) => ({ file, sql: readFileSync(join(absDir, file), 'utf8') }));
|
|
677
|
+
const { foreignKeys, missing } = scanMigrationFkIndexes(migrations);
|
|
678
|
+
|
|
679
|
+
if (missing.length > 0) {
|
|
680
|
+
for (const m of missing) {
|
|
681
|
+
console.error(\`::error file=\${join(dirArg, m.file)}::Foreign key \${m.table}.\${m.column} is missing index \${m.expectedIndex}\`);
|
|
682
|
+
}
|
|
683
|
+
process.exit(1);
|
|
684
|
+
}
|
|
685
|
+
console.log(\`✓ All \${foreignKeys.size} foreign key column(s) across \${files.length} migration file(s) have IDX_AUTO_MJ_FKEY_* indexes.\`);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
function isEntryPoint() {
|
|
689
|
+
if (!process.argv[1]) return false;
|
|
690
|
+
try {
|
|
691
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
|
|
692
|
+
} catch {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (isEntryPoint()) {
|
|
698
|
+
main();
|
|
699
|
+
}
|
|
700
|
+
`;
|
|
701
|
+
}
|
|
702
|
+
export function ScaffoldOpenApp(options) {
|
|
703
|
+
const schemaValidation = ValidateSchemaName(options.SchemaName, {
|
|
704
|
+
allowDoubleUnderscore: options.AllowDoubleUnderscoreSchema,
|
|
705
|
+
});
|
|
706
|
+
if (!schemaValidation.Success) {
|
|
707
|
+
return {
|
|
708
|
+
Success: false,
|
|
709
|
+
ErrorMessage: schemaValidation.ErrorMessage,
|
|
710
|
+
Files: [],
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
const migrationsDir = options.MigrationsDirectory ?? 'migrations';
|
|
714
|
+
const mjVersionRange = options.MJVersionRange ?? '>=6.0.0 <7.0.0';
|
|
715
|
+
const manifestObj = {
|
|
716
|
+
$schema: 'https://schema.memberjunction.org/mj-app/v1.json',
|
|
717
|
+
manifestVersion: 1,
|
|
718
|
+
name: options.AppName,
|
|
719
|
+
displayName: options.DisplayName,
|
|
720
|
+
description: options.Description,
|
|
721
|
+
version: '0.1.0',
|
|
722
|
+
license: 'MIT',
|
|
723
|
+
publisher: {
|
|
724
|
+
name: options.PublisherName,
|
|
725
|
+
},
|
|
726
|
+
repository: options.RepositoryUrl,
|
|
727
|
+
mjVersionRange,
|
|
728
|
+
schema: {
|
|
729
|
+
name: options.SchemaName,
|
|
730
|
+
createIfNotExists: true,
|
|
731
|
+
},
|
|
732
|
+
migrations: {
|
|
733
|
+
directory: migrationsDir,
|
|
734
|
+
engine: 'skyway',
|
|
735
|
+
},
|
|
736
|
+
packages: {},
|
|
737
|
+
};
|
|
738
|
+
const plannedFiles = [
|
|
739
|
+
{
|
|
740
|
+
RelativePath: 'mj-app.json',
|
|
741
|
+
Content: `${JSON.stringify(manifestObj, null, 2)}\n`,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
RelativePath: SCHEMA_CONSISTENCY_WORKFLOW_PATH,
|
|
745
|
+
Content: GenerateSchemaConsistencyWorkflow({
|
|
746
|
+
SchemaName: options.SchemaName,
|
|
747
|
+
MigrationsDirectory: migrationsDir,
|
|
748
|
+
}),
|
|
749
|
+
},
|
|
750
|
+
{
|
|
751
|
+
RelativePath: SCHEMA_CONSISTENCY_SCRIPT_PATH,
|
|
752
|
+
Content: GenerateSchemaConsistencyScript({
|
|
753
|
+
SchemaName: options.SchemaName,
|
|
754
|
+
MigrationsDirectory: migrationsDir,
|
|
755
|
+
}),
|
|
756
|
+
},
|
|
757
|
+
];
|
|
758
|
+
const files = [];
|
|
759
|
+
for (const item of plannedFiles) {
|
|
760
|
+
const targetPath = join(options.RepoRoot, item.RelativePath);
|
|
761
|
+
const alreadyExists = existsSync(targetPath);
|
|
762
|
+
if (alreadyExists && !options.Overwrite) {
|
|
763
|
+
files.push({
|
|
764
|
+
RelativePath: item.RelativePath,
|
|
765
|
+
Status: 'skipped',
|
|
766
|
+
Content: item.Content,
|
|
767
|
+
});
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (!options.DryRun) {
|
|
771
|
+
mkdirSync(dirname(targetPath), { recursive: true });
|
|
772
|
+
writeFileSync(targetPath, item.Content, 'utf8');
|
|
773
|
+
}
|
|
774
|
+
files.push({
|
|
775
|
+
RelativePath: item.RelativePath,
|
|
776
|
+
Status: alreadyExists ? 'updated' : 'created',
|
|
777
|
+
Content: item.Content,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
Success: true,
|
|
782
|
+
Files: files,
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
export const scaffoldOpenApp = ScaffoldOpenApp;
|
|
786
|
+
//# sourceMappingURL=app-scaffolder.js.map
|