@restforgejs/platform 5.1.7 → 5.1.20
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/bin/restforge-hwinfo-linux +0 -0
- package/bin/restforge-hwinfo.exe +0 -0
- package/build-info.json +2 -2
- package/cli/consumer-deploy.js +1 -1
- package/cli/consumer.js +1 -1
- package/generators/cli/catalog/dbschema.js +2 -1
- package/generators/cli/endpoint/list.js +264 -0
- package/generators/cli/fast-track.js +395 -37
- package/generators/cli/payload/generate.js +10 -2
- package/generators/cli/processor/create.js +7 -7
- package/generators/cli/processor/list.js +229 -0
- package/generators/cli/schema/apply.js +6 -1
- package/generators/cli/schema/diff.js +6 -1
- package/generators/cli/schema/introspect.js +32 -11
- package/generators/lib/data/db-executor.js +8 -8
- package/generators/lib/data/envelope.js +3 -3
- package/generators/lib/dbschema-kit/apply-engine.js +20 -0
- package/generators/lib/dbschema-kit/dialect/mysql.js +2 -0
- package/generators/lib/dbschema-kit/dialect/oracle.js +2 -0
- package/generators/lib/dbschema-kit/dialect/postgres.js +4 -0
- package/generators/lib/dbschema-kit/dialect/sqlite.js +5 -0
- package/generators/lib/dbschema-kit/diff-engine.js +22 -1
- package/generators/lib/dbschema-kit/diff-reporter.js +293 -272
- package/generators/lib/dbschema-kit/emitters/create-index.js +23 -1
- package/generators/lib/dbschema-kit/emitters/create-table.js +48 -0
- package/generators/lib/dbschema-kit/introspect-mapper.js +154 -2
- package/generators/lib/dbschema-kit/ir-builder.js +84 -1
- package/generators/lib/dbschema-kit/schema-printer.js +20 -0
- package/generators/lib/dbschema-kit/soft-delete-constants.js +111 -0
- package/generators/lib/dbschema-kit/validator/schema-validator.js +231 -0
- package/generators/lib/generators/dashboard-generator.js +5 -5
- package/generators/lib/generators/processor-validation-generator.js +16 -16
- package/generators/lib/payload/payload-runner.js +774 -1
- package/generators/lib/payload/schema-diff.js +7 -0
- package/generators/lib/templates/dashboard-catalog.js +1 -1
- package/generators/lib/templates/db-connection-env.js +1 -1
- package/generators/lib/templates/dbschema-catalog.js +1 -1
- package/generators/lib/templates/field-validation-catalog.js +1 -1
- package/generators/lib/templates/mysql-template.js +1 -1
- package/generators/lib/templates/oracle-template.js +1 -1
- package/generators/lib/templates/postgres-template.js +1 -1
- package/generators/lib/templates/query-declarative-catalog.js +1 -1
- package/generators/lib/templates/sqlite-template.js +1 -1
- package/generators/lib/utils/database-introspector.js +48 -0
- package/generators/lib/utils/env-manager.js +4 -4
- package/generators/lib/utils/file-utils.js +6 -6
- package/generators/lib/utils/payload-processor.js +18 -2
- package/generators/lib/validators/argument-validator.js +2 -2
- package/generators/lib/validators/dashboard-validator.js +35 -1
- package/generators/lib/validators/payload-validator.js +460 -33
- package/integrity-manifest.json +20 -20
- package/package.json +2 -1
- package/scripts/check-install.js +8 -8
- package/scripts/verify-integrity.js +1 -1
- package/server.js +1 -1
- package/src/components/handlers/adjust_handler.js +1 -1
- package/src/components/handlers/audit_handler.js +1 -1
- package/src/components/handlers/delete_handler.js +1 -1
- package/src/components/handlers/export_handler.js +1 -1
- package/src/components/handlers/import_handler.js +1 -1
- package/src/components/handlers/insert_handler.js +1 -1
- package/src/components/handlers/update_handler.js +1 -1
- package/src/components/handlers/upload_handler.js +1 -1
- package/src/components/handlers/workflow_handler.js +1 -1
- package/src/components/integrations/webhook.js +1 -1
- package/src/consumers/baseConsumer.js +1 -1
- package/src/consumers/declarativeMapper.js +1 -1
- package/src/consumers/handlers/apiHandler.js +1 -1
- package/src/consumers/handlers/consoleHandler.js +1 -1
- package/src/consumers/handlers/databaseHandler.js +1 -1
- package/src/consumers/handlers/index.js +1 -1
- package/src/consumers/handlers/kafkaHandler.js +1 -1
- package/src/consumers/index.js +1 -1
- package/src/consumers/messageTransformer.js +1 -1
- package/src/consumers/validator.js +1 -1
- package/src/core/db/dialect/base-dialect.js +1 -1
- package/src/core/db/dialect/index.js +1 -1
- package/src/core/db/dialect/mysql-dialect.js +1 -1
- package/src/core/db/dialect/oracle-dialect.js +1 -1
- package/src/core/db/dialect/postgres-dialect.js +1 -1
- package/src/core/db/dialect/sqlite-dialect.js +1 -1
- package/src/core/db/flatten-helper.js +1 -1
- package/src/core/db/query-builder-error.js +1 -1
- package/src/core/db/query-builder.js +1 -1
- package/src/core/db/relation-helper.js +1 -1
- package/src/core/handlers/delete_handler.js +1 -1
- package/src/core/handlers/insert_handler.js +1 -1
- package/src/core/handlers/update_handler.js +1 -1
- package/src/core/models/base-model.js +1 -1
- package/src/core/utils/cache-manager.js +1 -1
- package/src/core/utils/component-engine.js +1 -1
- package/src/core/utils/context-builder.js +1 -1
- package/src/core/utils/datetime-formatter.js +1 -1
- package/src/core/utils/datetime-parser.js +1 -1
- package/src/core/utils/db.js +1 -1
- package/src/core/utils/logger.js +1 -1
- package/src/core/utils/payload-loader.js +1 -1
- package/src/core/utils/security-checks.js +1 -1
- package/src/middleware/body-options.js +1 -1
- package/src/middleware/cors.js +1 -1
- package/src/middleware/idempotency.js +1 -1
- package/src/middleware/rate-limiter.js +1 -1
- package/src/middleware/request-logger.js +1 -1
- package/src/middleware/security-headers.js +1 -1
- package/src/models/base-model-mysql.js +1 -1
- package/src/models/base-model-oracle.js +1 -1
- package/src/models/base-model-sqlite.js +1 -1
- package/src/models/base-model.js +1 -1
- package/src/pro/caching/redis-client.js +1 -1
- package/src/pro/caching/redis-helper.js +1 -1
- package/src/pro/consumers/baseConsumer.js +1 -1
- package/src/pro/consumers/declarativeMapper.js +1 -1
- package/src/pro/consumers/handlers/apiHandler.js +1 -1
- package/src/pro/consumers/handlers/consoleHandler.js +1 -1
- package/src/pro/consumers/handlers/databaseHandler.js +1 -1
- package/src/pro/consumers/handlers/index.js +1 -1
- package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
- package/src/pro/consumers/index.js +1 -1
- package/src/pro/consumers/messageTransformer.js +1 -1
- package/src/pro/consumers/validator.js +1 -1
- package/src/pro/database/base-model-mysql.js +1 -1
- package/src/pro/database/base-model-oracle.js +1 -1
- package/src/pro/database/base-model-sqlite.js +1 -1
- package/src/pro/database/db-mysql.js +1 -1
- package/src/pro/database/db-oracle.js +1 -1
- package/src/pro/database/db-sqlite.js +1 -1
- package/src/pro/excel/excel-generator.js +1 -1
- package/src/pro/excel/excel-parser.js +1 -1
- package/src/pro/excel/export-service.js +1 -1
- package/src/pro/excel/export_handler.js +1 -1
- package/src/pro/excel/import-service.js +1 -1
- package/src/pro/excel/import-validator.js +1 -1
- package/src/pro/excel/import_handler.js +1 -1
- package/src/pro/excel/upsert-builder.js +1 -1
- package/src/pro/idgen/idgen-routes.js +1 -1
- package/src/pro/integrations/lookup-resolver.js +1 -1
- package/src/pro/integrations/upload-handler-v2.js +1 -1
- package/src/pro/integrations/upload-handler.js +1 -1
- package/src/pro/integrations/webhook.js +1 -1
- package/src/pro/locking/lock-routes.js +1 -1
- package/src/pro/locking/resource-lock-manager.js +1 -1
- package/src/pro/messaging/kafkaConsumerService.js +1 -1
- package/src/pro/messaging/kafkaService.js +1 -1
- package/src/pro/messaging/messagehubService.js +1 -1
- package/src/pro/messaging/rabbitmqService.js +1 -1
- package/src/pro/scheduler/job-manager.js +1 -1
- package/src/pro/scheduler/job-routes.js +1 -1
- package/src/pro/scheduler/job-validator.js +1 -1
- package/src/pro/storage/base-storage-provider.js +1 -1
- package/src/pro/storage/file-metadata-helper.js +1 -1
- package/src/pro/storage/index.js +1 -1
- package/src/pro/storage/local-storage-provider.js +1 -1
- package/src/pro/storage/s3-storage-provider.js +1 -1
- package/src/pro/storage/upload-cleanup-job.js +1 -1
- package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
- package/src/pro/storage/upload-pending-tracker.js +1 -1
- package/src/pro/websocket/broadcast-helper.js +1 -1
- package/src/pro/websocket/index.js +1 -1
- package/src/pro/websocket/livesync-server.js +1 -1
- package/src/pro/websocket/ws-broadcaster.js +1 -1
- package/src/services/export-service.js +1 -1
- package/src/services/import-service.js +1 -1
- package/src/services/kafkaConsumerService.js +1 -1
- package/src/services/kafkaService.js +1 -1
- package/src/services/messagehubService.js +1 -1
- package/src/services/rabbitmqService.js +1 -1
- package/src/utils/cache-invalidation-registry.js +1 -1
- package/src/utils/cache-manager.js +1 -1
- package/src/utils/component-engine.js +1 -1
- package/src/utils/config-extractor.js +1 -1
- package/src/utils/consumerLogger.js +1 -1
- package/src/utils/context-builder.js +1 -1
- package/src/utils/dashboard-helpers.js +1 -1
- package/src/utils/dateHelper.js +1 -1
- package/src/utils/datetime-formatter.js +1 -1
- package/src/utils/datetime-parser.js +1 -1
- package/src/utils/db-bootstrap.js +1 -1
- package/src/utils/db-mysql.js +1 -1
- package/src/utils/db-oracle.js +1 -1
- package/src/utils/db-sqlite.js +1 -1
- package/src/utils/db.js +1 -1
- package/src/utils/demo-generator.js +1 -1
- package/src/utils/excel-generator.js +1 -1
- package/src/utils/excel-parser.js +1 -1
- package/src/utils/file-watcher.js +1 -1
- package/src/utils/id-generator.js +1 -1
- package/src/utils/idempotency-manager.js +1 -1
- package/src/utils/import-validator.js +1 -1
- package/src/utils/license-client.js +1 -1
- package/src/utils/lock-manager.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/lookup-resolver.js +1 -1
- package/src/utils/payload-loader.js +1 -1
- package/src/utils/processor-response.js +1 -1
- package/src/utils/rabbitmq.js +1 -1
- package/src/utils/redis-client.js +1 -1
- package/src/utils/redis-helper.js +1 -1
- package/src/utils/request-scope.js +1 -1
- package/src/utils/security-checks.js +1 -1
- package/src/utils/service-resolver.js +1 -1
- package/src/utils/shutdown-coordinator.js +1 -1
- package/src/utils/soft-delete-dashboard-guard.js +1 -0
- package/src/utils/sql-table-extractor.js +1 -0
- package/src/utils/trusted-keys.js +1 -1
- package/src/utils/upload-handler.js +1 -1
- package/src/utils/upsert-builder.js +1 -1
- package/src/utils/workflow-hook-executor.js +1 -1
- package/generators/lib/utils/sql-table-extractor.js +0 -83
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const {
|
|
4
|
+
SOFT_DELETE_COLUMNS,
|
|
5
|
+
SOFT_DELETE_COLUMN_TYPES,
|
|
6
|
+
SOFT_DELETE_SUFFIX_LENGTH
|
|
7
|
+
} = require('../soft-delete-constants');
|
|
8
|
+
|
|
3
9
|
const VALID_RELATION_TYPES = new Set(['belongsTo', 'hasOne', 'hasMany']);
|
|
4
10
|
const VALID_REFERENTIAL_ACTIONS = new Set(['cascade', 'restrict', 'setNull', 'noAction']);
|
|
5
11
|
const COMPARISON_OPS = new Set(['gt', 'gte', 'lt', 'lte']);
|
|
@@ -7,6 +13,14 @@ const AUTO_UPDATE_VALID_TYPES = new Set(['timestamp', 'date']);
|
|
|
7
13
|
const TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/;
|
|
8
14
|
const AUTO_SUFFIX = '_auto';
|
|
9
15
|
|
|
16
|
+
// --- Soft-delete (Fase 1) -------------------------------------------------
|
|
17
|
+
// Nama kolom (R3), kontrak tipe (R5), dan suffix length (R7) berasal dari modul shared
|
|
18
|
+
// `soft-delete-constants` (sumber kebenaran tunggal, dipakai juga oleh emitter DDL Phase 02
|
|
19
|
+
// dan jalur reverse introspect Phase 03). Kontrak tipe forward (R5) di validator ini dan
|
|
20
|
+
// kontrak tipe reverse di introspect-mapper kini membaca satu konstanta yang sama.
|
|
21
|
+
// Reusable hanya didukung untuk tipe string (varchar/text) karena value-mutation (R9).
|
|
22
|
+
const REUSABLE_STRING_TYPES = new Set(['string', 'text']);
|
|
23
|
+
|
|
10
24
|
function validateSchema(ir) {
|
|
11
25
|
validateTableLevel(ir);
|
|
12
26
|
validateSchemaName(ir);
|
|
@@ -15,6 +29,7 @@ function validateSchema(ir) {
|
|
|
15
29
|
validateIndexAndUniqueReferences(ir);
|
|
16
30
|
validateChecks(ir);
|
|
17
31
|
validateAutoUpdate(ir);
|
|
32
|
+
validateSoftDelete(ir);
|
|
18
33
|
}
|
|
19
34
|
|
|
20
35
|
function prefix(ir, msg) {
|
|
@@ -207,4 +222,220 @@ function validateAutoUpdate(ir) {
|
|
|
207
222
|
}
|
|
208
223
|
}
|
|
209
224
|
|
|
225
|
+
// --- Soft-delete invariants (R3, R5, R7, R9) ------------------------------
|
|
226
|
+
//
|
|
227
|
+
// Predikat tunggal ini dipanggil dari validateSchema, sehingga seluruh command
|
|
228
|
+
// schema yang memuat SDF lewat defineModel (validate/migrate/diff/apply) menegakkan
|
|
229
|
+
// invariant yang sama tanpa ada jalur yang bisa melewatinya (R4).
|
|
230
|
+
//
|
|
231
|
+
// Catatan bahasa pesan: mengikuti gaya validator existing yang seluruhnya berbahasa
|
|
232
|
+
// Inggris dengan prefix `Table '<name>': ...`. Struktur multi-baris (mis. format
|
|
233
|
+
// declared/required pada R7) mengikuti contoh plan master.
|
|
234
|
+
function validateSoftDelete(ir) {
|
|
235
|
+
const { fields, softDelete } = ir;
|
|
236
|
+
|
|
237
|
+
const enabled = softDelete !== undefined && softDelete !== null && softDelete.enabled === true;
|
|
238
|
+
const presentColumns = SOFT_DELETE_COLUMNS.filter((col) => fields[col] !== undefined);
|
|
239
|
+
|
|
240
|
+
// R3 (arah balik): kolom soft-delete ada tetapi enabled bukan true → ERROR.
|
|
241
|
+
if (!enabled) {
|
|
242
|
+
if (presentColumns.length > 0) {
|
|
243
|
+
throw new Error(prefix(
|
|
244
|
+
ir,
|
|
245
|
+
`soft-delete columns ${presentColumns.join(', ')} are declared in 'fields', ` +
|
|
246
|
+
`but 'softDelete.enabled' is not true. ` +
|
|
247
|
+
`Either declare 'softDelete.enabled = true', or remove these columns.`
|
|
248
|
+
));
|
|
249
|
+
}
|
|
250
|
+
// Tabel normal: tidak ada blok softDelete dan tidak ada kolom soft-delete. OK.
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// R3 (arah maju): enabled = true tetapi ada kolom yang hilang → ERROR.
|
|
255
|
+
const missing = SOFT_DELETE_COLUMNS.filter((col) => fields[col] === undefined);
|
|
256
|
+
if (missing.length > 0) {
|
|
257
|
+
throw new Error(prefix(
|
|
258
|
+
ir,
|
|
259
|
+
`softDelete.enabled = true requires all soft-delete columns in 'fields', missing: ${missing.join(', ')}. ` +
|
|
260
|
+
`Declare is_deleted (boolean), deleted_at (timestamp), deleted_by (string), or set softDelete.enabled = false.`
|
|
261
|
+
));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// R5: validasi tipe kolom (hanya saat soft-delete aktif dan ketiga kolom ada).
|
|
265
|
+
for (const col of SOFT_DELETE_COLUMNS) {
|
|
266
|
+
const expected = SOFT_DELETE_COLUMN_TYPES[col];
|
|
267
|
+
const actual = fields[col].type;
|
|
268
|
+
if (actual !== expected) {
|
|
269
|
+
throw new Error(prefix(
|
|
270
|
+
ir,
|
|
271
|
+
`soft-delete column '${col}' must be type '${expected}', got '${actual}'.`
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// R7 + R9: validasi tiap field reusable.
|
|
277
|
+
if (softDelete.reusable !== undefined) {
|
|
278
|
+
for (const entry of softDelete.reusable) {
|
|
279
|
+
validateReusableEntry(ir, entry);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// R21: tolak generate bila ada UNIQUE yang tak punya jalur reusable (composite,
|
|
284
|
+
// atau single-column non-string). Hanya tercapai saat enabled === true.
|
|
285
|
+
validateSoftDeleteUniqueReusability(ir);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function validateReusableEntry(ir, entry) {
|
|
289
|
+
const { fields, uniques } = ir;
|
|
290
|
+
const fieldName = entry.field;
|
|
291
|
+
|
|
292
|
+
// Entry shape: harus mereferensi field lewat nama.
|
|
293
|
+
if (typeof fieldName !== 'string' || fieldName === '') {
|
|
294
|
+
throw new Error(prefix(
|
|
295
|
+
ir,
|
|
296
|
+
`softDelete.reusable entry must have a 'field' (non-empty string), got '${fieldName}'.`
|
|
297
|
+
));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// R9.1: field reusable harus ada di `fields`.
|
|
301
|
+
const field = fields[fieldName];
|
|
302
|
+
if (field === undefined) {
|
|
303
|
+
throw new Error(prefix(
|
|
304
|
+
ir,
|
|
305
|
+
`softDelete.reusable references unknown field '${fieldName}' (not declared in 'fields').`
|
|
306
|
+
));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// R7: base length wajib dan harus integer positif.
|
|
310
|
+
const baseLength = entry.length;
|
|
311
|
+
if (baseLength === undefined || baseLength === null) {
|
|
312
|
+
throw new Error(prefix(
|
|
313
|
+
ir,
|
|
314
|
+
`softDelete.reusable field '${fieldName}' is missing required 'length' (the base business-code length).`
|
|
315
|
+
));
|
|
316
|
+
}
|
|
317
|
+
if (typeof baseLength !== 'number' || !Number.isInteger(baseLength) || baseLength <= 0) {
|
|
318
|
+
throw new Error(prefix(
|
|
319
|
+
ir,
|
|
320
|
+
`softDelete.reusable field '${fieldName}' has invalid 'length' (must be a positive integer), got '${baseLength}'.`
|
|
321
|
+
));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// R9.3: reusable harus bertipe string (varchar/text).
|
|
325
|
+
if (!REUSABLE_STRING_TYPES.has(field.type)) {
|
|
326
|
+
throw new Error(prefix(
|
|
327
|
+
ir,
|
|
328
|
+
`softDelete.reusable declares '${fieldName}' as reusable, but its column type is '${field.type}'. ` +
|
|
329
|
+
`Reusable only supports string types (varchar/text) because it uses value-mutation '{code}##{uuidv7}'. ` +
|
|
330
|
+
`Change the column type to string, or remove '${fieldName}' from reusable.`
|
|
331
|
+
));
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// R9.2: reusable harus punya UNIQUE single-column (composite ditolak).
|
|
335
|
+
const uniqueState = resolveUniqueState(fieldName, field, uniques);
|
|
336
|
+
if (uniqueState === 'none') {
|
|
337
|
+
throw new Error(prefix(
|
|
338
|
+
ir,
|
|
339
|
+
`softDelete.reusable field '${fieldName}' must have a single-column UNIQUE constraint, but none was found.`
|
|
340
|
+
));
|
|
341
|
+
}
|
|
342
|
+
if (uniqueState === 'composite') {
|
|
343
|
+
throw new Error(prefix(
|
|
344
|
+
ir,
|
|
345
|
+
`softDelete.reusable field '${fieldName}' only participates in a composite UNIQUE constraint. ` +
|
|
346
|
+
`Reusable requires a single-column UNIQUE (a composite UNIQUE is a technical integrity rule, not a reusable business code).`
|
|
347
|
+
));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// R7: lebar fisik kolom harus muat base length + suffix.
|
|
351
|
+
// Hanya berlaku untuk `string` (punya length tetap); `text` tidak terbatas panjangnya.
|
|
352
|
+
if (field.type === 'string') {
|
|
353
|
+
const required = baseLength + SOFT_DELETE_SUFFIX_LENGTH;
|
|
354
|
+
if (field.length < required) {
|
|
355
|
+
throw new Error(prefix(
|
|
356
|
+
ir,
|
|
357
|
+
`Reusable unique column '${fieldName}' is too narrow for the soft-delete suffix:\n` +
|
|
358
|
+
` declared : string:${field.length}\n` +
|
|
359
|
+
` required : string:${required} (base ${baseLength} + suffix ${SOFT_DELETE_SUFFIX_LENGTH})`
|
|
360
|
+
));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Tentukan status UNIQUE sebuah field: 'single' (single-column unique, valid untuk
|
|
366
|
+
// reusable), 'composite' (hanya bagian composite unique → ditolak R9.2), atau 'none'.
|
|
367
|
+
// Sumber unique: shorthand `unique` (field.unique === true) maupun ir.uniques.
|
|
368
|
+
function resolveUniqueState(fieldName, field, uniques) {
|
|
369
|
+
if (field.unique === true) return 'single';
|
|
370
|
+
let hasComposite = false;
|
|
371
|
+
for (const uq of uniques) {
|
|
372
|
+
if (!Array.isArray(uq.columns) || !uq.columns.includes(fieldName)) continue;
|
|
373
|
+
if (uq.columns.length === 1) return 'single';
|
|
374
|
+
hasComposite = true;
|
|
375
|
+
}
|
|
376
|
+
return hasComposite ? 'composite' : 'none';
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// R21: gate pra-generate untuk soft-delete dengan UNIQUE tak ter-reusable.
|
|
380
|
+
//
|
|
381
|
+
// Mutasi nilai '{code}##{uuidv7}' yang membebaskan UNIQUE dari bentrok saat re-create
|
|
382
|
+
// baris HANYA berlaku untuk UNIQUE single-column bertipe string (R9.2 + R9.3). UNIQUE
|
|
383
|
+
// jenis lain tetap PENUH/non-partial (emitter create-index.js R10/R11 sengaja tidak
|
|
384
|
+
// mem-partial-kan UNIQUE: uniqueness tetap penuh), sehingga membuat ulang baris dengan
|
|
385
|
+
// kunci sama setelah soft-delete akan melanggar UNIQUE tanpa jalur pembebasan. Dua bentuk
|
|
386
|
+
// tanpa jalur reusable:
|
|
387
|
+
// (1) composite UNIQUE (lebih dari satu kolom);
|
|
388
|
+
// (2) single-column UNIQUE bertipe non-string (date/number/uuid/...).
|
|
389
|
+
// Keduanya ditolak di generate-time. Gate murni JS pada IR → identik untuk semua dialect
|
|
390
|
+
// (agnostic, deterministik); tidak menyentuh DDL/index/tipe kolom.
|
|
391
|
+
//
|
|
392
|
+
// Hanya dipanggil dari validateSoftDelete setelah blok enabled (jadi hanya saat
|
|
393
|
+
// softDelete.enabled === true). Throw pada pelanggaran pertama dengan urutan stabil:
|
|
394
|
+
// sumber A (shorthand `unique`, urut Object.keys(fields)) lalu sumber B (ir.uniques).
|
|
395
|
+
function validateSoftDeleteUniqueReusability(ir) {
|
|
396
|
+
const { fields, uniques } = ir;
|
|
397
|
+
|
|
398
|
+
// Sumber A: UNIQUE single-column via shorthand `unique` (field.unique === true).
|
|
399
|
+
for (const fieldName of Object.keys(fields)) {
|
|
400
|
+
const field = fields[fieldName];
|
|
401
|
+
if (field.unique !== true) continue;
|
|
402
|
+
if (!REUSABLE_STRING_TYPES.has(field.type)) {
|
|
403
|
+
throw new Error(softDeleteNonStringUniqueMessage(ir, fieldName, field.type));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Sumber B: UNIQUE dari ir.uniques (composite maupun single-column).
|
|
408
|
+
for (const uq of uniques) {
|
|
409
|
+
const columns = Array.isArray(uq.columns) ? uq.columns : [];
|
|
410
|
+
if (columns.length > 1) {
|
|
411
|
+
throw new Error(prefix(
|
|
412
|
+
ir,
|
|
413
|
+
`softDelete.enabled = true, but the table has a composite UNIQUE (${columns.join(', ')}) ` +
|
|
414
|
+
`that cannot be freed by reusable value-mutation on soft-delete. ` +
|
|
415
|
+
`Re-creating a row with the same key after soft-delete would violate this UNIQUE. ` +
|
|
416
|
+
`Use hard-delete for process-driven tables (balances/ledgers/snapshots/period-close), ` +
|
|
417
|
+
`or make the only UNIQUE a single-column string business code.`
|
|
418
|
+
));
|
|
419
|
+
}
|
|
420
|
+
if (columns.length === 1) {
|
|
421
|
+
const col = columns[0];
|
|
422
|
+
const field = fields[col];
|
|
423
|
+
// Eksistensi kolom sudah dijamin validateIndexAndUniqueReferences; cek defensif.
|
|
424
|
+
if (field !== undefined && !REUSABLE_STRING_TYPES.has(field.type)) {
|
|
425
|
+
throw new Error(softDeleteNonStringUniqueMessage(ir, col, field.type));
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function softDeleteNonStringUniqueMessage(ir, column, type) {
|
|
432
|
+
return prefix(
|
|
433
|
+
ir,
|
|
434
|
+
`softDelete.enabled = true, but single-column UNIQUE '${column}' is type '${type}', ` +
|
|
435
|
+
`which cannot be freed by reusable value-mutation on soft-delete (reusable requires string/text). ` +
|
|
436
|
+
`Re-creating a row with the same value after soft-delete would violate this UNIQUE. ` +
|
|
437
|
+
`Use hard-delete, or change the unique key to a single-column string business code.`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
|
|
210
441
|
module.exports = { validateSchema };
|
|
@@ -60,7 +60,7 @@ class DashboardGenerator {
|
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
62
|
* Tulis file module dashboard ke src/modules/{project}/dash-{name}.js.
|
|
63
|
-
* SQL di-embed langsung sebagai template literal
|
|
63
|
+
* SQL di-embed langsung sebagai template literal — tidak ada SQL file copy.
|
|
64
64
|
*
|
|
65
65
|
* @param {string} projectName
|
|
66
66
|
* @param {string} dashboardName
|
|
@@ -141,8 +141,8 @@ class DashboardGenerator {
|
|
|
141
141
|
/**
|
|
142
142
|
* Baca file SQL dari disk (saat generation time), trim trailing whitespace,
|
|
143
143
|
* lalu escape karakter yang berbahaya di template literal JS:
|
|
144
|
-
* - backtick (`)
|
|
145
|
-
* - dollar-brace (${)
|
|
144
|
+
* - backtick (`) → \`
|
|
145
|
+
* - dollar-brace (${) → \${
|
|
146
146
|
*
|
|
147
147
|
* Comment SQL dibiarkan apa adanya (defensive: bisa berisi info dialect /
|
|
148
148
|
* output columns yang berguna saat audit).
|
|
@@ -380,8 +380,8 @@ module.exports = router;
|
|
|
380
380
|
/**
|
|
381
381
|
* Render block cache set yang disisip setelah loop normalisasi (lowercaseKeysDeep
|
|
382
382
|
* + stringifyNumericDeep) dan sebelum `res.json(response)`. TTL literal:
|
|
383
|
-
* - bila cacheConfig.ttl numeric
|
|
384
|
-
* - bila cacheConfig.ttl null
|
|
383
|
+
* - bila cacheConfig.ttl numeric → emit angka literal
|
|
384
|
+
* - bila cacheConfig.ttl null → emit `null` agar `cacheManager.set`
|
|
385
385
|
* fallback ke env CACHE_TTL.
|
|
386
386
|
*/
|
|
387
387
|
static _renderCacheSetBlock(cacheConfig) {
|
|
@@ -34,10 +34,10 @@ const FORMAT_REGEX = {
|
|
|
34
34
|
};
|
|
35
35
|
|
|
36
36
|
const FORMAT_MESSAGES = {
|
|
37
|
-
'email': '
|
|
38
|
-
'url': '
|
|
39
|
-
'phone-id': '
|
|
40
|
-
'uuid': '
|
|
37
|
+
'email': 'must be a valid email address',
|
|
38
|
+
'url': 'must be a valid URL',
|
|
39
|
+
'phone-id': 'must be a valid Indonesian phone number',
|
|
40
|
+
'uuid': 'must be a valid UUID'
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
/**
|
|
@@ -89,7 +89,7 @@ function buildFieldCheckBlock(inputField, messagePrefix, required, followUpCheck
|
|
|
89
89
|
if (required) {
|
|
90
90
|
checks.push({
|
|
91
91
|
condition: `input['${safeField}'] === undefined || input['${safeField}'] === null || input['${safeField}'] === ''`,
|
|
92
|
-
message: `${messagePrefix}
|
|
92
|
+
message: `${messagePrefix} is required`
|
|
93
93
|
});
|
|
94
94
|
}
|
|
95
95
|
for (const check of followUpChecks) {
|
|
@@ -128,47 +128,47 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
|
|
|
128
128
|
case 'string':
|
|
129
129
|
checks.push({
|
|
130
130
|
condition: `typeof input['${safeField}'] !== 'string'`,
|
|
131
|
-
message: `${messagePrefix}
|
|
131
|
+
message: `${messagePrefix} must be a string`
|
|
132
132
|
});
|
|
133
133
|
break;
|
|
134
134
|
case 'number':
|
|
135
135
|
checks.push({
|
|
136
136
|
condition: `typeof input['${safeField}'] !== 'number' && isNaN(Number(input['${safeField}']))`,
|
|
137
|
-
message: `${messagePrefix}
|
|
137
|
+
message: `${messagePrefix} must be a number`
|
|
138
138
|
});
|
|
139
139
|
break;
|
|
140
140
|
case 'integer':
|
|
141
141
|
checks.push({
|
|
142
142
|
condition: `!Number.isInteger(Number(input['${safeField}']))`,
|
|
143
|
-
message: `${messagePrefix}
|
|
143
|
+
message: `${messagePrefix} must be an integer`
|
|
144
144
|
});
|
|
145
145
|
break;
|
|
146
146
|
case 'boolean':
|
|
147
147
|
checks.push({
|
|
148
148
|
condition: `typeof input['${safeField}'] !== 'boolean' && input['${safeField}'] !== 'true' && input['${safeField}'] !== 'false'`,
|
|
149
|
-
message: `${messagePrefix}
|
|
149
|
+
message: `${messagePrefix} must be a boolean`
|
|
150
150
|
});
|
|
151
151
|
break;
|
|
152
152
|
case 'uuid':
|
|
153
153
|
checks.push({
|
|
154
154
|
condition: `typeof input['${safeField}'] !== 'string'`,
|
|
155
|
-
message: `${messagePrefix}
|
|
155
|
+
message: `${messagePrefix} must be a string`
|
|
156
156
|
});
|
|
157
157
|
checks.push({
|
|
158
158
|
condition: `!${FORMAT_REGEX.uuid}.test(input['${safeField}'])`,
|
|
159
|
-
message: `${messagePrefix}
|
|
159
|
+
message: `${messagePrefix} must be a valid UUID`
|
|
160
160
|
});
|
|
161
161
|
break;
|
|
162
162
|
case 'array':
|
|
163
163
|
checks.push({
|
|
164
164
|
condition: `!Array.isArray(input['${safeField}'])`,
|
|
165
|
-
message: `${messagePrefix}
|
|
165
|
+
message: `${messagePrefix} must be an array`
|
|
166
166
|
});
|
|
167
167
|
break;
|
|
168
168
|
case 'object':
|
|
169
169
|
checks.push({
|
|
170
170
|
condition: `typeof input['${safeField}'] !== 'object' || Array.isArray(input['${safeField}'])`,
|
|
171
|
-
message: `${messagePrefix}
|
|
171
|
+
message: `${messagePrefix} must be an object`
|
|
172
172
|
});
|
|
173
173
|
break;
|
|
174
174
|
default:
|
|
@@ -193,7 +193,7 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
|
|
|
193
193
|
const listLiteral = `[${safeValues.join(', ')}]`;
|
|
194
194
|
checks.push({
|
|
195
195
|
condition: `!${listLiteral}.includes(input['${safeField}'])`,
|
|
196
|
-
message: `${messagePrefix}
|
|
196
|
+
message: `${messagePrefix} is not one of the allowed values`
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
}
|
|
@@ -202,13 +202,13 @@ function buildFollowUpChecks(inputField, messagePrefix, fieldDef) {
|
|
|
202
202
|
if (fieldDef && Number.isInteger(fieldDef.minLength) && fieldDef.minLength >= 0) {
|
|
203
203
|
checks.push({
|
|
204
204
|
condition: `typeof input['${safeField}'] === 'string' && input['${safeField}'].length < ${fieldDef.minLength}`,
|
|
205
|
-
message: `${messagePrefix}
|
|
205
|
+
message: `${messagePrefix} must be at least ${fieldDef.minLength} characters`
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
if (fieldDef && Number.isInteger(fieldDef.maxLength) && fieldDef.maxLength >= 0) {
|
|
209
209
|
checks.push({
|
|
210
210
|
condition: `typeof input['${safeField}'] === 'string' && input['${safeField}'].length > ${fieldDef.maxLength}`,
|
|
211
|
-
message: `${messagePrefix}
|
|
211
|
+
message: `${messagePrefix} must be at most ${fieldDef.maxLength} characters`
|
|
212
212
|
});
|
|
213
213
|
}
|
|
214
214
|
|