@restforgejs/platform 4.3.5 → 5.0.0

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.
Files changed (191) hide show
  1. package/bin/sdf-tools.exe +0 -0
  2. package/build-info.json +2 -2
  3. package/cli/consumer-deploy.js +1 -1
  4. package/cli/consumer.js +1 -1
  5. package/generators/cli/payload/migrate.js +96 -0
  6. package/generators/lib/dbschema-kit/apply-engine.js +211 -46
  7. package/generators/lib/dbschema-kit/diff-engine.js +14 -2
  8. package/generators/lib/dbschema-kit/emitters/alter-table.js +96 -2
  9. package/generators/lib/dbschema-kit/introspect-mapper.js +9 -0
  10. package/generators/lib/migrate/backend-payload-migrator.js +221 -0
  11. package/generators/lib/migrate/field-type-resolver.js +319 -0
  12. package/generators/lib/migrate/label-generator.js +38 -0
  13. package/generators/lib/migrate/migrate-runner.js +187 -0
  14. package/generators/lib/migrate/naming.js +43 -0
  15. package/generators/lib/migrate/sql-parser.js +124 -0
  16. package/generators/lib/payload/payload-runner.js +106 -11
  17. package/generators/lib/templates/dashboard-catalog.js +1 -1
  18. package/generators/lib/templates/db-connection-env.js +1 -1
  19. package/generators/lib/templates/dbschema-catalog.js +1 -1
  20. package/generators/lib/templates/field-validation-catalog.js +1 -1
  21. package/generators/lib/templates/mysql-template.js +1 -1
  22. package/generators/lib/templates/oracle-template.js +1 -1
  23. package/generators/lib/templates/postgres-template.js +1 -1
  24. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  25. package/generators/lib/templates/sqlite-template.js +1 -1
  26. package/integrity-manifest.json +18 -18
  27. package/node_modules/brace-expansion/index.js +1 -1
  28. package/node_modules/brace-expansion/package.json +1 -1
  29. package/node_modules/dayjs/CHANGELOG.md +7 -0
  30. package/node_modules/dayjs/README.md +12 -10
  31. package/node_modules/dayjs/dayjs.min.js +1 -1
  32. package/node_modules/dayjs/esm/constant.js +1 -1
  33. package/node_modules/dayjs/esm/plugin/duration/index.js +5 -4
  34. package/node_modules/dayjs/locale.json +1 -1
  35. package/node_modules/dayjs/package.json +2 -2
  36. package/node_modules/dayjs/plugin/duration.js +1 -1
  37. package/node_modules/tmp/lib/tmp.js +37 -7
  38. package/node_modules/tmp/package.json +4 -16
  39. package/package.json +1 -1
  40. package/scripts/verify-integrity.js +1 -1
  41. package/server.js +1 -1
  42. package/src/components/handlers/adjust_handler.js +1 -1
  43. package/src/components/handlers/audit_handler.js +1 -1
  44. package/src/components/handlers/delete_handler.js +1 -1
  45. package/src/components/handlers/export_handler.js +1 -1
  46. package/src/components/handlers/import_handler.js +1 -1
  47. package/src/components/handlers/insert_handler.js +1 -1
  48. package/src/components/handlers/update_handler.js +1 -1
  49. package/src/components/handlers/upload_handler.js +1 -1
  50. package/src/components/handlers/workflow_handler.js +1 -1
  51. package/src/components/integrations/webhook.js +1 -1
  52. package/src/consumers/baseConsumer.js +1 -1
  53. package/src/consumers/declarativeMapper.js +1 -1
  54. package/src/consumers/handlers/apiHandler.js +1 -1
  55. package/src/consumers/handlers/consoleHandler.js +1 -1
  56. package/src/consumers/handlers/databaseHandler.js +1 -1
  57. package/src/consumers/handlers/index.js +1 -1
  58. package/src/consumers/handlers/kafkaHandler.js +1 -1
  59. package/src/consumers/index.js +1 -1
  60. package/src/consumers/messageTransformer.js +1 -1
  61. package/src/consumers/validator.js +1 -1
  62. package/src/core/db/dialect/base-dialect.js +1 -1
  63. package/src/core/db/dialect/index.js +1 -1
  64. package/src/core/db/dialect/mysql-dialect.js +1 -1
  65. package/src/core/db/dialect/oracle-dialect.js +1 -1
  66. package/src/core/db/dialect/postgres-dialect.js +1 -1
  67. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  68. package/src/core/db/flatten-helper.js +1 -1
  69. package/src/core/db/query-builder-error.js +1 -1
  70. package/src/core/db/query-builder.js +1 -1
  71. package/src/core/db/relation-helper.js +1 -1
  72. package/src/core/handlers/delete_handler.js +1 -1
  73. package/src/core/handlers/insert_handler.js +1 -1
  74. package/src/core/handlers/update_handler.js +1 -1
  75. package/src/core/models/base-model.js +1 -1
  76. package/src/core/utils/cache-manager.js +1 -1
  77. package/src/core/utils/component-engine.js +1 -1
  78. package/src/core/utils/context-builder.js +1 -1
  79. package/src/core/utils/datetime-formatter.js +1 -1
  80. package/src/core/utils/datetime-parser.js +1 -1
  81. package/src/core/utils/db.js +1 -1
  82. package/src/core/utils/logger.js +1 -1
  83. package/src/core/utils/payload-loader.js +1 -1
  84. package/src/core/utils/security-checks.js +1 -1
  85. package/src/middleware/body-options.js +1 -1
  86. package/src/middleware/cors.js +1 -1
  87. package/src/middleware/idempotency.js +1 -1
  88. package/src/middleware/rate-limiter.js +1 -1
  89. package/src/middleware/request-logger.js +1 -1
  90. package/src/middleware/security-headers.js +1 -1
  91. package/src/models/base-model-mysql.js +1 -1
  92. package/src/models/base-model-oracle.js +1 -1
  93. package/src/models/base-model-sqlite.js +1 -1
  94. package/src/models/base-model.js +1 -1
  95. package/src/pro/caching/redis-client.js +1 -1
  96. package/src/pro/caching/redis-helper.js +1 -1
  97. package/src/pro/consumers/baseConsumer.js +1 -1
  98. package/src/pro/consumers/declarativeMapper.js +1 -1
  99. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  100. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  101. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  102. package/src/pro/consumers/handlers/index.js +1 -1
  103. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  104. package/src/pro/consumers/index.js +1 -1
  105. package/src/pro/consumers/messageTransformer.js +1 -1
  106. package/src/pro/consumers/validator.js +1 -1
  107. package/src/pro/database/base-model-mysql.js +1 -1
  108. package/src/pro/database/base-model-oracle.js +1 -1
  109. package/src/pro/database/base-model-sqlite.js +1 -1
  110. package/src/pro/database/db-mysql.js +1 -1
  111. package/src/pro/database/db-oracle.js +1 -1
  112. package/src/pro/database/db-sqlite.js +1 -1
  113. package/src/pro/excel/excel-generator.js +1 -1
  114. package/src/pro/excel/excel-parser.js +1 -1
  115. package/src/pro/excel/export-service.js +1 -1
  116. package/src/pro/excel/export_handler.js +1 -1
  117. package/src/pro/excel/import-service.js +1 -1
  118. package/src/pro/excel/import-validator.js +1 -1
  119. package/src/pro/excel/import_handler.js +1 -1
  120. package/src/pro/excel/upsert-builder.js +1 -1
  121. package/src/pro/idgen/idgen-routes.js +1 -1
  122. package/src/pro/integrations/lookup-resolver.js +1 -1
  123. package/src/pro/integrations/upload-handler-v2.js +1 -1
  124. package/src/pro/integrations/upload-handler.js +1 -1
  125. package/src/pro/integrations/webhook.js +1 -1
  126. package/src/pro/locking/lock-routes.js +1 -1
  127. package/src/pro/locking/resource-lock-manager.js +1 -1
  128. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  129. package/src/pro/messaging/kafkaService.js +1 -1
  130. package/src/pro/messaging/messagehubService.js +1 -1
  131. package/src/pro/messaging/rabbitmqService.js +1 -1
  132. package/src/pro/scheduler/job-manager.js +1 -1
  133. package/src/pro/scheduler/job-routes.js +1 -1
  134. package/src/pro/scheduler/job-validator.js +1 -1
  135. package/src/pro/storage/base-storage-provider.js +1 -1
  136. package/src/pro/storage/file-metadata-helper.js +1 -1
  137. package/src/pro/storage/index.js +1 -1
  138. package/src/pro/storage/local-storage-provider.js +1 -1
  139. package/src/pro/storage/s3-storage-provider.js +1 -1
  140. package/src/pro/storage/upload-cleanup-job.js +1 -1
  141. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  142. package/src/pro/storage/upload-pending-tracker.js +1 -1
  143. package/src/pro/websocket/broadcast-helper.js +1 -1
  144. package/src/pro/websocket/index.js +1 -1
  145. package/src/pro/websocket/livesync-server.js +1 -1
  146. package/src/pro/websocket/ws-broadcaster.js +1 -1
  147. package/src/services/export-service.js +1 -1
  148. package/src/services/import-service.js +1 -1
  149. package/src/services/kafkaConsumerService.js +1 -1
  150. package/src/services/kafkaService.js +1 -1
  151. package/src/services/messagehubService.js +1 -1
  152. package/src/services/rabbitmqService.js +1 -1
  153. package/src/utils/cache-invalidation-registry.js +1 -1
  154. package/src/utils/cache-manager.js +1 -1
  155. package/src/utils/component-engine.js +1 -1
  156. package/src/utils/config-extractor.js +1 -1
  157. package/src/utils/consumerLogger.js +1 -1
  158. package/src/utils/context-builder.js +1 -1
  159. package/src/utils/dashboard-helpers.js +1 -1
  160. package/src/utils/dateHelper.js +1 -1
  161. package/src/utils/datetime-formatter.js +1 -1
  162. package/src/utils/datetime-parser.js +1 -1
  163. package/src/utils/db-bootstrap.js +1 -1
  164. package/src/utils/db-mysql.js +1 -1
  165. package/src/utils/db-oracle.js +1 -1
  166. package/src/utils/db-sqlite.js +1 -1
  167. package/src/utils/db.js +1 -1
  168. package/src/utils/demo-generator.js +1 -1
  169. package/src/utils/excel-generator.js +1 -1
  170. package/src/utils/excel-parser.js +1 -1
  171. package/src/utils/file-watcher.js +1 -1
  172. package/src/utils/id-generator.js +1 -1
  173. package/src/utils/idempotency-manager.js +1 -1
  174. package/src/utils/import-validator.js +1 -1
  175. package/src/utils/license-client.js +1 -1
  176. package/src/utils/lock-manager.js +1 -1
  177. package/src/utils/logger.js +1 -1
  178. package/src/utils/lookup-resolver.js +1 -1
  179. package/src/utils/payload-loader.js +1 -1
  180. package/src/utils/processor-response.js +1 -1
  181. package/src/utils/rabbitmq.js +1 -1
  182. package/src/utils/redis-client.js +1 -1
  183. package/src/utils/redis-helper.js +1 -1
  184. package/src/utils/request-scope.js +1 -1
  185. package/src/utils/security-checks.js +1 -1
  186. package/src/utils/service-resolver.js +1 -1
  187. package/src/utils/shutdown-coordinator.js +1 -1
  188. package/src/utils/trusted-keys.js +1 -1
  189. package/src/utils/upload-handler.js +1 -1
  190. package/src/utils/upsert-builder.js +1 -1
  191. package/src/utils/workflow-hook-executor.js +1 -1
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Contract: payload migrate
5
+ *
6
+ * Konversi file payload backend (RDF / RESTForge consumer) menjadi file payload
7
+ * frontend (UDF / designer). Port dari `rfd migrate` di packages/designer/.
8
+ *
9
+ * Perbedaan dengan `rfd migrate`:
10
+ * - `apiBaseUrl` di-construct dari SERVER_ADDRESS + SERVER_PORT (db-connection.env)
11
+ * + --project sehingga sesuai dengan runtime server actual
12
+ * - Port di-baca dari db-connection.env (SERVER_PORT), bukan hard-coded 3000
13
+ * - Primary key (constraints.primaryKey=true) di-skip dari fields[] apapun
14
+ * tipenya (uuid/string/integer), karena PK adalah identifier teknis dan
15
+ * tidak perlu di-render di UI form
16
+ */
17
+
18
+ const runner = require('../../lib/migrate/migrate-runner');
19
+
20
+ module.exports = {
21
+ resource: 'payload',
22
+ verb: 'migrate',
23
+ description: 'Convert backend payload (RDF) file into frontend payload (UDF) for the designer',
24
+ category: 'generation',
25
+ flags: {
26
+ name: {
27
+ type: 'string',
28
+ required: true,
29
+ description: 'Backend payload file name (e.g. visitors.json). Relative to cwd or cwd/payload/.'
30
+ },
31
+ output: {
32
+ type: 'string',
33
+ required: false,
34
+ default: null,
35
+ description: 'Output directory (the file will be written with the same name as --name inside it). If ending with `.json`, treated as an explicit file path. Default: frontend/payload/'
36
+ },
37
+ config: {
38
+ type: 'string',
39
+ required: false,
40
+ default: null,
41
+ description: 'Database config file (.env). Used to read SERVER_ADDRESS and SERVER_PORT. Falls back to `.restforge/defaults.json`.'
42
+ },
43
+ project: {
44
+ type: 'string',
45
+ required: true,
46
+ description: 'Project name (kebab-case code) used as the path segment in apiBaseUrl: http://{host}:{port}/api/{project}'
47
+ },
48
+ 'app-name': {
49
+ type: 'string',
50
+ required: false,
51
+ default: null,
52
+ description: 'Application name (default: "My Application")'
53
+ },
54
+ 'app-code': {
55
+ type: 'string',
56
+ required: false,
57
+ default: null,
58
+ description: 'Application code in kebab-case (default: follows --project)'
59
+ },
60
+ plugin: {
61
+ type: 'string',
62
+ required: false,
63
+ default: null,
64
+ description: 'Designer plugin ID (default: "vanilla-js-basic")'
65
+ },
66
+ port: {
67
+ type: 'number',
68
+ required: false,
69
+ default: null,
70
+ description: 'Frontend application port written to appConfig.port (default: 8000). Independent from the backend port used in apiBaseUrl.'
71
+ },
72
+ overwrite: {
73
+ type: 'boolean',
74
+ required: false,
75
+ default: false,
76
+ description: 'Overwrite the output file if it already exists'
77
+ }
78
+ },
79
+ examples: [
80
+ 'npx restforge payload migrate --name=visitors.json --output=..\\sandbox\\frontend\\payload --config=db.env --project=myapp',
81
+ 'npx restforge payload migrate --name=visitors.json --project=myapp --overwrite'
82
+ ],
83
+ async handler(args) {
84
+ await runner.run({
85
+ name: args.name,
86
+ output: args.output || null,
87
+ config: args.config || null,
88
+ project: args.project,
89
+ appName: args['app-name'] || null,
90
+ appCode: args['app-code'] || null,
91
+ plugin: args.plugin || null,
92
+ port: typeof args.port === 'number' ? args.port : null,
93
+ overwrite: args.overwrite === true
94
+ });
95
+ }
96
+ };
@@ -5,34 +5,40 @@
5
5
  * statement incremental. Komplemen `ddl-generator.js` yang menghasilkan full
6
6
  * CREATE TABLE.
7
7
  *
8
- * Operasi yang di-handle (MVP):
9
- * - ADD COLUMN (additive, default)
10
- * - DROP COLUMN (destruktif, butuh allowDrop)
11
- * - MODIFY COLUMN (length / nullable, butuh allowModify)
12
- * - CREATE INDEX (additive)
13
- * - DROP INDEX (destruktif, butuh allowDrop)
14
- * - ADD CONSTRAINT UNIQUE (additive)
15
- * - DROP CONSTRAINT UNIQUE (destruktif, butuh allowDrop)
8
+ * Operasi yang di-handle:
9
+ * - ADD COLUMN (additive, default)
10
+ * - DROP COLUMN (destruktif, butuh allowDrop)
11
+ * - MODIFY COLUMN (length / nullable, butuh allowModify)
12
+ * - CREATE INDEX (additive)
13
+ * - DROP INDEX (destruktif, butuh allowDrop)
14
+ * - ADD CONSTRAINT UNIQUE (additive)
15
+ * - DROP CONSTRAINT UNIQUE (destruktif, butuh allowDrop)
16
+ * - ADD FOREIGN KEY (additive, default — kecuali sqlite)
17
+ * - DROP FOREIGN KEY (destruktif, butuh allowDrop — kecuali sqlite)
18
+ * - REPLACE FOREIGN KEY action (destruktif: DROP+ADD, butuh allowModify — kecuali sqlite)
16
19
  *
17
20
  * Operasi yang TIDAK di-handle (deferred):
18
21
  * - ALTER COLUMN type change (butuh data conversion strategy per dialect)
19
22
  * - PK changes (butuh rebuild table)
20
- * - FK changes (butuh cross-model validation + dialect syntax kompleks)
21
23
  * - DEFAULT value changes (defer ke fase berikutnya)
22
24
  * - CHECK constraint changes
25
+ * - FK changes pada sqlite (butuh rebuild table)
23
26
  *
24
- * SQLite tidak mendukung ALTER COLUMN / DROP COLUMN tanpa rebuild table.
25
- * Untuk dialect ini, MODIFY dan DROP COLUMN otomatis di-skip dengan reason
26
- * 'sqlite limitation' walaupun flag opt-in aktif.
27
+ * SQLite tidak mendukung ALTER COLUMN / DROP COLUMN / ALTER ADD CONSTRAINT FK
28
+ * tanpa rebuild table. Untuk dialect ini, MODIFY/DROP COLUMN dan semua perubahan
29
+ * FK otomatis di-skip dengan reason 'sqlite limitation' walaupun flag opt-in
30
+ * aktif.
27
31
  *
28
32
  * Output dipesan dengan urutan aman untuk dependency:
29
- * 1. ADD COLUMN (no dependencies)
30
- * 2. CREATE INDEX (depend on column existence)
31
- * 3. ADD CONSTRAINT UNIQUE (depend on column existence)
32
- * 4. ALTER/MODIFY COLUMN (modify after additive done)
33
- * 5. DROP CONSTRAINT (before drop column to avoid dependency error)
34
- * 6. DROP INDEX (before drop column)
35
- * 7. DROP COLUMN (last)
33
+ * 1. ADD COLUMN (no dependencies)
34
+ * 2. CREATE INDEX (depend on column existence)
35
+ * 3. ADD CONSTRAINT UNIQUE (depend on column existence)
36
+ * 4. ADD CONSTRAINT FOREIGN KEY (depend on column existence di kedua sisi)
37
+ * 5. ALTER/MODIFY COLUMN (modify after additive done)
38
+ * 6. DROP CONSTRAINT FK (release FK sebelum drop unique/index/column)
39
+ * 7. DROP CONSTRAINT UNIQUE (before drop column to avoid dependency error)
40
+ * 8. DROP INDEX (before drop column)
41
+ * 9. DROP COLUMN (last)
36
42
  *
37
43
  * @module lib/dbschema-kit/apply-engine
38
44
  */
@@ -45,7 +51,9 @@ const {
45
51
  emitCreateIndex,
46
52
  emitDropIndex,
47
53
  emitAddUnique,
48
- emitDropUnique
54
+ emitDropUnique,
55
+ emitAddForeignKey,
56
+ emitDropForeignKey
49
57
  } = require('./emitters/alter-table');
50
58
 
51
59
  const VALID_DIALECTS = ['postgres', 'mysql', 'oracle', 'sqlite'];
@@ -142,7 +150,10 @@ function buildBuckets() {
142
150
  addColumns: [],
143
151
  createIndexes: [],
144
152
  addUniques: [],
153
+ addForeignKeys: [],
145
154
  modifyColumns: [],
155
+ dropForeignKeys: [],
156
+ replaceForeignKeysAdd: [],
146
157
  dropConstraints: [],
147
158
  dropIndexes: [],
148
159
  dropColumns: []
@@ -154,7 +165,10 @@ function flattenBuckets(buckets) {
154
165
  ...buckets.addColumns,
155
166
  ...buckets.createIndexes,
156
167
  ...buckets.addUniques,
168
+ ...buckets.addForeignKeys,
157
169
  ...buckets.modifyColumns,
170
+ ...buckets.dropForeignKeys,
171
+ ...buckets.replaceForeignKeysAdd,
158
172
  ...buckets.dropConstraints,
159
173
  ...buckets.dropIndexes,
160
174
  ...buckets.dropColumns
@@ -412,51 +426,201 @@ function processUniques(delta, tableIR, dialect, options, buckets, skipped, summ
412
426
  }
413
427
  }
414
428
 
415
- function noteDeferredSections(delta, skipped) {
416
- // PK changes — deferred dari MVP karena butuh rebuild table.
417
- if (delta.primaryKey && delta.primaryKey.match === false) {
418
- const sdf = Array.isArray(delta.primaryKey.sdf) ? delta.primaryKey.sdf : [];
419
- const db = Array.isArray(delta.primaryKey.db) ? delta.primaryKey.db : [];
420
- skipped.push({
421
- table: delta.tableName,
422
- kind: 'primary-key',
423
- target: `pk(${sdf.join(',') || '∅'} vs ${db.join(',') || '∅'})`,
424
- reason: 'deferred',
425
- description: `Primary key changes for ${delta.tableName} deferred from MVP (requires table rebuild)`
426
- });
427
- }
429
+ // ─────────────────────────────────────────────────────────────
430
+ // FK delta processing
431
+ // ─────────────────────────────────────────────────────────────
428
432
 
429
- if (delta.foreignKeys) {
430
- const fkOnlySdf = Array.isArray(delta.foreignKeys.onlyInSdf) ? delta.foreignKeys.onlyInSdf : [];
431
- const fkOnlyDb = Array.isArray(delta.foreignKeys.onlyInDb) ? delta.foreignKeys.onlyInDb : [];
432
- const fkMismatch = Array.isArray(delta.foreignKeys.mismatched) ? delta.foreignKeys.mismatched : [];
433
+ // FK delta entries dari diff-engine memuat triplet (localKey, target, references)
434
+ // plus optional `name` (relName dari relations map) dan `dbName` (untuk mismatched).
435
+ // Apply path hanya menjalankan validator opsional, jadi kita fallback ke localKey
436
+ // untuk derive constraint name bila relName tidak tersedia.
437
+ function pickRelName(fk) {
438
+ if (fk && typeof fk.name === 'string' && fk.name !== '') return fk.name;
439
+ if (fk && typeof fk.localKey === 'string' && fk.localKey !== '') return fk.localKey;
440
+ return null;
441
+ }
442
+
443
+ function fkSkipTarget(fk) {
444
+ const local = fk && fk.localKey ? fk.localKey : '?';
445
+ const target = fk && fk.target ? fk.target : '?';
446
+ const ref = fk && fk.references ? fk.references : '?';
447
+ return `${local} -> ${target}.${ref}`;
448
+ }
449
+
450
+ function processForeignKeys(delta, tableIR, dialect, options, buckets, skipped, summary) {
451
+ if (!delta.foreignKeys) return;
452
+
453
+ const fkOnlySdf = Array.isArray(delta.foreignKeys.onlyInSdf) ? delta.foreignKeys.onlyInSdf : [];
454
+ const fkOnlyDb = Array.isArray(delta.foreignKeys.onlyInDb) ? delta.foreignKeys.onlyInDb : [];
455
+ const fkMismatch = Array.isArray(delta.foreignKeys.mismatched) ? delta.foreignKeys.mismatched : [];
456
+
457
+ // SQLite: semua perubahan FK butuh table rebuild — defer dengan reason akurat
458
+ if (dialect.name === 'sqlite') {
433
459
  for (const fk of fkOnlySdf) {
434
460
  skipped.push({
435
461
  table: delta.tableName,
436
462
  kind: 'foreign-key',
437
- target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
438
- reason: 'deferred',
439
- description: `Foreign key changes deferred from MVP (additive ${delta.tableName}.${fk.localKey})`
463
+ target: fkSkipTarget(fk),
464
+ reason: 'sqlite limitation',
465
+ description: `SQLite does not support ALTER TABLE ADD CONSTRAINT FOREIGN KEY (${delta.tableName}.${fk.localKey})`
440
466
  });
441
467
  }
442
468
  for (const fk of fkOnlyDb) {
443
469
  skipped.push({
444
470
  table: delta.tableName,
445
471
  kind: 'foreign-key',
446
- target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
447
- reason: 'deferred',
448
- description: `Foreign key changes deferred from MVP (drop ${delta.tableName}.${fk.localKey})`
472
+ target: fkSkipTarget(fk),
473
+ reason: 'sqlite limitation',
474
+ description: `SQLite does not support ALTER TABLE DROP CONSTRAINT FOREIGN KEY (${delta.tableName}.${fk.localKey})`
449
475
  });
450
476
  }
451
477
  for (const fk of fkMismatch) {
452
478
  skipped.push({
453
479
  table: delta.tableName,
454
480
  kind: 'foreign-key',
455
- target: `${fk.localKey} -> ${fk.target}.${fk.references}`,
456
- reason: 'deferred',
457
- description: `Foreign key action change deferred from MVP (${delta.tableName}.${fk.localKey})`
481
+ target: fkSkipTarget(fk),
482
+ reason: 'sqlite limitation',
483
+ description: `SQLite does not support ALTER TABLE for FK action change (${delta.tableName}.${fk.localKey})`
484
+ });
485
+ }
486
+ return;
487
+ }
488
+
489
+ // FK additive (onlyInSdf) — non-destruktif, emit langsung tanpa flag opt-in
490
+ for (const fk of fkOnlySdf) {
491
+ const relName = pickRelName(fk);
492
+ if (!relName) {
493
+ skipped.push({
494
+ table: delta.tableName,
495
+ kind: 'foreign-key',
496
+ target: fkSkipTarget(fk),
497
+ reason: 'emit-error',
498
+ description: `Foreign key for ${delta.tableName} missing both name and localKey`
458
499
  });
500
+ continue;
459
501
  }
502
+ try {
503
+ buckets.addForeignKeys.push(emitAddForeignKey(tableIR, relName, fk, dialect));
504
+ summary.totalAdditions++;
505
+ } catch (err) {
506
+ skipped.push({
507
+ table: delta.tableName,
508
+ kind: 'foreign-key',
509
+ target: fkSkipTarget(fk),
510
+ reason: 'emit-error',
511
+ description: err && err.message ? err.message : String(err)
512
+ });
513
+ }
514
+ }
515
+
516
+ // FK drop (onlyInDb) — destruktif, butuh --allow-drop
517
+ for (const fk of fkOnlyDb) {
518
+ const relName = pickRelName(fk);
519
+ if (!relName) {
520
+ skipped.push({
521
+ table: delta.tableName,
522
+ kind: 'foreign-key',
523
+ target: fkSkipTarget(fk),
524
+ reason: 'emit-error',
525
+ description: `Foreign key for ${delta.tableName} missing both name and localKey`
526
+ });
527
+ continue;
528
+ }
529
+ if (!options.allowDrop) {
530
+ skipped.push({
531
+ table: delta.tableName,
532
+ kind: 'foreign-key',
533
+ target: fkSkipTarget(fk),
534
+ reason: 'requires --allow-drop',
535
+ description: `Drop foreign key on ${delta.tableName}.${fk.localKey} requires --allow-drop`
536
+ });
537
+ continue;
538
+ }
539
+ try {
540
+ // DROP memakai conname aktual dari introspeksi (fk.dbConstraintName) agar
541
+ // statement menargetkan constraint yang benar-benar ada di DB. Bila conname
542
+ // tidak tersedia (driver kustom), emitter fallback ke derivasi nama lama
543
+ // (generateConstraintName) — perilaku legacy, tanpa entry skipped/warning.
544
+ buckets.dropForeignKeys.push(
545
+ emitDropForeignKey(tableIR, relName, dialect, { name: fk.dbConstraintName })
546
+ );
547
+ summary.totalDeletions++;
548
+ } catch (err) {
549
+ skipped.push({
550
+ table: delta.tableName,
551
+ kind: 'foreign-key',
552
+ target: fkSkipTarget(fk),
553
+ reason: 'emit-error',
554
+ description: err && err.message ? err.message : String(err)
555
+ });
556
+ }
557
+ }
558
+
559
+ // FK mismatch (action change) — destruktif (DROP + ADD), butuh --allow-modify
560
+ for (const fk of fkMismatch) {
561
+ const relName = pickRelName(fk);
562
+ if (!relName) {
563
+ skipped.push({
564
+ table: delta.tableName,
565
+ kind: 'foreign-key',
566
+ target: fkSkipTarget(fk),
567
+ reason: 'emit-error',
568
+ description: `Foreign key for ${delta.tableName} missing both name and localKey`
569
+ });
570
+ continue;
571
+ }
572
+ if (!options.allowModify) {
573
+ skipped.push({
574
+ table: delta.tableName,
575
+ kind: 'foreign-key',
576
+ target: fkSkipTarget(fk),
577
+ reason: 'requires --allow-modify',
578
+ description: `Foreign key action change on ${delta.tableName}.${fk.localKey} requires --allow-modify`
579
+ });
580
+ continue;
581
+ }
582
+ try {
583
+ const sdfActions = fk.sdf || {};
584
+ const addRel = {
585
+ localKey: fk.localKey,
586
+ target: fk.target,
587
+ references: fk.references,
588
+ onDelete: sdfActions.onDelete,
589
+ onUpdate: sdfActions.onUpdate
590
+ };
591
+ // DROP memakai conname aktual dari DB (fk.dbConstraintName, fallback derive
592
+ // bila absen); ADD memakai nama SDF-derive (relName) konsisten dengan
593
+ // create-table. ADD masuk replaceForeignKeysAdd bucket supaya terbit SETELAH
594
+ // semua DROP FK, mencegah konflik bila nama lama dan baru kebetulan sama.
595
+ buckets.dropForeignKeys.push(
596
+ emitDropForeignKey(tableIR, relName, dialect, { name: fk.dbConstraintName })
597
+ );
598
+ buckets.replaceForeignKeysAdd.push(emitAddForeignKey(tableIR, relName, addRel, dialect));
599
+ summary.totalModifications++;
600
+ } catch (err) {
601
+ skipped.push({
602
+ table: delta.tableName,
603
+ kind: 'foreign-key',
604
+ target: fkSkipTarget(fk),
605
+ reason: 'emit-error',
606
+ description: err && err.message ? err.message : String(err)
607
+ });
608
+ }
609
+ }
610
+ }
611
+
612
+ function noteDeferredSections(delta, skipped) {
613
+ // PK changes — deferred dari MVP karena butuh rebuild table.
614
+ if (delta.primaryKey && delta.primaryKey.match === false) {
615
+ const sdf = Array.isArray(delta.primaryKey.sdf) ? delta.primaryKey.sdf : [];
616
+ const db = Array.isArray(delta.primaryKey.db) ? delta.primaryKey.db : [];
617
+ skipped.push({
618
+ table: delta.tableName,
619
+ kind: 'primary-key',
620
+ target: `pk(${sdf.join(',') || '∅'} vs ${db.join(',') || '∅'})`,
621
+ reason: 'deferred',
622
+ description: `Primary key changes for ${delta.tableName} deferred from MVP (requires table rebuild)`
623
+ });
460
624
  }
461
625
 
462
626
  if (delta.checks) {
@@ -548,6 +712,7 @@ function generateAlterStatements(deltas, options) {
548
712
  processFieldsMismatched(delta, tableIR, dialect, localOptions, buckets, skipped, sdfModels, summary);
549
713
  processIndexes(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
550
714
  processUniques(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
715
+ processForeignKeys(delta, tableIR, dialect, localOptions, buckets, skipped, summary);
551
716
  noteDeferredSections(delta, skipped);
552
717
 
553
718
  const flat = flattenBuckets(buckets);
@@ -410,7 +410,11 @@ function extractForeignKeys(ir) {
410
410
  target: stripSchemaPrefix(rel.target),
411
411
  references: rel.references || null,
412
412
  onDelete: rel.onDelete,
413
- onUpdate: rel.onUpdate
413
+ onUpdate: rel.onUpdate,
414
+ // Carry the actual DB constraint name (introspect-mapper sets it on the
415
+ // rel) past this whitelist so DROP/MODIFY can target the real conname.
416
+ // Undefined for SDF-side FKs, which never emit a DROP.
417
+ _dbConstraintName: rel._dbConstraintName
414
418
  });
415
419
  }
416
420
  return out;
@@ -456,11 +460,14 @@ function compareFkActions(sdfFk, dbFk) {
456
460
 
457
461
  function formatFkEntry(fk) {
458
462
  return {
463
+ name: fk.name,
459
464
  localKey: fk.localKey,
460
465
  target: fk.target,
461
466
  references: fk.references,
462
467
  onDelete: normalizeFkAction(fk.onDelete),
463
- onUpdate: normalizeFkAction(fk.onUpdate)
468
+ onUpdate: normalizeFkAction(fk.onUpdate),
469
+ // Actual DB constraint name for onlyInDb (DROP); undefined for onlyInSdf (ADD).
470
+ dbConstraintName: fk._dbConstraintName
464
471
  };
465
472
  }
466
473
 
@@ -493,6 +500,11 @@ function diffForeignKeys(sdfIR, dbIR) {
493
500
  const reasons = compareFkActions(sdfFk, dbFk);
494
501
  if (reasons.length > 0) {
495
502
  mismatched.push({
503
+ name: sdfFk.name,
504
+ dbName: dbFk.name,
505
+ // Actual DB constraint name (conname) from the DB side, used to DROP
506
+ // the existing constraint before re-adding with SDF-derived naming.
507
+ dbConstraintName: dbFk._dbConstraintName,
496
508
  localKey: sdfFk.localKey,
497
509
  target: sdfFk.target,
498
510
  references: sdfFk.references,
@@ -10,7 +10,9 @@
10
10
  * - CREATE INDEX
11
11
  * - DROP INDEX (dialect-aware)
12
12
  * - ADD CONSTRAINT UNIQUE
13
- * - DROP CONSTRAINT
13
+ * - DROP CONSTRAINT (unique)
14
+ * - ADD CONSTRAINT FOREIGN KEY
15
+ * - DROP CONSTRAINT FOREIGN KEY (MySQL: DROP FOREIGN KEY)
14
16
  *
15
17
  * Konvensi penamaan constraint/index reuse `naming.js` (generateConstraintName,
16
18
  * deriveCompositeShortName) agar nama yang di-emit identik dengan create-table
@@ -262,6 +264,95 @@ function emitDropUnique(tableIR, columns, dialect, options) {
262
264
  return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(name)}`;
263
265
  }
264
266
 
267
+ // ─────────────────────────────────────────────────────────────
268
+ // ADD / DROP FOREIGN KEY
269
+ // ─────────────────────────────────────────────────────────────
270
+
271
+ // Cross-model-validator augments rel with `_targetSchemaName` + `_targetTableName`.
272
+ // Apply path tidak selalu menjalankan validator, jadi fallback dengan split
273
+ // `rel.target` di '.' bila tersedia, lalu treat sebagai schema-less target.
274
+ function resolveTargetIr(rel) {
275
+ if (rel && rel._targetTableName) {
276
+ return { schemaName: rel._targetSchemaName || null, tableName: rel._targetTableName };
277
+ }
278
+ const target = rel && rel.target;
279
+ if (typeof target === 'string' && target.includes('.')) {
280
+ const idx = target.indexOf('.');
281
+ return { schemaName: target.slice(0, idx), tableName: target.slice(idx + 1) };
282
+ }
283
+ return { schemaName: null, tableName: target };
284
+ }
285
+
286
+ function deriveForeignKeyConstraintName(tableName, relName, dialect, explicitName) {
287
+ if (typeof explicitName === 'string' && explicitName !== '') {
288
+ return explicitName;
289
+ }
290
+ return generateConstraintName('fk', tableName, relName, dialect.maxIdentifierLength);
291
+ }
292
+
293
+ function emitAddForeignKey(tableIR, relName, rel, dialect) {
294
+ if (!tableIR || typeof tableIR.tableName !== 'string') {
295
+ throw new Error('emitAddForeignKey: tableIR with tableName is required');
296
+ }
297
+ if (typeof relName !== 'string' || relName === '') {
298
+ throw new Error('emitAddForeignKey: relName must be a non-empty string');
299
+ }
300
+ if (!rel || typeof rel !== 'object') {
301
+ throw new Error(`emitAddForeignKey: rel definition required for '${relName}'`);
302
+ }
303
+ if (typeof rel.localKey !== 'string' || rel.localKey === '') {
304
+ throw new Error(`emitAddForeignKey: rel.localKey required for '${relName}'`);
305
+ }
306
+ if (typeof rel.references !== 'string' || rel.references === '') {
307
+ throw new Error(`emitAddForeignKey: rel.references required for '${relName}'`);
308
+ }
309
+ if (dialect.name === 'sqlite') {
310
+ throw new Error(
311
+ `emitAddForeignKey: sqlite does not support ALTER TABLE ADD CONSTRAINT FOREIGN KEY ` +
312
+ `(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
313
+ );
314
+ }
315
+
316
+ const tableIdentifier = dialect.formatTableIdentifier(tableIR);
317
+ const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect);
318
+ const local = dialect.quoteIdentifier(rel.localKey);
319
+ const target = dialect.formatTableIdentifier(resolveTargetIr(rel));
320
+ const ref = dialect.quoteIdentifier(rel.references);
321
+
322
+ let stmt = `ALTER TABLE ${tableIdentifier} ADD CONSTRAINT ${dialect.quoteIdentifier(constraintName)} `
323
+ + `FOREIGN KEY (${local}) REFERENCES ${target} (${ref})`;
324
+ if (rel.onDelete) stmt += ` ON DELETE ${dialect.formatReferentialAction(rel.onDelete)}`;
325
+ if (rel.onUpdate) stmt += ` ON UPDATE ${dialect.formatReferentialAction(rel.onUpdate)}`;
326
+ return stmt;
327
+ }
328
+
329
+ function emitDropForeignKey(tableIR, relName, dialect, options) {
330
+ if (!tableIR || typeof tableIR.tableName !== 'string') {
331
+ throw new Error('emitDropForeignKey: tableIR with tableName is required');
332
+ }
333
+ if (typeof relName !== 'string' || relName === '') {
334
+ throw new Error('emitDropForeignKey: relName must be a non-empty string');
335
+ }
336
+ if (dialect.name === 'sqlite') {
337
+ throw new Error(
338
+ `emitDropForeignKey: sqlite does not support ALTER TABLE DROP CONSTRAINT FOREIGN KEY ` +
339
+ `(requires table rebuild). Filter FK operations in apply-engine before calling emitter.`
340
+ );
341
+ }
342
+
343
+ const explicitName = options && typeof options.name === 'string' && options.name.length > 0
344
+ ? options.name
345
+ : null;
346
+ const constraintName = deriveForeignKeyConstraintName(tableIR.tableName, relName, dialect, explicitName);
347
+ const tableIdentifier = dialect.formatTableIdentifier(tableIR);
348
+
349
+ if (dialect.name === 'mysql') {
350
+ // MySQL: DROP FOREIGN KEY syntax (DROP CONSTRAINT FK hanya supported MySQL 8.0.19+)
351
+ return `ALTER TABLE ${tableIdentifier} DROP FOREIGN KEY ${dialect.quoteIdentifier(constraintName)}`;
352
+ }
353
+ return `ALTER TABLE ${tableIdentifier} DROP CONSTRAINT ${dialect.quoteIdentifier(constraintName)}`;
354
+ }
355
+
265
356
  module.exports = {
266
357
  emitAddColumn,
267
358
  emitDropColumn,
@@ -270,6 +361,9 @@ module.exports = {
270
361
  emitDropIndex,
271
362
  emitAddUnique,
272
363
  emitDropUnique,
364
+ emitAddForeignKey,
365
+ emitDropForeignKey,
273
366
  deriveIndexName,
274
- deriveUniqueConstraintName
367
+ deriveUniqueConstraintName,
368
+ deriveForeignKeyConstraintName
275
369
  };
@@ -120,6 +120,15 @@ function mapTableMetaToIR(tableMeta, dialect) {
120
120
  _sourceField: null
121
121
  };
122
122
 
123
+ // Preserve the actual DB constraint name (conname). relName above is derived
124
+ // from the target table for SDF generation and does not match the constraint
125
+ // stored in the database, so the apply-engine needs the real conname to emit a
126
+ // DROP that targets an existing constraint. Internal field (prefix `_`): the
127
+ // schema-printer whitelist (VALID_REL_FIELDS) keeps it out of SDF output.
128
+ if (typeof fk.name === 'string' && fk.name !== '') {
129
+ rel._dbConstraintName = fk.name;
130
+ }
131
+
123
132
  const onDelete = mapReferentialAction(fk.onDelete);
124
133
  if (onDelete) rel.onDelete = onDelete;
125
134
  const onUpdate = mapReferentialAction(fk.onUpdate);