@restforgejs/platform 5.1.4 → 5.1.7

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 (174) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +1 -1
  3. package/cli/consumer.js +1 -1
  4. package/generators/cli/data/pull.js +16 -7
  5. package/generators/cli/data/push.js +17 -7
  6. package/generators/cli/fast-track.js +63 -43
  7. package/generators/cli/init.js +347 -97
  8. package/generators/lib/data/data-scope.js +138 -0
  9. package/generators/lib/data/envelope.js +25 -9
  10. package/generators/lib/data/pull-runner.js +44 -37
  11. package/generators/lib/data/push-runner.js +64 -46
  12. package/generators/lib/templates/dashboard-catalog.js +1 -1
  13. package/generators/lib/templates/db-connection-env.js +1 -1
  14. package/generators/lib/templates/dbschema-catalog.js +1 -1
  15. package/generators/lib/templates/field-validation-catalog.js +1 -1
  16. package/generators/lib/templates/mysql-template.js +1 -1
  17. package/generators/lib/templates/oracle-template.js +1 -1
  18. package/generators/lib/templates/postgres-template.js +1 -1
  19. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  20. package/generators/lib/templates/sqlite-template.js +1 -1
  21. package/integrity-manifest.json +18 -18
  22. package/package.json +1 -1
  23. package/scripts/verify-integrity.js +1 -1
  24. package/server.js +1 -1
  25. package/src/components/handlers/adjust_handler.js +1 -1
  26. package/src/components/handlers/audit_handler.js +1 -1
  27. package/src/components/handlers/delete_handler.js +1 -1
  28. package/src/components/handlers/export_handler.js +1 -1
  29. package/src/components/handlers/import_handler.js +1 -1
  30. package/src/components/handlers/insert_handler.js +1 -1
  31. package/src/components/handlers/update_handler.js +1 -1
  32. package/src/components/handlers/upload_handler.js +1 -1
  33. package/src/components/handlers/workflow_handler.js +1 -1
  34. package/src/components/integrations/webhook.js +1 -1
  35. package/src/consumers/baseConsumer.js +1 -1
  36. package/src/consumers/declarativeMapper.js +1 -1
  37. package/src/consumers/handlers/apiHandler.js +1 -1
  38. package/src/consumers/handlers/consoleHandler.js +1 -1
  39. package/src/consumers/handlers/databaseHandler.js +1 -1
  40. package/src/consumers/handlers/index.js +1 -1
  41. package/src/consumers/handlers/kafkaHandler.js +1 -1
  42. package/src/consumers/index.js +1 -1
  43. package/src/consumers/messageTransformer.js +1 -1
  44. package/src/consumers/validator.js +1 -1
  45. package/src/core/db/dialect/base-dialect.js +1 -1
  46. package/src/core/db/dialect/index.js +1 -1
  47. package/src/core/db/dialect/mysql-dialect.js +1 -1
  48. package/src/core/db/dialect/oracle-dialect.js +1 -1
  49. package/src/core/db/dialect/postgres-dialect.js +1 -1
  50. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  51. package/src/core/db/flatten-helper.js +1 -1
  52. package/src/core/db/query-builder-error.js +1 -1
  53. package/src/core/db/query-builder.js +1 -1
  54. package/src/core/db/relation-helper.js +1 -1
  55. package/src/core/handlers/delete_handler.js +1 -1
  56. package/src/core/handlers/insert_handler.js +1 -1
  57. package/src/core/handlers/update_handler.js +1 -1
  58. package/src/core/models/base-model.js +1 -1
  59. package/src/core/utils/cache-manager.js +1 -1
  60. package/src/core/utils/component-engine.js +1 -1
  61. package/src/core/utils/context-builder.js +1 -1
  62. package/src/core/utils/datetime-formatter.js +1 -1
  63. package/src/core/utils/datetime-parser.js +1 -1
  64. package/src/core/utils/db.js +1 -1
  65. package/src/core/utils/logger.js +1 -1
  66. package/src/core/utils/payload-loader.js +1 -1
  67. package/src/core/utils/security-checks.js +1 -1
  68. package/src/middleware/body-options.js +1 -1
  69. package/src/middleware/cors.js +1 -1
  70. package/src/middleware/idempotency.js +1 -1
  71. package/src/middleware/rate-limiter.js +1 -1
  72. package/src/middleware/request-logger.js +1 -1
  73. package/src/middleware/security-headers.js +1 -1
  74. package/src/models/base-model-mysql.js +1 -1
  75. package/src/models/base-model-oracle.js +1 -1
  76. package/src/models/base-model-sqlite.js +1 -1
  77. package/src/models/base-model.js +1 -1
  78. package/src/pro/caching/redis-client.js +1 -1
  79. package/src/pro/caching/redis-helper.js +1 -1
  80. package/src/pro/consumers/baseConsumer.js +1 -1
  81. package/src/pro/consumers/declarativeMapper.js +1 -1
  82. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  83. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  84. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  85. package/src/pro/consumers/handlers/index.js +1 -1
  86. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  87. package/src/pro/consumers/index.js +1 -1
  88. package/src/pro/consumers/messageTransformer.js +1 -1
  89. package/src/pro/consumers/validator.js +1 -1
  90. package/src/pro/database/base-model-mysql.js +1 -1
  91. package/src/pro/database/base-model-oracle.js +1 -1
  92. package/src/pro/database/base-model-sqlite.js +1 -1
  93. package/src/pro/database/db-mysql.js +1 -1
  94. package/src/pro/database/db-oracle.js +1 -1
  95. package/src/pro/database/db-sqlite.js +1 -1
  96. package/src/pro/excel/excel-generator.js +1 -1
  97. package/src/pro/excel/excel-parser.js +1 -1
  98. package/src/pro/excel/export-service.js +1 -1
  99. package/src/pro/excel/export_handler.js +1 -1
  100. package/src/pro/excel/import-service.js +1 -1
  101. package/src/pro/excel/import-validator.js +1 -1
  102. package/src/pro/excel/import_handler.js +1 -1
  103. package/src/pro/excel/upsert-builder.js +1 -1
  104. package/src/pro/idgen/idgen-routes.js +1 -1
  105. package/src/pro/integrations/lookup-resolver.js +1 -1
  106. package/src/pro/integrations/upload-handler-v2.js +1 -1
  107. package/src/pro/integrations/upload-handler.js +1 -1
  108. package/src/pro/integrations/webhook.js +1 -1
  109. package/src/pro/locking/lock-routes.js +1 -1
  110. package/src/pro/locking/resource-lock-manager.js +1 -1
  111. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  112. package/src/pro/messaging/kafkaService.js +1 -1
  113. package/src/pro/messaging/messagehubService.js +1 -1
  114. package/src/pro/messaging/rabbitmqService.js +1 -1
  115. package/src/pro/scheduler/job-manager.js +1 -1
  116. package/src/pro/scheduler/job-routes.js +1 -1
  117. package/src/pro/scheduler/job-validator.js +1 -1
  118. package/src/pro/storage/base-storage-provider.js +1 -1
  119. package/src/pro/storage/file-metadata-helper.js +1 -1
  120. package/src/pro/storage/index.js +1 -1
  121. package/src/pro/storage/local-storage-provider.js +1 -1
  122. package/src/pro/storage/s3-storage-provider.js +1 -1
  123. package/src/pro/storage/upload-cleanup-job.js +1 -1
  124. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  125. package/src/pro/storage/upload-pending-tracker.js +1 -1
  126. package/src/pro/websocket/broadcast-helper.js +1 -1
  127. package/src/pro/websocket/index.js +1 -1
  128. package/src/pro/websocket/livesync-server.js +1 -1
  129. package/src/pro/websocket/ws-broadcaster.js +1 -1
  130. package/src/services/export-service.js +1 -1
  131. package/src/services/import-service.js +1 -1
  132. package/src/services/kafkaConsumerService.js +1 -1
  133. package/src/services/kafkaService.js +1 -1
  134. package/src/services/messagehubService.js +1 -1
  135. package/src/services/rabbitmqService.js +1 -1
  136. package/src/utils/cache-invalidation-registry.js +1 -1
  137. package/src/utils/cache-manager.js +1 -1
  138. package/src/utils/component-engine.js +1 -1
  139. package/src/utils/config-extractor.js +1 -1
  140. package/src/utils/consumerLogger.js +1 -1
  141. package/src/utils/context-builder.js +1 -1
  142. package/src/utils/dashboard-helpers.js +1 -1
  143. package/src/utils/dateHelper.js +1 -1
  144. package/src/utils/datetime-formatter.js +1 -1
  145. package/src/utils/datetime-parser.js +1 -1
  146. package/src/utils/db-bootstrap.js +1 -1
  147. package/src/utils/db-mysql.js +1 -1
  148. package/src/utils/db-oracle.js +1 -1
  149. package/src/utils/db-sqlite.js +1 -1
  150. package/src/utils/db.js +1 -1
  151. package/src/utils/demo-generator.js +1 -1
  152. package/src/utils/excel-generator.js +1 -1
  153. package/src/utils/excel-parser.js +1 -1
  154. package/src/utils/file-watcher.js +1 -1
  155. package/src/utils/id-generator.js +1 -1
  156. package/src/utils/idempotency-manager.js +1 -1
  157. package/src/utils/import-validator.js +1 -1
  158. package/src/utils/license-client.js +1 -1
  159. package/src/utils/lock-manager.js +1 -1
  160. package/src/utils/logger.js +1 -1
  161. package/src/utils/lookup-resolver.js +1 -1
  162. package/src/utils/payload-loader.js +1 -1
  163. package/src/utils/processor-response.js +1 -1
  164. package/src/utils/rabbitmq.js +1 -1
  165. package/src/utils/redis-client.js +1 -1
  166. package/src/utils/redis-helper.js +1 -1
  167. package/src/utils/request-scope.js +1 -1
  168. package/src/utils/security-checks.js +1 -1
  169. package/src/utils/service-resolver.js +1 -1
  170. package/src/utils/shutdown-coordinator.js +1 -1
  171. package/src/utils/trusted-keys.js +1 -1
  172. package/src/utils/upload-handler.js +1 -1
  173. package/src/utils/upsert-builder.js +1 -1
  174. package/src/utils/workflow-hook-executor.js +1 -1
@@ -4,27 +4,33 @@
4
4
  * envelope — Bentuk + I/O envelope file data.
5
5
  *
6
6
  * Struktur envelope (urutan key wajib dipertahankan):
7
- * { format_version, table, source_dialect, columns, rows }
7
+ * { format_version, schema, table, source_dialect, columns, rows }
8
+ *
9
+ * `schema` (= `ir.schemaName`, `null` bila tanpa schema) ditambahkan di format
10
+ * version 1.1. File 1.0 (tanpa `schema`) tetap diterima oleh `validateEnvelope`
11
+ * untuk back-compat baca `data push`.
8
12
  *
9
13
  * Mengacu pada docs/plan/data-pull-push/data-pull-push-03-data-format.md.
10
14
  *
11
15
  * @module generators/lib/data/envelope
12
16
  */
13
17
 
14
- const FORMAT_VERSION = '1.0';
18
+ const FORMAT_VERSION = '1.1';
15
19
 
16
20
  /**
17
21
  * Bangun header envelope (tanpa rows).
18
22
  *
19
23
  * @param {Object} args
24
+ * @param {string|null} [args.schema] - Nama schema (= ir.schemaName; null bila tanpa schema)
20
25
  * @param {string} args.table - Nama tabel
21
26
  * @param {string} args.sourceDialect - Dialect sumber (mis. 'postgresql')
22
27
  * @param {Array<Object>} args.columns - Daftar kolom { name, type, nullable, primary_key }
23
- * @returns {{format_version: string, table: string, source_dialect: string, columns: Array}}
28
+ * @returns {{format_version: string, schema: (string|null), table: string, source_dialect: string, columns: Array}}
24
29
  */
25
- function buildHeader({ table, sourceDialect, columns }) {
30
+ function buildHeader({ schema = null, table, sourceDialect, columns }) {
26
31
  return {
27
32
  format_version: FORMAT_VERSION,
33
+ schema,
28
34
  table,
29
35
  source_dialect: sourceDialect,
30
36
  columns
@@ -58,11 +64,11 @@ function resolveSink(sink) {
58
64
  * Menulis header, meng-append rows per batch, lalu menutup array `rows` dan envelope.
59
65
  * Cocok untuk tabel besar karena rows tidak perlu ditahan seluruhnya di memori.
60
66
  * Output akhir adalah JSON valid dengan urutan key:
61
- * format_version, table, source_dialect, columns, rows
67
+ * format_version, schema, table, source_dialect, columns, rows
62
68
  *
63
69
  * Penggunaan:
64
70
  * const w = new EnvelopeWriter(fs.createWriteStream(path));
65
- * w.writeHeader({ table, sourceDialect, columns });
71
+ * w.writeHeader({ schema, table, sourceDialect, columns });
66
72
  * w.appendRows(batch1);
67
73
  * w.appendRows(batch2);
68
74
  * w.end();
@@ -83,16 +89,17 @@ class EnvelopeWriter {
83
89
 
84
90
  /**
85
91
  * Tulis prefix header dan buka array `rows`.
86
- * @param {Object} header - { table, sourceDialect, columns }
92
+ * @param {Object} header - { schema, table, sourceDialect, columns }
87
93
  * @returns {EnvelopeWriter} this
88
94
  */
89
- writeHeader({ table, sourceDialect, columns }) {
95
+ writeHeader({ schema, table, sourceDialect, columns }) {
90
96
  if (this._state !== 'init') {
91
97
  throw new Error('EnvelopeWriter: writeHeader() hanya boleh dipanggil sekali di awal');
92
98
  }
93
99
  const prefix =
94
100
  '{' +
95
101
  `"format_version":${JSON.stringify(FORMAT_VERSION)},` +
102
+ `"schema":${JSON.stringify(schema ?? null)},` +
96
103
  `"table":${JSON.stringify(table)},` +
97
104
  `"source_dialect":${JSON.stringify(sourceDialect)},` +
98
105
  `"columns":${JSON.stringify(columns)},` +
@@ -163,7 +170,11 @@ function parseEnvelope(jsonString) {
163
170
  * Validasi shape envelope. Throw (exitCode=1) bila tidak valid.
164
171
  *
165
172
  * Wajib: format_version (ada), table (string), columns (array of {name,type}),
166
- * rows (array of object).
173
+ * rows (array of object). Opsional: `schema` (string atau null bila ada).
174
+ *
175
+ * `format_version` tetap LENIENT (cukup ada/non-null) agar file 1.0 maupun 1.1
176
+ * sama-sama diterima tanpa penolakan versi. `schema` opsional untuk back-compat:
177
+ * file 1.0 (tanpa key `schema`) diterima; bila ada, wajib string atau null.
167
178
  *
168
179
  * @param {Object} obj
169
180
  * @returns {Object} obj yang sama (untuk chaining)
@@ -182,6 +193,11 @@ function validateEnvelope(obj) {
182
193
  if (obj.format_version === undefined || obj.format_version === null) {
183
194
  fail('field "format_version" wajib ada');
184
195
  }
196
+ // `schema` opsional (back-compat 1.0). Bila absen → diterima; bila ada → wajib
197
+ // string atau null. `null` eksplisit (1.1 tabel schemaless) tetap valid.
198
+ if (obj.schema !== undefined && obj.schema !== null && typeof obj.schema !== 'string') {
199
+ fail('field "schema" wajib berupa string atau null bila ada');
200
+ }
185
201
  if (typeof obj.table !== 'string' || obj.table === '') {
186
202
  fail('field "table" wajib berupa string non-kosong');
187
203
  }
@@ -3,17 +3,18 @@
3
3
  /**
4
4
  * pull-runner — Business logic command `data pull`.
5
5
  *
6
- * `run()` adalah dispatcher (single vs all-tables); `runOne(ir, ctx)` mengeksekusi
6
+ * `run()` adalah dispatcher (single vs multi-schema); `runOne(ir, ctx)` mengeksekusi
7
7
  * pull satu tabel dan mengelola executor-nya sendiri (Phase 02, data-pull-push-v2).
8
8
  *
9
9
  * Alur dispatcher `run()`:
10
10
  * 1. Resolve config DB (config-resolver) → loadConfig (dbschema-kit/connection); SEKALI.
11
- * 2. Validasi mutual-exclusive (sebelum load SDF / koneksi DB): tepat satu dari
12
- * --table / --all-tables (keduanya / tidak keduanya → exit 2).
11
+ * 2. parseSchemaFlag + validateScopeFlags (sebelum load SDF / koneksi DB): tepat satu
12
+ * dari --table / --schema / --all-schemas (selain itu → exit 2).
13
13
  * 3. Validasi --format (hanya 'json').
14
14
  * 4. Load SDF SEKALI → Map<qualifiedName, IR> (sdf-reader).
15
- * 5a. Jalur --table: findTableOrThrow → runOne(ir, ctx) → ringkasan single.
16
- * 5b. Jalur --all-tables: iterasi models.values() FAIL-FAST (Q3) → runOne per IR →
15
+ * 5. resolveScope(models, args, schemaList) → daftar IR sesuai flag scope.
16
+ * 5a. Jalur --table: satu IR → runOne(ir, ctx) → ringkasan single.
17
+ * 5b. Jalur --schema/--all-schemas: iterasi subset FAIL-FAST (Q3) → runOne per IR →
17
18
  * ringkasan agregat. Tabel yang sudah selesai sebelum error tetap tertulis.
18
19
  *
19
20
  * Alur `runOne(ir, ctx)` (per tabel; metadata kolom/namespace/PK murni dari SDF IR):
@@ -37,11 +38,11 @@ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-re
37
38
  const { loadConfig } = require('../dbschema-kit/connection');
38
39
  const {
39
40
  loadSdf,
40
- findTableOrThrow,
41
41
  irToColumns,
42
42
  getNamespace,
43
43
  getPrimaryKey
44
44
  } = require('./sdf-reader');
45
+ const { parseSchemaFlag, validateScopeFlags, resolveScope, relDataFilePath } = require('./data-scope');
45
46
  const { createDialect, validateIdentifier } = require('./dialect-kit');
46
47
  const { createExecutor } = require('./db-executor');
47
48
  const { encodeValue } = require('./value-codec');
@@ -176,10 +177,12 @@ async function pullInBatches({ executor, dialect, queryTable, columns, pk, batch
176
177
  }
177
178
 
178
179
  /**
179
- * Pull satu tabel SDF ke file envelope JSON `<output>/<qualifiedName>.json`.
180
+ * Pull satu tabel SDF ke file envelope JSON (path via relDataFilePath: bersarang
181
+ * `<output>/<schema>/<table>.json` bila ber-schema, flat `<output>/<table>.json`
182
+ * bila tanpa schema).
180
183
  *
181
184
  * Mengelola executor-nya sendiri (buat → disconnect, termasuk cleanup file partial
182
- * saat error) agar isolasi per-tabel jelas dan jalur --all-tables dapat memanggilnya
185
+ * saat error) agar isolasi per-tabel jelas dan jalur multi-schema dapat memanggilnya
183
186
  * berulang tanpa kebocoran koneksi. Metadata kolom, namespace, dan PK murni dari IR.
184
187
  *
185
188
  * @param {Object} ir - IR model SDF satu tabel
@@ -188,24 +191,24 @@ async function pullInBatches({ executor, dialect, queryTable, columns, pk, batch
188
191
  * @param {string} ctx.sourceDialect - dialect kanonik untuk header envelope
189
192
  * @param {Object} ctx.args - argumen ter-parse (storage-path, force, limit, batch-size)
190
193
  * @param {string} ctx.cwd - working directory
191
- * @returns {Promise<{table: string, rows: number, file: string, source_dialect: string}>}
194
+ * @returns {Promise<{schema: (string|null), table: string, rows: number, file: string, source_dialect: string}>}
192
195
  */
193
196
  async function runOne(ir, ctx) {
194
197
  const { config, sourceDialect, args, cwd } = ctx;
195
198
 
196
199
  const columns = irToColumns(ir);
197
- const { qualifiedName } = getNamespace(ir);
200
+ const { schemaName, qualifiedName } = getNamespace(ir);
198
201
  const rawPk = getPrimaryKey(ir);
199
202
  const pk = Array.isArray(rawPk) ? rawPk : [];
200
203
 
201
204
  // ── a. Cek file output (SEBELUM menulis/konek) ───────────────────────────────
202
- // Nama file = qualifiedName SDF (bukan ir.tableName), agar dua tabel bernama sama
203
- // di schema berbeda tidak collision (mis. `sales.order_item.json`). Tabel tanpa
204
- // schema: qualifiedName === tableName `visitors.json` (tak berubah). Titik pada
205
- // qualifiedName dipertahankan (valid sebagai nama file). push (Phase 03) menurunkan
206
- // nama file dengan aturan identik dari SDF.
205
+ // Path diturunkan dari relDataFilePath (aturan seragam Q2): tabel ber-schema
206
+ // bersarang `<schema>/<table>.json` (mis. `sales/order_item.json`); tabel tanpa
207
+ // schema flat `<table>.json` (mis. `visitors.json`). Helper bersama dengan push
208
+ // (data-scope) agar derivasi tidak drift. queryTable SELECT tetap memakai
209
+ // qualifiedName (identitas DB).
207
210
  const outputDir = path.resolve(cwd, args['storage-path']);
208
- const outFile = path.join(outputDir, `${qualifiedName}.json`);
211
+ const outFile = path.join(outputDir, relDataFilePath(ir));
209
212
  if (fs.existsSync(outFile) && args.force !== true) {
210
213
  const rel = path.relative(cwd, outFile).split(path.sep).join('/');
211
214
  throw failWith(
@@ -238,11 +241,14 @@ async function runOne(ir, ctx) {
238
241
  let rowCount = 0;
239
242
  let fd = null;
240
243
  try {
241
- fs.mkdirSync(outputDir, { recursive: true });
244
+ // Buat direktori parent dari outFile (bukan sekadar outputDir): pada layout
245
+ // bersarang, outFile berada di subfolder schema (mis. `<storage>/main/`) yang
246
+ // belum tentu ada. path.dirname mencakup subfolder schema maupun flat root.
247
+ fs.mkdirSync(path.dirname(outFile), { recursive: true });
242
248
  fd = fs.openSync(outFile, 'w');
243
249
 
244
250
  const writer = new EnvelopeWriter((chunk) => fs.writeSync(fd, chunk));
245
- writer.writeHeader({ table: ir.tableName, sourceDialect, columns });
251
+ writer.writeHeader({ schema: schemaName, table: ir.tableName, sourceDialect, columns });
246
252
 
247
253
  rowCount = await pullInBatches({
248
254
  executor,
@@ -277,6 +283,7 @@ async function runOne(ir, ctx) {
277
283
  // ── e. Ringkasan per-tabel (tanpa `command`; dirakit caller) ─────────────────
278
284
  const relFile = path.relative(cwd, outFile).split(path.sep).join('/');
279
285
  return {
286
+ schema: schemaName,
280
287
  table: ir.tableName,
281
288
  rows: rowCount,
282
289
  file: relFile,
@@ -285,10 +292,10 @@ async function runOne(ir, ctx) {
285
292
  }
286
293
 
287
294
  /**
288
- * Jalankan command `data pull` (dispatcher: single-table vs --all-tables).
295
+ * Jalankan command `data pull` (dispatcher: single-table vs multi-schema).
289
296
  * @param {Object} args - argumen ter-parse dari contract
290
- * @returns {Promise<Object>} ringkasan single { command, table, rows, file, source_dialect }
291
- * atau agregat { command, all_tables, table_count, total_rows, tables: [...] }
297
+ * @returns {Promise<Object>} ringkasan single { command, schema, table, rows, file, source_dialect }
298
+ * atau agregat { command, scope, table_count, total_rows, tables: [...] }
292
299
  */
293
300
  async function run(args) {
294
301
  const cwd = process.cwd();
@@ -312,17 +319,12 @@ async function run(args) {
312
319
  throw failWith(e.message, 2);
313
320
  }
314
321
 
315
- // ── 2. Validasi mutual-exclusive --table / --all-tables (SEBELUM SDF/DB) ──────
316
- // Arg-parser tidak punya mekanisme XOR lintas-flag (hanya `required` per-flag),
317
- // sehingga "tepat satu" ditegakkan di sini. Exit 2 (precondition/usage).
322
+ // ── 2. Validasi scope flags (SEBELUM SDF/DB) ─────────────────────────────────
323
+ // Tepat satu dari --table / --schema / --all-schemas (selain itu → exit 2).
324
+ // Helper bersama dengan push (data-scope) agar perilaku identik.
325
+ const schemaList = parseSchemaFlag(args.schema);
326
+ validateScopeFlags(args, schemaList);
318
327
  const hasTable = args.table != null && args.table !== '';
319
- const hasAll = args['all-tables'] === true;
320
- if (hasTable && hasAll) {
321
- throw failWith('Specify exactly one of --table or --all-tables.', 2);
322
- }
323
- if (!hasTable && !hasAll) {
324
- throw failWith('Specify either --table=<name> or --all-tables.', 2);
325
- }
326
328
 
327
329
  // ── 3. Validasi --format ─────────────────────────────────────────────────────
328
330
  if (args.format !== 'json') {
@@ -350,10 +352,14 @@ async function run(args) {
350
352
  cwd
351
353
  };
352
354
 
355
+ // ── 5. Resolusi scope → daftar IR ────────────────────────────────────────────
356
+ // --table → [ir] (exit 2 bila tak terdaftar/ambigu); --schema → subset (exit 2
357
+ // bila 0-match); --all-schemas → seluruh IR (boleh kosong → agregat 0 tabel).
358
+ const subset = resolveScope(models, args, schemaList);
359
+
353
360
  // ── 5a. Jalur --table (single; regresi nol) ──────────────────────────────────
354
361
  if (hasTable) {
355
- const ir = findTableOrThrow(models, args.table); // throw exitCode=2 bila tak terdaftar
356
- const per = await runOne(ir, ctx);
362
+ const per = await runOne(subset[0], ctx);
357
363
  const summary = { command: 'pull', ...per };
358
364
 
359
365
  if (args.json === true) {
@@ -366,13 +372,14 @@ async function run(args) {
366
372
  return summary;
367
373
  }
368
374
 
369
- // ── 5b. Jalur --all-tables (fail-fast Q3; agregat) ───────────────────────────
375
+ // ── 5b. Jalur --schema/--all-schemas (fail-fast Q3; agregat) ─────────────────
370
376
  // Iterasi mengikuti urutan insertion Map (file SDF ter-sort alfabetis). Error
371
377
  // pada satu tabel menghentikan loop (fail-fast); tabel yang sudah selesai
372
- // sebelum error tetap tertulis (konsekuensi Q3/R4, didokumentasikan Phase 05).
378
+ // sebelum error tetap tertulis (konsekuensi Q3/R4, didokumentasikan Phase 03).
379
+ const scope = schemaList ? 'schema' : 'all-schemas';
373
380
  const tables = [];
374
381
  let totalRows = 0;
375
- for (const ir of models.values()) {
382
+ for (const ir of subset) {
376
383
  const per = await runOne(ir, ctx);
377
384
  tables.push(per);
378
385
  totalRows += per.rows;
@@ -385,7 +392,7 @@ async function run(args) {
385
392
 
386
393
  const aggregate = {
387
394
  command: 'pull',
388
- all_tables: true,
395
+ scope,
389
396
  table_count: tables.length,
390
397
  total_rows: totalRows,
391
398
  tables
@@ -7,21 +7,23 @@
7
7
  * di-descope dari v1 (User konfirmasi 2026-06-03), sehingga flag `--mode`/`--force`
8
8
  * dihapus dari contract dan runner tidak lagi membaca/memvalidasi keduanya.
9
9
  *
10
- * `run()` adalah dispatcher (single vs all-tables); `runOne(ir, ctx)` mengeksekusi
10
+ * `run()` adalah dispatcher (single vs multi-schema); `runOne(ir, ctx)` mengeksekusi
11
11
  * push satu tabel dan mengelola executor-nya sendiri (Phase 03, data-pull-push-v2).
12
12
  *
13
13
  * Alur dispatcher `run()`:
14
14
  * 1. Resolve config DB tujuan (config-resolver) → loadConfig (dbschema-kit/connection); SEKALI.
15
- * 2. Validasi mutual-exclusive (sebelum load SDF / koneksi DB): tepat satu dari
16
- * --table / --all-tables (keduanya / tidak keduanya → exit 2).
15
+ * 2. parseSchemaFlag + validateScopeFlags (sebelum load SDF / koneksi DB): tepat satu
16
+ * dari --table / --schema / --all-schemas (selain itu → exit 2).
17
17
  * 3. Load SDF SEKALI → Map<qualifiedName, IR> (sdf-reader).
18
- * 4a. Jalur --table: findTableOrThrow → runOne(ir, ctx) → ringkasan single (regresi nol).
19
- * 4b. Jalur --all-tables: topoSortByFk(models) → urutan parent→child (table-order); siklus →
20
- * warning stderr + fallback urutan deklarasi. Loop FAIL-FAST (Q3); tabel tanpa file
21
- * input di-SKIP (warning stderr, catat di skipped[], lanjut) runOne per IR → agregat.
18
+ * 4. resolveScope(models, args, schemaList) → daftar IR sesuai flag scope.
19
+ * 4a. Jalur --table: satu IR → runOne(ir, ctx) → ringkasan single (regresi nol).
20
+ * 4b. Jalur --schema/--all-schemas: topoSortByFk(SUBSET) urutan parent→child (table-order);
21
+ * siklus warning stderr + fallback urutan deklarasi. Loop FAIL-FAST (Q3); tabel tanpa
22
+ * file input di-SKIP (warning stderr, catat di skipped[], lanjut) → runOne per IR → agregat.
22
23
  *
23
24
  * Alur `runOne(ir, ctx)` (per tabel; metadata kolom/namespace/PK murni dari SDF IR):
24
- * a. Derive nama file dari SDF (aturan IDENTIK pull): {cwd}/{input}/{qualifiedName}.json.
25
+ * a. Derive nama file dari SDF (aturan IDENTIK pull, relDataFilePath): bersarang
26
+ * `<input>/<schema>/<table>.json` bila ber-schema, flat `<input>/<table>.json`.
25
27
  * File tidak ada → exit 1. Baca + parseEnvelope + validateEnvelope (shape invalid → exit 1).
26
28
  * b. Column match vs SDF (BUKAN introspeksi DB): tiap envelope.columns[].name harus ada
27
29
  * di SDF dan tipe konsisten. Mismatch → exit 1. No-PK guard (exit 1).
@@ -46,11 +48,11 @@ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-re
46
48
  const { loadConfig } = require('../dbschema-kit/connection');
47
49
  const {
48
50
  loadSdf,
49
- findTableOrThrow,
50
51
  irToColumns,
51
52
  getNamespace,
52
53
  getPrimaryKey
53
54
  } = require('./sdf-reader');
55
+ const { parseSchemaFlag, validateScopeFlags, resolveScope, relDataFilePath } = require('./data-scope');
54
56
  const { createDialect, validateIdentifier, placeholder } = require('./dialect-kit');
55
57
  const { createExecutor } = require('./db-executor');
56
58
  const { decodeValue } = require('./value-codec');
@@ -135,10 +137,12 @@ function matchColumns(envelopeColumns, sdfColumns, tableName) {
135
137
  }
136
138
 
137
139
  /**
138
- * Push satu tabel SDF dari file envelope JSON `<input>/<qualifiedName>.json` ke DB.
140
+ * Push satu tabel SDF dari file envelope JSON ke DB. Path input via relDataFilePath
141
+ * (bersarang `<input>/<schema>/<table>.json` bila ber-schema, flat
142
+ * `<input>/<table>.json` bila tanpa schema).
139
143
  *
140
144
  * Mengelola executor-nya sendiri (buat → disconnect di finally) agar isolasi
141
- * per-tabel jelas dan jalur --all-tables dapat memanggilnya berulang tanpa kebocoran
145
+ * per-tabel jelas dan jalur multi-schema dapat memanggilnya berulang tanpa kebocoran
142
146
  * koneksi. Metadata kolom, namespace, dan PK murni dari IR.
143
147
  *
144
148
  * @param {Object} ir - IR model SDF satu tabel
@@ -147,24 +151,24 @@ function matchColumns(envelopeColumns, sdfColumns, tableName) {
147
151
  * @param {string} ctx.targetDialect - dialect kanonik (untuk output, dirakit caller)
148
152
  * @param {Object} ctx.args - argumen ter-parse (storage-path, batch-size)
149
153
  * @param {string} ctx.cwd - working directory
150
- * @returns {Promise<{table: string, rows: number, committed_batches: number}>}
154
+ * @returns {Promise<{schema: (string|null), table: string, rows: number, committed_batches: number}>}
151
155
  */
152
156
  async function runOne(ir, ctx) {
153
157
  const { config, args, cwd } = ctx;
154
158
 
155
159
  const sdfColumns = irToColumns(ir);
156
- const { qualifiedName } = getNamespace(ir);
160
+ const { schemaName, qualifiedName } = getNamespace(ir);
157
161
  const rawPk = getPrimaryKey(ir);
158
162
  const pk = Array.isArray(rawPk) ? rawPk : [];
159
163
 
160
- // ── a. Derive nama file dari SDF (aturan IDENTIK pull) + baca envelope ─────────
161
- // Nama file = {input}/{qualifiedName}.json, turunan murni dari SDF (bukan --table
162
- // mentah). Konsisten dengan pull (lihat phase-02b Keputusan K2): tabel ber-schema
163
- // → `<schema>.<table>.json`; tanpa schema → `<table>.json`. Single-table:
164
- // file-not-found = exit 1 (tak berubah). Jalur --all-tables menyaring file-missing
165
- // SEBELUM memanggil runOne (skip-missing), jadi guard ini hanya tercapai pada single.
164
+ // ── a. Derive nama file dari SDF (aturan IDENTIK pull, relDataFilePath) ────────
165
+ // Path = {input}/relDataFilePath(ir), turunan MURNI dari SDF (bukan --table mentah
166
+ // maupun envelope.schema). Tabel ber-schema bersarang `<schema>/<table>.json`;
167
+ // tanpa schema → flat `<table>.json`. Single-table: file-not-found = exit 1 (tak
168
+ // berubah). Jalur --schema/--all-schemas menyaring file-missing SEBELUM memanggil
169
+ // runOne (skip-missing), jadi guard ini hanya tercapai pada single.
166
170
  const inputDir = path.resolve(cwd, args['storage-path']);
167
- const inFile = path.join(inputDir, `${qualifiedName}.json`);
171
+ const inFile = path.join(inputDir, relDataFilePath(ir));
168
172
  if (!fs.existsSync(inFile)) {
169
173
  const rel = path.relative(cwd, inFile).split(path.sep).join('/');
170
174
  throw failWith(`Input file not found: ${rel}`, 1);
@@ -180,6 +184,17 @@ async function runOne(ir, ctx) {
180
184
  throw failWith(`Failed to read envelope '${path.relative(cwd, inFile).split(path.sep).join('/')}': ${e.message}`, 1);
181
185
  }
182
186
 
187
+ // Cross-check OPSIONAL: path diturunkan dari SDF, bukan envelope. Bila envelope 1.1
188
+ // membawa `schema` yang berbeda dari schema SDF (mis. file dipindah/diedit manual),
189
+ // cetak warning informatif (bukan error) — konsisten perlakuan envelope.table.
190
+ // File 1.0 (tanpa `schema`, undefined) dilewati.
191
+ if (envelope.schema !== undefined && envelope.schema !== schemaName) {
192
+ process.stderr.write(
193
+ `Warning: envelope schema '${envelope.schema}' differs from SDF schema ` +
194
+ `'${schemaName === null ? '(none)' : schemaName}' for table '${ir.tableName}'\n`
195
+ );
196
+ }
197
+
183
198
  // ── b. Column match vs SDF (tanpa introspeksi DB) + no-PK guard ──────────────
184
199
  const insertColumns = matchColumns(envelope.columns, sdfColumns, ir.tableName);
185
200
 
@@ -233,6 +248,7 @@ async function runOne(ir, ctx) {
233
248
 
234
249
  // ── e. Ringkasan per-tabel (tanpa command/target_dialect; dirakit caller) ─────
235
250
  return {
251
+ schema: schemaName,
236
252
  table: ir.tableName,
237
253
  rows: rowCount,
238
254
  committed_batches: committedBatches
@@ -240,10 +256,10 @@ async function runOne(ir, ctx) {
240
256
  }
241
257
 
242
258
  /**
243
- * Jalankan command `data push` (dispatcher: single-table vs --all-tables, append-only).
259
+ * Jalankan command `data push` (dispatcher: single-table vs multi-schema, append-only).
244
260
  * @param {Object} args - argumen ter-parse dari contract
245
- * @returns {Promise<Object>} ringkasan single { command, table, rows, target_dialect, committed_batches }
246
- * atau agregat { command, all_tables, table_count, total_rows, tables: [...], skipped: [...] }
261
+ * @returns {Promise<Object>} ringkasan single { command, schema, table, rows, target_dialect, committed_batches }
262
+ * atau agregat { command, scope, table_count, total_rows, tables: [...], skipped: [...] }
247
263
  */
248
264
  async function run(args) {
249
265
  const cwd = process.cwd();
@@ -267,18 +283,12 @@ async function run(args) {
267
283
  throw failWith(e.message, 2);
268
284
  }
269
285
 
270
- // ── 2. Validasi mutual-exclusive --table / --all-tables (SEBELUM SDF/DB) ──────
271
- // Arg-parser tidak punya mekanisme XOR lintas-flag (hanya `required` per-flag),
272
- // sehingga "tepat satu" ditegakkan di sini. Exit 2 (precondition/usage). Identik
273
- // pull-runner Phase 02.
286
+ // ── 2. Validasi scope flags (SEBELUM SDF/DB) ─────────────────────────────────
287
+ // Tepat satu dari --table / --schema / --all-schemas (selain itu → exit 2).
288
+ // Helper bersama dengan pull (data-scope) agar perilaku identik.
289
+ const schemaList = parseSchemaFlag(args.schema);
290
+ validateScopeFlags(args, schemaList);
274
291
  const hasTable = args.table != null && args.table !== '';
275
- const hasAll = args['all-tables'] === true;
276
- if (hasTable && hasAll) {
277
- throw failWith('Specify exactly one of --table or --all-tables.', 2);
278
- }
279
- if (!hasTable && !hasAll) {
280
- throw failWith('Specify either --table=<name> or --all-tables.', 2);
281
- }
282
292
 
283
293
  // ── 3. Load SDF (SEKALI) ─────────────────────────────────────────────────────
284
294
  const schemaPath = args['schema-path'];
@@ -298,12 +308,17 @@ async function run(args) {
298
308
  cwd
299
309
  };
300
310
 
311
+ // ── 4. Resolusi scope → daftar IR ────────────────────────────────────────────
312
+ // --table → [ir] (exit 2 bila tak terdaftar/ambigu); --schema → subset (exit 2
313
+ // bila 0-match); --all-schemas → seluruh IR (boleh kosong → agregat 0 tabel).
314
+ const subset = resolveScope(models, args, schemaList);
315
+
301
316
  // ── 4a. Jalur --table (single; regresi nol) ──────────────────────────────────
302
317
  if (hasTable) {
303
- const ir = findTableOrThrow(models, args.table); // throw exitCode=2 bila tak terdaftar/ambigu
304
- const per = await runOne(ir, ctx);
318
+ const per = await runOne(subset[0], ctx);
305
319
  const summary = {
306
320
  command: 'push',
321
+ schema: per.schema,
307
322
  table: per.table,
308
323
  rows: per.rows,
309
324
  target_dialect: ctx.targetDialect,
@@ -320,25 +335,28 @@ async function run(args) {
320
335
  return summary;
321
336
  }
322
337
 
323
- // ── 4b. Jalur --all-tables (topo-sort FK parent→child, fail-fast Q3, skip-missing) ──
324
- // Urutan push diturunkan dari FK SDF: parent (tabel direferensikan) SEBELUM child
325
- // agar INSERT child tidak melanggar FK constraint. Siklus → fallback urutan deklarasi.
326
- const { ordered, cycle } = topoSortByFk(models);
338
+ // ── 4b. Jalur --schema/--all-schemas (topo-sort FK parent→child, fail-fast Q3, skip-missing) ──
339
+ // Urutan push diturunkan dari FK SDF SUBSET (bukan seluruh models): parent (tabel
340
+ // direferensikan) SEBELUM child agar INSERT child tidak melanggar FK constraint.
341
+ // Siklus fallback urutan deklarasi. Catatan (Phase 03): untuk --schema subset, FK
342
+ // ke parent di luar subset → edge diabaikan table-order; child bisa di-push tanpa
343
+ // parent → potensi FK error runtime (exit 3), tanpa pre-warning.
344
+ const scope = schemaList ? 'schema' : 'all-schemas';
345
+ const subsetMap = new Map(subset.map((ir) => [getNamespace(ir).qualifiedName, ir]));
346
+ const { ordered, cycle } = topoSortByFk(subsetMap);
327
347
  if (cycle) {
328
348
  process.stderr.write('FK cycle detected; falling back to declaration order\n');
329
349
  }
330
350
 
331
- // Skip-missing HANYA di --all-tables: tabel tanpa file input dilewati (warning +
332
- // catat), bukan error. Catatan (Phase 05): bila parent di-skip namun child ada,
333
- // INSERT child dapat melanggar FK → muncul sebagai DB error exit 3 (fail-fast).
351
+ // Skip-missing HANYA di jalur multi: tabel tanpa file input dilewati (warning +
352
+ // catat), bukan error. Path input via relDataFilePath (aturan IDENTIK pull).
334
353
  const inputDir = path.resolve(cwd, args['storage-path']);
335
354
  const tables = [];
336
355
  const skipped = [];
337
356
  let totalRows = 0;
338
357
 
339
358
  for (const ir of ordered) {
340
- const { qualifiedName } = getNamespace(ir);
341
- const inFile = path.join(inputDir, `${qualifiedName}.json`);
359
+ const inFile = path.join(inputDir, relDataFilePath(ir));
342
360
  if (!fs.existsSync(inFile)) {
343
361
  process.stderr.write(`Skipped ${ir.tableName}: input file not found\n`);
344
362
  skipped.push(ir.tableName);
@@ -357,7 +375,7 @@ async function run(args) {
357
375
 
358
376
  const aggregate = {
359
377
  command: 'push',
360
- all_tables: true,
378
+ scope,
361
379
  table_count: tables.length,
362
380
  total_rows: totalRows,
363
381
  tables,
@@ -1 +1 @@
1
- const a0_0x8f7365=a0_0x5fba;function a0_0x1bd9(){const _0x1b6fce=['zMLSztPXDwvYEs88Cgf0Ad4VyNjLywTKB3DUlNnXBa','Aw5OzxjPDhmGq0fdsevFvfrmigvUDG','C2nHBgfYihbYAw1PDgL2zq','u0vmrunuicOGrLjptsbZDg9JA19PBMjVDw5KifDirvjfievyvfjbq1qOwuvbuIbguK9nigLUyM91BMrFzgf0zsKGpsa6EwvHCG','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5Nie4GCM93CW','yxjYyxK8C3rYAw5NpIWGB3b0Aw9UywWG4Ocuihn1yNnLDcbVzIb3AwrNzxqGsurZihrVigv4zwn1DguUie9TAxqGDg8GzxHLy3v0zsbHBgWGzgvJBgfYzwqGD2LKz2v0CY4','DMfSDwu','zMLSztPXDwvYEs88Cgf0Ad4VCg9PBNrZlNnXBa','twv0CMLJicSGu3bHCMTSAw5L','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5NideGCM93imoxig11BhrPCgXLignVBhvTBNm','zMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','zxHWB3j0CW','sgvHzgXPBMuGBwv0CMLJihDPDgGGDhjLBMqGy2HPCcbHBMqGChjVz3jLC3mGyMfYigfNywLUC3qGysbWzxjPB2qGDgfYz2v0lIbtDwL0ywjSzsbMB3iGD2LKz2v0CYbSAwTLicDpCMrLCNmGvgHPCYbnB250AcCU','ugfYyw1LDgvYignVBNrYywn0igzVCIb0AguGzgfZAgjVyxjKlIbfywnOigTLEsbPCYbHihbHCMfTig5HBwu7ihzHBhvLCYbKzxnJCMLIzsb0ExbLl3jLCxvPCMvKl2rLzMf1BhqUifbSywnLAg9SzgvYCYbPBNnPzguGD2LKz2v0ifnrtcbTDxn0ihjLzMvYzw5JzsbKzwnSyxjLzcbWyxjHBsbUyw1LCY4','ue9tvcaVyxbPl21PBMKTAw52zw50B3j5l2rHC2GTAw5IB3vUzc9KyxnOyM9HCMq','yxjYyxK','n05KD0PuBq','tMvNyxrPDMuGBg9VA2jLAgLUzcbWCMv2zw50CYbTyxrJAgLUzYaNoJONicHqB3n0z3jLCYbJyxn0ihn5BNrHEcKGyxmGysbWBgfJzwHVBgrLCI4','DgfIBgvoyw1L','sgvHzgXPBMuGBwv0CMLJihDPDgGGDhjLBMqGy2HPCcbHBMqGC3bHCMTSAw5Lig1PBMKTy2HHCNqGzM9YihnOB3j0ihDPBMrVD3mGkdCGzgf5CYWGmtiGBw9UDgHZlcbLDgmUks4Gu3vPDgfIBguGzM9YihDPzgDLDhmGBgLRzsaNqxzLCMfNzsbeywLSEsbtywXLCYCU','msbYB3CGW5CGmIbJB2X1Bw5Z','vuKGBgfIzwWGAxmGysbMCM9UDgvUzcbYzw5KzxjPBMCGy29Uy2vYBI4','q29SBgfWC2uGDg8GC2nHBgfYihbYAw1PDgL2zsaODgHLihzHBhvLig9MihrOzsbZAw5NBguGy29SDw1Uks4','zgfZAgjVyxjKlwnHDgfSB2C','y2fJAgu','yw55icHTDxn0igjLignVBxbHDgLIBguGD2L0AcbKzwnSyxjLzcaNDhLWzsCP','EYbZDwnJzxnZoIbIB29SzwfUlcbKyxrHoIb7idX3AwrNzxrjzd46idXWzxjxAwrNzxrszxnWB25Zzt4Sic4UlIb9ih0','w3SGiMXHyMvSiJOGiLnOB2vZiIWGiNzHBhvLiJOGiJC2nJaIih0SihSGiMXHyMvSiJOGiKDHBwLUzYiSicj2ywX1zsi6iciYodiWiIb9lcb7icjSywjLBci6icjpDgHLCNmIlcaIDMfSDwuIoIaInduYntCIih1D','otK0nde5tgLUA3HA','nZqWmZeYDK94C3zc','Dg9WlwXLDMvSicDWyxjHBxmNig9IAMvJDa','msbYB3CGW5CGmsbJB2X1Bw4','zMLSztPXDwvYEs88Cgf0Ad4VDMfSDwuUC3fS','mty0odCWngXssM1guG','BM9UlwvTChr5lcb1BMLXDwuGywnYB3nZihDPzgDLDhmGAw4GDgHLihnHBwuGCgf5Bg9Hza','DhLWzq','mJqWnJiYnw5QwwrnuG','CgfYyw1Z','vg9Nz2XLignHy2HLigzLyxr1CMuGzM9YihrOAxmGzgfZAgjVyxjKlG','Bwv0CMLJx3bYB2DYzxnZx3rVx2DVywW','ugvYlwTLEsbIyxnLzcbVBIbZy2fSyxjdB2XSyxbZzvj1BgvZigjLBg93lG','B3jKzxjZx3rOAxnFBw9UDgG','ndCWC3zJvvvd','yM9VBgvHBG','qsbWyxLSB2fKihDPDgGGyM90AcaND2LKz2v0CYCGyw5KicD0ywjSzu5HBwuNigLZihjLAMvJDgvKigj5ierHC2HIB2fYzfzHBgLKyxrVCI4GugLJAYbVBMuGC2HHCguU','D2LKz2v0vhLWzq','DhjLBMq','BgfIzwW','uMvZzxj2zwqGzM9YiensvuqGCgf5Bg9HzhmUieeGzgfZAgjVyxjKihbHEwXVywqGBxvZDcbKzwnSyxjLicD3AwrNzxrZjYbPBNn0zwfKlG','msbYB3CGW5CGmsbJB2WSig91Dhb1DcbJB2X1Bw4Gj3zHBhvLjW','B2jQzwn0','iJi0mJaI','rgfZAgjVyxjKigvUzhbVAw50ig1HEsbVChqTAw4GDg8GuMvKAxmTyMfZzwqGy2fJAguUifbHDhrLCM4GzM9SBg93CYbWCM9JzxnZB3iGy2fJAguGkhnLzsbMzwf0lwnHy2HLlM1Kks4Gq2fJAguGC2nVCguGAxmGDgHLigz1BgWGCMvZCg9UC2uGzw52zwXVCgu7ig9UzsbJywnOzsbLBNrYEsbWzxiGkhbHCMfTCYaRihDPzgDLDhnBxsbZDwjZzxqPignVBwjPBMf0Aw9UlG','Dg9Wtgv2zwXbBgXVD2vK','v2LKz2v0igLKzw50AwzPzxi7ihvZzwqGyxmGDgHLihjLC3bVBNnLigTLEsbPBIb0AguGzgfZAgjVyxjKigvUDMvSB3bLlG','CxvLCMLLCW','DgfYz2v0','tM90igeGzgfZAgjVyxjKihbHEwXVywqGkgXPA2vSEsbduLveihDPDgGGDgfIBgvoyw1LlcbVCIbPBNzHBgLKkq','yxzNx2rHAwX5x3nHBgvZ','qwX3yxLZihSGAxrLBxm6ifSUlI5Dih0GCMvNyxjKBgvZCYbVzIbtuuWGCMvZDwX0ihnOyxbLlG','Cgn0','msbYB3CGW5CGmIbJB2XZlcbVDxrWDxqGy29SDw1UCYaNzgLYzwn0Aw9UjYWGj3bJDcC','v2HLBIb0CNvLlcb0AguGCMvXDwvZDcbIB2r5ie1vu1qGAw5JBhvKzsb0AgLZihbHCMfTicHVDgHLCNDPC2uGndaWks4','zw5HyMXLza','zMLSztPXDwvYEs88Cgf0Ad4VDhjLBMqUC3fS','CMvXDwLYzwq','mJC2nde5EfbnqK1M','ugfYyw0GBMfTzsbTDxn0ig1HDgnOihrOzsbWBgfJzwHVBgrLCIbYzwDLEcbGw2eTEKeTwL9Dw2eTEKeTwJaTov9DkMaGkgfSCgHHBNvTzxjPyYaRihvUzgvYC2nVCMuSig11C3qGC3rHCNqGD2L0AcbSzxr0zxiGB3iGDw5KzxjZy29YzsKU','zgLYzwn0Aw9U','CgvYAw9K','zgfZAc1PBMjVDw5K','BgvUz3rO','u2LUz2XLifnrtcbXDwvYEsbMB3iGDgHLihDPzgDLDc4','CxvLCNK','zxHWzwn0zwrFzwfYBMLUz3m','mti1ode2zuf2v3nu','Cg9PBNrZ','rNjVBNrLBMqGzgv0zxjTAw5LCYbKB251Dc9WAwuGDMfYAwfUDcWGy29SB3iGCgvYignHDgvNB3j5lcbHBMqGBgfIzwWGB3jKzxiUieLMihbLCI1JyxrLz29YEsbWzxjJzw50ywDLigLZig5LzwrLzcbMB3iGDgHLigrVBNv0igfYyYWGzNjVBNrLBMqGy29TChv0zxmGAxqGzNjVBsbPDgvTC1TPxs52ywX1zsaVihn1BsHPDgvTC1SQxs52ywX1zsKUie5Vig5LzwqGDg8GC2vUzcaNCgn0jYbMCM9TigjHy2TLBMqGDw5SzxnZihrOzsbMAwD1CMuGAxmGysbZDgfIBguGyNvZAw5LC3mGy2fSy3vSyxrPB24GAw5KzxbLBMrLBNqGB2yGDMLZDwfSihjLBMrLCMLUzY4','m2XwEfbwuq','tgLZDcbVzIbduLveihrHyMXLig5HBwvZihrOyxqSihDOzw4GD3jPDhrLBIWGD2LSBcb0CMLNz2vYigLUDMfSAwrHDgLVBIbVzIb0AgLZigrHC2HIB2fYzcbJywnOzs4','z2vUzxjHDgLVBIb0Aw1LicHot1qGCNvUDgLTzsK','nLnXrKrcvW','zgfZAgjVyxjKihbHEwXVywq','iJy5nZaWiG','vgLTzs10BY1SAxzLigLUihnLy29UzhmUidaGzwzMzwn0AxzLBhKGzgLZywjSzxmGy2fJAguGzM9YihrOAxmGzw50CNKU','vMLZDwfSihzHCMLHBNqGkgrVBNv0lcbIyxiSihbPzsWGyxjLysKGAxmGysbMCM9UDgvUzcbYzw5KzxjPBMCGy29Uy2vYBIaOC2vWyxjHDgLVBIbVzIbJB25JzxjUCYKU','q29SBgfWC2uGDg8GB2jQzwn0ihDOB3nLigTLExmGyxjLifnrtcbJB2X1Bw4GBMfTzxmGkgXVD2vYy2fZzwqPlG','mty4mJi1nNDOvgrZAa','Aw52ywXPzgf0zxm','twv0CMLJicSGuhjVz3jLC3mGDg8Gr29HBa','qsb3AwrNzxqGtvvtvcbKzwnSyxjLigv4ywn0BhKGB25Lig9MoIaNCxvLCNKNie9sicDXDwvYAwvZjY4GqM90AcbVCIbUzwL0AgvYigLZihjLAMvJDgvKlG','tgLZDcbVzIb3AwrNzxqGzgvMAw5PDgLVBNmUie9YzgvYigLZigLUzM9YBwf0Aw9UywWGB25SEsaOCMvZCg9UC2uGA2v5CYbHCMuGyNKGD2LKz2v0igLKlcbUB3qGyxjYyxKGAw5KzxGPlG'];a0_0x1bd9=function(){return _0x1b6fce;};return a0_0x1bd9();}function a0_0x5fba(_0x577dad,_0x13c29a){_0x577dad=_0x577dad-0x9c;const _0x1bd98f=a0_0x1bd9();let _0x5fba85=_0x1bd98f[_0x577dad];if(a0_0x5fba['DVwhas']===undefined){var _0xb4672d=function(_0xf33517){const _0x5e9201='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x5a52be='',_0x2471e8='';for(let _0x39d2ff=0x0,_0x4dcdff,_0x45d33c,_0x4988dc=0x0;_0x45d33c=_0xf33517['charAt'](_0x4988dc++);~_0x45d33c&&(_0x4dcdff=_0x39d2ff%0x4?_0x4dcdff*0x40+_0x45d33c:_0x45d33c,_0x39d2ff++%0x4)?_0x5a52be+=String['fromCharCode'](0xff&_0x4dcdff>>(-0x2*_0x39d2ff&0x6)):0x0){_0x45d33c=_0x5e9201['indexOf'](_0x45d33c);}for(let _0x34a890=0x0,_0x29c11f=_0x5a52be['length'];_0x34a890<_0x29c11f;_0x34a890++){_0x2471e8+='%'+('00'+_0x5a52be['charCodeAt'](_0x34a890)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x2471e8);};a0_0x5fba['JGsvKy']=_0xb4672d,a0_0x5fba['LnyIKt']={},a0_0x5fba['DVwhas']=!![];}const _0x47ded6=_0x1bd98f[0x0],_0x30874b=_0x577dad+_0x47ded6,_0x38cad0=a0_0x5fba['LnyIKt'][_0x30874b];return!_0x38cad0?(_0x5fba85=a0_0x5fba['JGsvKy'](_0x5fba85),a0_0x5fba['LnyIKt'][_0x30874b]=_0x5fba85):_0x5fba85=_0x38cad0,_0x5fba85;}(function(_0x533163,_0x463dd6){const _0x4da6d0=a0_0x5fba,_0x58da42=_0x533163();while(!![]){try{const _0x16ed12=parseInt(_0x4da6d0(0xb0))/0x1*(parseInt(_0x4da6d0(0xad))/0x2)+parseInt(_0x4da6d0(0xb9))/0x3+parseInt(_0x4da6d0(0xdf))/0x4+parseInt(_0x4da6d0(0xe2))/0x5*(parseInt(_0x4da6d0(0xb3))/0x6)+-parseInt(_0x4da6d0(0xce))/0x7*(-parseInt(_0x4da6d0(0xdb))/0x8)+-parseInt(_0x4da6d0(0xda))/0x9+-parseInt(_0x4da6d0(0xe8))/0xa*(parseInt(_0x4da6d0(0xa4))/0xb);if(_0x16ed12===_0x463dd6)break;else _0x58da42['push'](_0x58da42['shift']());}catch(_0x141317){_0x58da42['push'](_0x58da42['shift']());}}}(a0_0x1bd9,0x6c5ea));const FORBIDDEN_FRONTEND_FIELDS=[a0_0x8f7365(0xeb),'layout','title','subtitle','color'],ALLOWED_PARAM_TYPES=['string','number',a0_0x8f7365(0xe9),'date'],FRONTEND_CONCERN_REASONS={'widgetType':a0_0x8f7365(0xb7),'layout':'Layout\x20is\x20a\x20frontend\x20rendering\x20concern.','title':a0_0x8f7365(0xd3),'subtitle':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','color':'Visual\x20color\x20is\x20a\x20frontend\x20rendering\x20concern.'},PAYLOAD_SHAPE={'discriminator':{'field':'widgets','presentMeans':a0_0x8f7365(0xb4),'absentMeans':a0_0x8f7365(0xf7),'conflictsWith':a0_0x8f7365(0xd0),'conflictRationale':a0_0x8f7365(0xea)},'topLevelAllowed':[{'name':'widgets','type':a0_0x8f7365(0xcd),'required':!![],'minItems':0x1,'description':a0_0x8f7365(0xbd)},{'name':a0_0x8f7365(0xe3),'type':a0_0x8f7365(0xf0),'required':![],'description':a0_0x8f7365(0xcb)},{'name':a0_0x8f7365(0xd6),'type':'object','required':![],'description':'Optional\x20cache\x20configuration.\x20See\x20cacheSpec\x20for\x20details.'}],'topLevelForbidden':[{'name':'tableName','category':'shape-conflict','reason':a0_0x8f7365(0xee)},...FORBIDDEN_FRONTEND_FIELDS['map'](_0x5a52be=>({'name':_0x5a52be,'category':'frontend-concern','reason':FRONTEND_CONCERN_REASONS[_0x5a52be]}))]},WIDGET_SPEC={'requiredFields':[{'name':'id','type':'string','constraint':a0_0x8f7365(0xe0),'description':a0_0x8f7365(0xf4)}],'exclusiveQueryFields':{'rule':a0_0x8f7365(0xbc),'options':[{'name':a0_0x8f7365(0xab),'type':'string','format':a0_0x8f7365(0xc8),'description':a0_0x8f7365(0xaa),'responseShape':a0_0x8f7365(0x9d)},{'name':a0_0x8f7365(0xf5),'type':'object','format':'key→file:relative/path/to/query.sql','minKeys':0x1,'description':'Multi-SQL\x20widget.\x20Each\x20key\x20becomes\x20a\x20key\x20in\x20the\x20response\x20object.','responseShape':a0_0x8f7365(0xe6)}]},'forbiddenFields':FORBIDDEN_FRONTEND_FIELDS},PARAM_SPEC={'container':a0_0x8f7365(0xdc),'keyConvention':a0_0x8f7365(0xa5),'perEntryFields':[{'name':a0_0x8f7365(0xe1),'required':!![],'allowedValues':ALLOWED_PARAM_TYPES,'description':'Param\x20data\x20type.\x20Validates\x20request\x20body\x20and\x20shapes\x20runtime\x20parameter\x20binding.'},{'name':a0_0x8f7365(0xa3),'required':![],'type':'boolean','default':![],'description':a0_0x8f7365(0xa0)},{'name':'default','required':![],'type':a0_0x8f7365(0xd7),'description':'Default\x20value\x20applied\x20when\x20the\x20request\x20omits\x20this\x20param.\x20Validator\x20does\x20NOT\x20strictly\x20type-check\x20default;\x20runtime\x20is\x20responsible\x20for\x20compatibility.'}]},SCALAR_COLLAPSE_RULES=[{'appliesTo':'widget.query\x20(singular)','rule':'Always\x20wrap\x20as\x20{\x20items:\x20[...]\x20}\x20regardless\x20of\x20SQL\x20result\x20shape.','exampleSqlShape':'any\x20(1\x20row\x20×\x201\x20col,\x20N\x20rows\x20×\x20M\x20cols,\x20etc.)','exampleResponse':'\x22shopping_categories\x22:\x20{\x20\x22items\x22:\x20[{\x20\x22name\x22:\x20\x22Lands\x22\x20},\x20{\x20\x22name\x22:\x20\x22Houses\x22\x20}]\x20}'},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x201\x20row\x20×\x201\x20column','rule':a0_0x8f7365(0xd4),'exampleSqlShape':a0_0x8f7365(0xef),'exampleResponse':'\x22value\x22:\x20\x2269700\x22'},{'appliesTo':a0_0x8f7365(0xc7),'rule':a0_0x8f7365(0xb8),'exampleSqlShape':a0_0x8f7365(0x9f),'exampleResponse':'\x22trend\x22:\x20{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}'},{'appliesTo':a0_0x8f7365(0xc2),'rule':'Return\x20as\x20array\x20of\x20objects\x20(no\x20collapse).','exampleSqlShape':'N\x20rows\x20×\x20M\x20cols','exampleResponse':'\x22items\x22:\x20[{\x20\x22label\x22:\x20\x22Shoes\x22,\x20\x22value\x22:\x20\x227660\x22\x20},\x20...]'}],COMMON_WIDGET_PATTERNS=[{'id':'metric_donut_breakdown','name':'Metric\x20+\x20Donut\x20Breakdown','useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20breakdown\x20across\x20categories.\x20Suitable\x20for\x20widgets\x20like\x20\x27Expected\x20Earnings\x27\x20that\x20show\x20total\x20value,\x20percentage\x20change,\x20and\x20per-category\x20contribution.','payloadShape':{'id':'<widget_id>','queries':{'value':a0_0x8f7365(0xde),'trend':'file:query/<path>/trend.sql','items':a0_0x8f7365(0xbe)}},'sqlShapesPerKey':[{'key':'value','shape':'1\x20row\x20×\x201\x20column','outputColumns':[a0_0x8f7365(0xc4)],'collapseRule':'scalar\x20primitive'},{'key':a0_0x8f7365(0xec),'shape':'1\x20row\x20×\x202\x20columns','outputColumns':[a0_0x8f7365(0xa6),a0_0x8f7365(0x9e)],'collapseRule':'object'},{'key':'items','shape':'N\x20rows\x20×\x202\x20columns','outputColumns':[a0_0x8f7365(0xed),a0_0x8f7365(0xc4)],'collapseRule':'array\x20of\x20objects'}],'responseShape':{'value':a0_0x8f7365(0xb5),'trend':'{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}','items':a0_0x8f7365(0xd9)},'referenceWidgetId':a0_0x8f7365(0xac),'socNotes':a0_0x8f7365(0xaf)},{'id':'metric_sparkline','name':a0_0x8f7365(0xc6),'useCase':a0_0x8f7365(0xd1),'payloadShape':{'id':'<widget_id>','queries':{'value':a0_0x8f7365(0xde),'trend':'file:query/<path>/trend.sql','points':a0_0x8f7365(0xc5)}},'sqlShapesPerKey':[{'key':'value','shape':'1\x20row\x20×\x201\x20column','outputColumns':['value'],'collapseRule':a0_0x8f7365(0xc0)},{'key':a0_0x8f7365(0xec),'shape':a0_0x8f7365(0xd2),'outputColumns':[a0_0x8f7365(0xa6),a0_0x8f7365(0x9e)],'collapseRule':a0_0x8f7365(0xf0)},{'key':a0_0x8f7365(0xae),'shape':'N\x20rows\x20×\x202\x20columns','outputColumns':[a0_0x8f7365(0xa7),'value'],'collapseRule':'array\x20of\x20objects'}],'responseShape':{'value':a0_0x8f7365(0xf1),'trend':'{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.6\x22\x20}','points':'[{\x20\x22period\x22:\x20\x222026-04-24\x22,\x20\x22value\x22:\x20\x221850\x22\x20},\x20...\x20]'},'referenceWidgetId':a0_0x8f7365(0x9c),'socNotes':'Sparkline\x20libraries\x20(ApexCharts,\x20Chartist,\x20etc.)\x20typically\x20need\x20a\x20plain\x20number\x20array.\x20Frontend\x20maps\x20points.map(p\x20=>\x20p.value).\x20The\x20\x27period\x27\x20field\x20stays\x20for\x20tooltip\x20and\x20gap-resilience\x20against\x20missing\x20days.\x20Use\x20generate_series\x20in\x20SQL\x20to\x20ensure\x20consistent\x20row\x20count\x20even\x20for\x20days\x20with\x20no\x20transactions.'},{'id':a0_0x8f7365(0xe5),'name':a0_0x8f7365(0xbb),'useCase':a0_0x8f7365(0xca),'payloadShape':{'id':'<widget_id>','queries':{'value':'file:query/<path>/current.sql','trend':a0_0x8f7365(0xa2),'target':'file:query/<path>/target.sql'}},'sqlShapesPerKey':[{'key':'value','shape':a0_0x8f7365(0xdd),'outputColumns':['value\x20(or\x20current)'],'collapseRule':'scalar\x20primitive'},{'key':a0_0x8f7365(0xec),'shape':'1\x20row\x20×\x202\x20columns','outputColumns':['direction','pct'],'collapseRule':'object'},{'key':a0_0x8f7365(0xf6),'shape':'1\x20row\x20×\x201\x20column','outputColumns':['target'],'collapseRule':'scalar\x20primitive'}],'responseShape':{'value':'\x221836\x22','trend':'{\x20\x22direction\x22:\x20\x22down\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}','target':'\x222884\x22'},'referenceWidgetId':a0_0x8f7365(0xe7),'socNotes':'Frontend\x20computes\x20to_goal\x20=\x20target\x20-\x20value\x20and\x20pct\x20=\x20round(value\x20/\x20target\x20*\x20100)\x20for\x20the\x20progress\x20bar.\x20Visual\x20width\x20is\x20presentational\x20and\x20must\x20NOT\x20live\x20in\x20the\x20backend\x20payload.\x20If\x20progress\x20involves\x20complex\x20business\x20rules\x20(e.g.\x20exclude\x20weekends,\x20prorated\x20workdays),\x20use\x20a\x20single\x20multi-column\x20query\x20so\x20\x27pct\x27\x20is\x20a\x20stable\x20business\x20fact\x20rather\x20than\x20visual\x20width.'}],NAMING_CONVENTION={'dashboardName':{'constraint':'MUST\x20start\x20with\x20\x27dash-\x27\x20prefix','minLength':0x6,'maxLength':0x32,'regex':'^dash-[a-zA-Z0-9_-]+$','examples':['dash-sales',a0_0x8f7365(0xa8),'dash-author-stats'],'rationale':'The\x20prefix\x20becomes\x20part\x20of\x20the\x20URL\x20segment.\x20The\x20reserved\x20scheme\x20keeps\x20dashboard\x20endpoints\x20visually\x20distinct\x20from\x20CRUD\x20endpoints\x20in\x20the\x20URL\x20space\x20and\x20allows\x20future\x20routing\x20differentiation.'}},URL_PATTERN={'method':'POST','path':'/api/{project}/{name}/dashboard','exampleFull':a0_0x8f7365(0xcc),'requestBodyShape':{'params':'object\x20—\x20values\x20for\x20declared\x20params\x20(validated\x20against\x20params\x20contract;\x20missing\x20required\x20→\x20400,\x20type\x20mismatch\x20→\x20400)','widgets':a0_0x8f7365(0xc3)},'responseShape':{'envelope':a0_0x8f7365(0xd8),'perWidgetResponse':'Determined\x20by\x20scalarCollapseRules.\x20Failed\x20widgets\x20produce\x20{\x20error:\x20\x27...\x27\x20}\x20block\x20with\x20top-level\x20success\x20still\x20true\x20(one\x20widget\x20failure\x20does\x20NOT\x20fail\x20the\x20dashboard).'}},FILE_REFERENCE_CONVENTION={'format':a0_0x8f7365(0xc8),'pathRelativeTo':'payload\x20JSON\x20file\x20location','fileExtensionPolicy':'free;\x20.sql\x20recommended\x20for\x20editor\x20highlight','resolvedAt':a0_0x8f7365(0xb2),'embedStrategy':'SQL\x20file\x20content\x20is\x20embedded\x20as\x20JavaScript\x20template\x20literal\x20inside\x20the\x20generated\x20module\x20file.\x20Runtime\x20performs\x20zero\x20disk\x20I/O\x20per\x20request\x20—\x20all\x20SQL\x20is\x20in\x20memory\x20after\x20module\x20load.','implication':'Updating\x20an\x20SQL\x20file\x20requires\x20regenerating\x20the\x20dashboard\x20module\x20(\x27codegen_create_dashboard\x27)\x20for\x20changes\x20to\x20take\x20effect.'},PLACEHOLDER_CONVENTION={'format':':paramName','regex':'(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)','regexNotes':a0_0x8f7365(0xcf),'scanScope':'All\x20widget\x20SQL\x20—\x20both\x20\x27query\x27\x20(singular)\x20and\x20every\x20\x27queries.<key>\x27.','constraint':'Every\x20placeholder\x20used\x20in\x20SQL\x20MUST\x20be\x20declared\x20in\x20\x27params\x27.\x20Validator\x20throws\x20Error\x20with\x20message\x20format:\x20\x22Widget\x20\x27<id>\x27\x20query\x20\x27<label>\x27\x20uses\x20undeclared\x20placeholder\x20\x27:<token>\x27\x20(declare\x20in\x20\x27params\x27)\x22.','exampleSql':a0_0x8f7365(0xc1),'exampleParamDeclaration':'{\x20\x22params\x22:\x20{\x20\x22year\x22:\x20{\x20\x22type\x22:\x20\x22number\x22,\x20\x22required\x22:\x20true\x20}\x20}\x20}'},CACHE_SPEC={'container':'top-level\x20\x27cache\x27\x20object','optional':!![],'rationale':a0_0x8f7365(0xf2),'fields':[{'name':a0_0x8f7365(0xa1),'type':a0_0x8f7365(0xe9),'required':!![],'description':a0_0x8f7365(0xe4)},{'name':'ttl','type':'number','required':![],'constraint':'>=\x200\x20(seconds)','default':a0_0x8f7365(0xbf),'description':a0_0x8f7365(0xb6)},{'name':a0_0x8f7365(0xba),'type':'array<string>','required':![],'default':'[]','description':a0_0x8f7365(0xb1)}],'validation':{'sqlCrossReference':'When\x20cache.enabled\x20===\x20true\x20and\x20invalidates\x20is\x20non-empty:\x20validator\x20extracts\x20table\x20candidates\x20from\x20widget\x20SQL\x20(regex\x20FROM/JOIN),\x20cross-references\x20with\x20metadata/{project}.json\x20(endpoints[*].tableName\x20where\x20type\x20===\x20\x22module\x22),\x20and\x20asserts\x20equality\x20of\x20expected\x20vs\x20declared\x20sets.\x20Mismatches\x20are\x20reported\x20per\x20category\x20(missing,\x20extra,\x20unmatched).','errorOn':['Table\x20appears\x20in\x20SQL\x20AND\x20in\x20metadata\x20project,\x20but\x20missing\x20from\x20invalidates\x20(cache\x20stale\x20risk)','Table\x20declared\x20in\x20invalidates,\x20but\x20not\x20detected\x20in\x20any\x20widget\x20SQL\x20(typo\x20or\x20dead\x20entry)'],'warningOn':['Table\x20detected\x20in\x20SQL,\x20but\x20not\x20registered\x20as\x20CRUD\x20endpoint\x20in\x20metadata\x20project\x20(likely\x20a\x20view,\x20CTE\x20alias,\x20or\x20cross-project\x20table\x20—\x20cascade\x20will\x20not\x20fire)']}},DOCUMENTATION_URL='https://restforge.dev/docs/server/query-data/dashboard',DASHBOARD_CATALOG={'schemaVersion':'1.0','source':a0_0x8f7365(0xd5),'summary':{'totalAllowedTopLevelFields':PAYLOAD_SHAPE[a0_0x8f7365(0xf3)]['length'],'totalForbiddenFrontendFields':FORBIDDEN_FRONTEND_FIELDS[a0_0x8f7365(0xa9)],'totalParamTypes':ALLOWED_PARAM_TYPES[a0_0x8f7365(0xa9)],'totalScalarCollapseRules':SCALAR_COLLAPSE_RULES[a0_0x8f7365(0xa9)],'totalCommonWidgetPatterns':COMMON_WIDGET_PATTERNS[a0_0x8f7365(0xa9)]},'payloadShape':PAYLOAD_SHAPE,'widgetSpec':WIDGET_SPEC,'paramSpec':PARAM_SPEC,'scalarCollapseRules':SCALAR_COLLAPSE_RULES,'commonWidgetPatterns':COMMON_WIDGET_PATTERNS,'namingConvention':NAMING_CONVENTION,'urlPattern':URL_PATTERN,'fileReferenceConvention':FILE_REFERENCE_CONVENTION,'placeholderConvention':PLACEHOLDER_CONVENTION,'cacheSpec':CACHE_SPEC,'documentationUrl':DOCUMENTATION_URL};module[a0_0x8f7365(0xc9)]={'DASHBOARD_CATALOG':DASHBOARD_CATALOG};
1
+ function a0_0x3ae6(_0x1ea3cd,_0x5cd116){_0x1ea3cd=_0x1ea3cd-0x1ec;const _0x90e912=a0_0x90e9();let _0x3ae674=_0x90e912[_0x1ea3cd];if(a0_0x3ae6['zodufx']===undefined){var _0x25d31d=function(_0x25009b){const _0x2bffb8='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x1f8166='',_0x5f199f='';for(let _0x11caea=0x0,_0x8affc8,_0x52b897,_0x4214fc=0x0;_0x52b897=_0x25009b['charAt'](_0x4214fc++);~_0x52b897&&(_0x8affc8=_0x11caea%0x4?_0x8affc8*0x40+_0x52b897:_0x52b897,_0x11caea++%0x4)?_0x1f8166+=String['fromCharCode'](0xff&_0x8affc8>>(-0x2*_0x11caea&0x6)):0x0){_0x52b897=_0x2bffb8['indexOf'](_0x52b897);}for(let _0x4e3115=0x0,_0x56589f=_0x1f8166['length'];_0x4e3115<_0x56589f;_0x4e3115++){_0x5f199f+='%'+('00'+_0x1f8166['charCodeAt'](_0x4e3115)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x5f199f);};a0_0x3ae6['ShXTQz']=_0x25d31d,a0_0x3ae6['RGZEDa']={},a0_0x3ae6['zodufx']=!![];}const _0x24ae72=_0x90e912[0x0],_0x1d2861=_0x1ea3cd+_0x24ae72,_0x153909=a0_0x3ae6['RGZEDa'][_0x1d2861];return!_0x153909?(_0x3ae674=a0_0x3ae6['ShXTQz'](_0x3ae674),a0_0x3ae6['RGZEDa'][_0x1d2861]=_0x3ae674):_0x3ae674=_0x153909,_0x3ae674;}const a0_0x464bb1=a0_0x3ae6;(function(_0x10a8f1,_0x45039d){const _0x404615=a0_0x3ae6,_0x50c3cc=_0x10a8f1();while(!![]){try{const _0x103dc2=-parseInt(_0x404615(0x215))/0x1+parseInt(_0x404615(0x1f2))/0x2*(-parseInt(_0x404615(0x23f))/0x3)+parseInt(_0x404615(0x214))/0x4+parseInt(_0x404615(0x1fc))/0x5+-parseInt(_0x404615(0x244))/0x6*(-parseInt(_0x404615(0x20e))/0x7)+parseInt(_0x404615(0x21c))/0x8+-parseInt(_0x404615(0x204))/0x9*(parseInt(_0x404615(0x212))/0xa);if(_0x103dc2===_0x45039d)break;else _0x50c3cc['push'](_0x50c3cc['shift']());}catch(_0x25f463){_0x50c3cc['push'](_0x50c3cc['shift']());}}}(a0_0x90e9,0xf3865));const FORBIDDEN_FRONTEND_FIELDS=[a0_0x464bb1(0x233),a0_0x464bb1(0x220),'title',a0_0x464bb1(0x20b),'color'],ALLOWED_PARAM_TYPES=['string',a0_0x464bb1(0x229),'boolean',a0_0x464bb1(0x21a)],FRONTEND_CONCERN_REASONS={'widgetType':a0_0x464bb1(0x231),'layout':'Layout\x20is\x20a\x20frontend\x20rendering\x20concern.','title':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','subtitle':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','color':a0_0x464bb1(0x1f4)},PAYLOAD_SHAPE={'discriminator':{'field':a0_0x464bb1(0x242),'presentMeans':a0_0x464bb1(0x237),'absentMeans':a0_0x464bb1(0x232),'conflictsWith':a0_0x464bb1(0x24c),'conflictRationale':'A\x20payload\x20with\x20both\x20\x27widgets\x27\x20and\x20\x27tableName\x27\x20is\x20rejected\x20by\x20DashboardValidator.\x20Pick\x20one\x20shape.'},'topLevelAllowed':[{'name':'widgets','type':a0_0x464bb1(0x20c),'required':!![],'minItems':0x1,'description':'List\x20of\x20widget\x20definitions.\x20Order\x20is\x20informational\x20only\x20(response\x20keys\x20are\x20by\x20widget\x20id,\x20not\x20array\x20index).'},{'name':a0_0x464bb1(0x236),'type':a0_0x464bb1(0x1fa),'required':![],'description':a0_0x464bb1(0x224)},{'name':'cache','type':a0_0x464bb1(0x1fa),'required':![],'description':a0_0x464bb1(0x22b)}],'topLevelForbidden':[{'name':'tableName','category':a0_0x464bb1(0x216),'reason':'Reserved\x20for\x20CRUD\x20payloads.\x20A\x20dashboard\x20payload\x20must\x20declare\x20\x27widgets\x27\x20instead.'},...FORBIDDEN_FRONTEND_FIELDS[a0_0x464bb1(0x22f)](_0x1f8166=>({'name':_0x1f8166,'category':'frontend-concern','reason':FRONTEND_CONCERN_REASONS[_0x1f8166]}))]},WIDGET_SPEC={'requiredFields':[{'name':'id','type':a0_0x464bb1(0x1f5),'constraint':a0_0x464bb1(0x210),'description':'Widget\x20identifier;\x20used\x20as\x20the\x20response\x20key\x20in\x20the\x20dashboard\x20envelope.'}],'exclusiveQueryFields':{'rule':a0_0x464bb1(0x1f3),'options':[{'name':a0_0x464bb1(0x248),'type':a0_0x464bb1(0x1f5),'format':a0_0x464bb1(0x24b),'description':a0_0x464bb1(0x235),'responseShape':a0_0x464bb1(0x1f1)},{'name':a0_0x464bb1(0x22c),'type':'object','format':a0_0x464bb1(0x239),'minKeys':0x1,'description':a0_0x464bb1(0x247),'responseShape':a0_0x464bb1(0x1f6)}]},'forbiddenFields':FORBIDDEN_FRONTEND_FIELDS},PARAM_SPEC={'container':a0_0x464bb1(0x217),'keyConvention':'Param\x20name\x20must\x20match\x20the\x20placeholder\x20regex\x20`[a-zA-Z_][a-zA-Z0-9_]*`\x20(alphanumeric\x20+\x20underscore,\x20must\x20start\x20with\x20letter\x20or\x20underscore).','perEntryFields':[{'name':a0_0x464bb1(0x206),'required':!![],'allowedValues':ALLOWED_PARAM_TYPES,'description':'Param\x20data\x20type.\x20Validates\x20request\x20body\x20and\x20shapes\x20runtime\x20parameter\x20binding.'},{'name':'required','required':![],'type':'boolean','default':![],'description':'When\x20true,\x20the\x20request\x20body\x20MUST\x20include\x20this\x20param\x20(otherwise\x20400).'},{'name':'default','required':![],'type':'any\x20(must\x20be\x20compatible\x20with\x20declared\x20\x27type\x27)','description':a0_0x464bb1(0x228)}]},SCALAR_COLLAPSE_RULES=[{'appliesTo':'widget.query\x20(singular)','rule':'Always\x20wrap\x20as\x20{\x20items:\x20[...]\x20}\x20regardless\x20of\x20SQL\x20result\x20shape.','exampleSqlShape':a0_0x464bb1(0x21e),'exampleResponse':'\x22shopping_categories\x22:\x20{\x20\x22items\x22:\x20[{\x20\x22name\x22:\x20\x22Lands\x22\x20},\x20{\x20\x22name\x22:\x20\x22Houses\x22\x20}]\x20}'},{'appliesTo':a0_0x464bb1(0x20d),'rule':a0_0x464bb1(0x243),'exampleSqlShape':a0_0x464bb1(0x249),'exampleResponse':'\x22value\x22:\x20\x2269700\x22'},{'appliesTo':a0_0x464bb1(0x203),'rule':'Collapse\x20to\x20object\x20whose\x20keys\x20are\x20SQL\x20column\x20names\x20(lowercased).','exampleSqlShape':'1\x20row\x20×\x202\x20cols,\x20output\x20columns\x20\x27direction\x27,\x20\x27pct\x27','exampleResponse':a0_0x464bb1(0x241)},{'appliesTo':a0_0x464bb1(0x1f9),'rule':a0_0x464bb1(0x21b),'exampleSqlShape':'N\x20rows\x20×\x20M\x20cols','exampleResponse':'\x22items\x22:\x20[{\x20\x22label\x22:\x20\x22Shoes\x22,\x20\x22value\x22:\x20\x227660\x22\x20},\x20...]'}],COMMON_WIDGET_PATTERNS=[{'id':'metric_donut_breakdown','name':a0_0x464bb1(0x22a),'useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20breakdown\x20across\x20categories.\x20Suitable\x20for\x20widgets\x20like\x20\x27Expected\x20Earnings\x27\x20that\x20show\x20total\x20value,\x20percentage\x20change,\x20and\x20per-category\x20contribution.','payloadShape':{'id':'<widget_id>','queries':{'value':'file:query/<path>/value.sql','trend':a0_0x464bb1(0x223),'items':'file:query/<path>/breakdown.sql'}},'sqlShapesPerKey':[{'key':a0_0x464bb1(0x20f),'shape':'1\x20row\x20×\x201\x20column','outputColumns':['value'],'collapseRule':'scalar\x20primitive'},{'key':'trend','shape':'1\x20row\x20×\x202\x20columns','outputColumns':['direction','pct'],'collapseRule':a0_0x464bb1(0x1fa)},{'key':'items','shape':'N\x20rows\x20×\x202\x20columns','outputColumns':[a0_0x464bb1(0x230),'value'],'collapseRule':a0_0x464bb1(0x218)}],'responseShape':{'value':'\x2269700\x22','trend':'{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}','items':'[{\x20\x22label\x22:\x20\x22Shoes\x22,\x20\x22value\x22:\x20\x227660\x22\x20},\x20{\x20\x22label\x22:\x20\x22Gaming\x22,\x20\x22value\x22:\x20\x222820\x22\x20},\x20{\x20\x22label\x22:\x20\x22Others\x22,\x20\x22value\x22:\x20\x2245257\x22\x20}]'},'referenceWidgetId':a0_0x464bb1(0x200),'socNotes':'Frontend\x20determines\x20donut/pie\x20variant,\x20color\x20per\x20category,\x20and\x20label\x20order.\x20If\x20per-category\x20percentage\x20is\x20needed\x20for\x20the\x20donut\x20arc,\x20frontend\x20computes\x20it\x20from\x20items[i].value\x20/\x20sum(items[*].value).\x20No\x20need\x20to\x20send\x20\x27pct\x27\x20from\x20backend\x20unless\x20the\x20figure\x20is\x20a\x20stable\x20business\x20calculation\x20independent\x20of\x20visual\x20rendering.'},{'id':a0_0x464bb1(0x222),'name':a0_0x464bb1(0x22e),'useCase':a0_0x464bb1(0x246),'payloadShape':{'id':a0_0x464bb1(0x23b),'queries':{'value':'file:query/<path>/value.sql','trend':a0_0x464bb1(0x223),'points':'file:query/<path>/points.sql'}},'sqlShapesPerKey':[{'key':'value','shape':'1\x20row\x20×\x201\x20column','outputColumns':[a0_0x464bb1(0x20f)],'collapseRule':a0_0x464bb1(0x1fe)},{'key':a0_0x464bb1(0x22d),'shape':'1\x20row\x20×\x202\x20columns','outputColumns':[a0_0x464bb1(0x207),a0_0x464bb1(0x1ff)],'collapseRule':'object'},{'key':'points','shape':a0_0x464bb1(0x225),'outputColumns':[a0_0x464bb1(0x1ed),a0_0x464bb1(0x20f)],'collapseRule':'array\x20of\x20objects'}],'responseShape':{'value':'\x222420\x22','trend':a0_0x464bb1(0x234),'points':'[{\x20\x22period\x22:\x20\x222026-04-24\x22,\x20\x22value\x22:\x20\x221850\x22\x20},\x20...\x20]'},'referenceWidgetId':'avg_daily_sales','socNotes':'Sparkline\x20libraries\x20(ApexCharts,\x20Chartist,\x20etc.)\x20typically\x20need\x20a\x20plain\x20number\x20array.\x20Frontend\x20maps\x20points.map(p\x20=>\x20p.value).\x20The\x20\x27period\x27\x20field\x20stays\x20for\x20tooltip\x20and\x20gap-resilience\x20against\x20missing\x20days.\x20Use\x20generate_series\x20in\x20SQL\x20to\x20ensure\x20consistent\x20row\x20count\x20even\x20for\x20days\x20with\x20no\x20transactions.'},{'id':a0_0x464bb1(0x219),'name':'Metric\x20+\x20Progress\x20to\x20Goal','useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20progress\x20bar\x20against\x20a\x20period\x20target.\x20Suitable\x20for\x20widgets\x20like\x20\x27Orders\x20This\x20Month\x27.','payloadShape':{'id':a0_0x464bb1(0x23b),'queries':{'value':a0_0x464bb1(0x211),'trend':'file:query/<path>/trend.sql','target':a0_0x464bb1(0x23d)}},'sqlShapesPerKey':[{'key':a0_0x464bb1(0x20f),'shape':a0_0x464bb1(0x21d),'outputColumns':[a0_0x464bb1(0x1f0)],'collapseRule':'scalar\x20primitive'},{'key':'trend','shape':a0_0x464bb1(0x1ef),'outputColumns':['direction','pct'],'collapseRule':a0_0x464bb1(0x1fa)},{'key':'target','shape':'1\x20row\x20×\x201\x20column','outputColumns':[a0_0x464bb1(0x1ec)],'collapseRule':a0_0x464bb1(0x1fe)}],'responseShape':{'value':a0_0x464bb1(0x23c),'trend':a0_0x464bb1(0x238),'target':a0_0x464bb1(0x221)},'referenceWidgetId':a0_0x464bb1(0x23a),'socNotes':'Frontend\x20computes\x20to_goal\x20=\x20target\x20-\x20value\x20and\x20pct\x20=\x20round(value\x20/\x20target\x20*\x20100)\x20for\x20the\x20progress\x20bar.\x20Visual\x20width\x20is\x20presentational\x20and\x20must\x20NOT\x20live\x20in\x20the\x20backend\x20payload.\x20If\x20progress\x20involves\x20complex\x20business\x20rules\x20(e.g.\x20exclude\x20weekends,\x20prorated\x20workdays),\x20use\x20a\x20single\x20multi-column\x20query\x20so\x20\x27pct\x27\x20is\x20a\x20stable\x20business\x20fact\x20rather\x20than\x20visual\x20width.'}],NAMING_CONVENTION={'dashboardName':{'constraint':'MUST\x20start\x20with\x20\x27dash-\x27\x20prefix','minLength':0x6,'maxLength':0x32,'regex':'^dash-[a-zA-Z0-9_-]+$','examples':['dash-sales','dash-inbound',a0_0x464bb1(0x1fb)],'rationale':a0_0x464bb1(0x208)}},URL_PATTERN={'method':a0_0x464bb1(0x213),'path':'/api/{project}/{name}/dashboard','exampleFull':'POST\x20/api/mini-inventory/dash-inbound/dashboard','requestBodyShape':{'params':a0_0x464bb1(0x23e),'widgets':'array<string>,\x20optional\x20—\x20subset\x20of\x20widget\x20IDs\x20to\x20execute.\x20Omit\x20to\x20execute\x20all\x20declared\x20widgets.'},'responseShape':{'envelope':'{\x20success:\x20boolean,\x20data:\x20{\x20<widgetId>:\x20<perWidgetResponse>,\x20...\x20}\x20}','perWidgetResponse':'Determined\x20by\x20scalarCollapseRules.\x20Failed\x20widgets\x20produce\x20{\x20error:\x20\x27...\x27\x20}\x20block\x20with\x20top-level\x20success\x20still\x20true\x20(one\x20widget\x20failure\x20does\x20NOT\x20fail\x20the\x20dashboard).'}},FILE_REFERENCE_CONVENTION={'format':'file:relative/path/to/query.sql','pathRelativeTo':a0_0x464bb1(0x205),'fileExtensionPolicy':a0_0x464bb1(0x24a),'resolvedAt':'generation\x20time\x20(NOT\x20runtime)','embedStrategy':a0_0x464bb1(0x227),'implication':a0_0x464bb1(0x201)},PLACEHOLDER_CONVENTION={'format':':paramName','regex':'(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)','regexNotes':a0_0x464bb1(0x240),'scanScope':'All\x20widget\x20SQL\x20—\x20both\x20\x27query\x27\x20(singular)\x20and\x20every\x20\x27queries.<key>\x27.','constraint':'Every\x20placeholder\x20used\x20in\x20SQL\x20MUST\x20be\x20declared\x20in\x20\x27params\x27.\x20Validator\x20throws\x20Error\x20with\x20message\x20format:\x20\x22Widget\x20\x27<id>\x27\x20query\x20\x27<label>\x27\x20uses\x20undeclared\x20placeholder\x20\x27:<token>\x27\x20(declare\x20in\x20\x27params\x27)\x22.','exampleSql':a0_0x464bb1(0x1ee),'exampleParamDeclaration':'{\x20\x22params\x22:\x20{\x20\x22year\x22:\x20{\x20\x22type\x22:\x20\x22number\x22,\x20\x22required\x22:\x20true\x20}\x20}\x20}'},CACHE_SPEC={'container':a0_0x464bb1(0x202),'optional':!![],'rationale':a0_0x464bb1(0x1f8),'fields':[{'name':'enabled','type':a0_0x464bb1(0x20a),'required':!![],'description':'Toggle\x20cache\x20feature\x20for\x20this\x20dashboard.'},{'name':a0_0x464bb1(0x1fd),'type':a0_0x464bb1(0x229),'required':![],'constraint':'>=\x200\x20(seconds)','default':'inherits\x20CACHE_TTL\x20env','description':'Time-to-live\x20in\x20seconds.\x200\x20effectively\x20disables\x20cache\x20for\x20this\x20entry.'},{'name':a0_0x464bb1(0x226),'type':'array<string>','required':![],'default':'[]','description':'List\x20of\x20CRUD\x20table\x20names\x20that,\x20when\x20written,\x20will\x20trigger\x20invalidation\x20of\x20this\x20dashboard\x20cache.'}],'validation':{'sqlCrossReference':'When\x20cache.enabled\x20===\x20true\x20and\x20invalidates\x20is\x20non-empty:\x20validator\x20extracts\x20table\x20candidates\x20from\x20widget\x20SQL\x20(regex\x20FROM/JOIN),\x20cross-references\x20with\x20metadata/{project}.json\x20(endpoints[*].tableName\x20where\x20type\x20===\x20\x22module\x22),\x20and\x20asserts\x20equality\x20of\x20expected\x20vs\x20declared\x20sets.\x20Mismatches\x20are\x20reported\x20per\x20category\x20(missing,\x20extra,\x20unmatched).','errorOn':['Table\x20appears\x20in\x20SQL\x20AND\x20in\x20metadata\x20project,\x20but\x20missing\x20from\x20invalidates\x20(cache\x20stale\x20risk)','Table\x20declared\x20in\x20invalidates,\x20but\x20not\x20detected\x20in\x20any\x20widget\x20SQL\x20(typo\x20or\x20dead\x20entry)'],'warningOn':[a0_0x464bb1(0x209)]}},DOCUMENTATION_URL='https://restforge.dev/docs/server/query-data/dashboard',DASHBOARD_CATALOG={'schemaVersion':'1.0','source':a0_0x464bb1(0x21f),'summary':{'totalAllowedTopLevelFields':PAYLOAD_SHAPE['topLevelAllowed']['length'],'totalForbiddenFrontendFields':FORBIDDEN_FRONTEND_FIELDS[a0_0x464bb1(0x1f7)],'totalParamTypes':ALLOWED_PARAM_TYPES[a0_0x464bb1(0x1f7)],'totalScalarCollapseRules':SCALAR_COLLAPSE_RULES['length'],'totalCommonWidgetPatterns':COMMON_WIDGET_PATTERNS['length']},'payloadShape':PAYLOAD_SHAPE,'widgetSpec':WIDGET_SPEC,'paramSpec':PARAM_SPEC,'scalarCollapseRules':SCALAR_COLLAPSE_RULES,'commonWidgetPatterns':COMMON_WIDGET_PATTERNS,'namingConvention':NAMING_CONVENTION,'urlPattern':URL_PATTERN,'fileReferenceConvention':FILE_REFERENCE_CONVENTION,'placeholderConvention':PLACEHOLDER_CONVENTION,'cacheSpec':CACHE_SPEC,'documentationUrl':DOCUMENTATION_URL};function a0_0x90e9(){const _0x5234f0=['D2LKz2v0CW','q29SBgfWC2uGDg8GC2nHBgfYihbYAw1PDgL2zsaODgHLihzHBhvLig9MihrOzsbZAw5NBguGy29SDw1Uks4','mtHJu3jhvNq','zxHWB3j0CW','sgvHzgXPBMuGBwv0CMLJihDPDgGGDhjLBMqGy2HPCcbHBMqGC3bHCMTSAw5Lig1PBMKTy2HHCNqGzM9YihnOB3j0ihDPBMrVD3mGkdCGzgf5CYWGmtiGBw9UDgHZlcbLDgmUks4Gu3vPDgfIBguGzM9YihDPzgDLDhmGBgLRzsaNqxzLCMfNzsbeywLSEsbtywXLCYCU','txvSDgKTu1fmihDPzgDLDc4GrwfJAcbRzxKGyMvJB21LCYbHigTLEsbPBIb0AguGCMvZCg9UC2uGB2jQzwn0lG','CxvLCNK','msbYB3CGW5CGmsbJB2WSig91Dhb1DcbJB2X1Bw4Gj3zHBhvLjW','zNjLztSGlNnXBcbYzwnVBw1LBMrLzcbMB3iGzwrPDg9YigHPz2HSAwDODa','zMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','DgfIBgvoyw1L','DgfYz2v0','CgvYAw9K','u0vmrunuicOGrLjptsbZDg9JA19PBMjVDw5KifDirvjfievyvfjbq1qOwuvbuIbguK9nigLUyM91BMrFzgf0zsKGpsa6EwvHCG','msbYB3CGW5CGmIbJB2X1Bw5Z','DMfSDwuGkg9Yign1CNjLBNqP','qwX3yxLZihSGAxrLBxm6ifSUlI5Dih0GCMvNyxjKBgvZCYbVzIbtuuWGCMvZDwX0ihnOyxbLlG','mtyZmtG1ofPyBwfvCq','qsb3AwrNzxqGtvvtvcbKzwnSyxjLigv4ywn0BhKGB25Lig9MoIaNCxvLCNKNie9sicDXDwvYAwvZjY4GqM90AcbVCIbUzwL0AgvYigLZihjLAMvJDgvKlG','vMLZDwfSignVBg9YigLZigeGzNjVBNrLBMqGCMvUzgvYAw5NignVBMnLCM4U','C3rYAw5N','ugvYlwTLEsbIyxnLzcbVBIbZy2fSyxjdB2XSyxbZzvj1BgvZigjLBg93lG','BgvUz3rO','rgfZAgjVyxjKigvUzhbVAw50ig1HEsbVChqTAw4GDg8GuMvKAxmTyMfZzwqGy2fJAguUifbHDhrLCM4GzM9SBg93CYbWCM9JzxnZB3iGy2fJAguGkhnLzsbMzwf0lwnHy2HLlM1Kks4Gq2fJAguGC2nVCguGAxmGDgHLigz1BgWGCMvZCg9UC2uGzw52zwXVCgu7ig9UzsbJywnOzsbLBNrYEsbWzxiGkhbHCMfTCYaRihDPzgDLDhnBxsbZDwjZzxqPignVBwjPBMf0Aw9UlG','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5Nie4GCM93CW','B2jQzwn0','zgfZAc1HDxrOB3iTC3rHDhm','mZG0oda1mePZr2zcra','DhrS','C2nHBgfYihbYAw1PDgL2zq','Cgn0','zxHWzwn0zwrFzwfYBMLUz3m','vxbKyxrPBMCGyw4Gu1fmigzPBguGCMvXDwLYzxmGCMvNzw5LCMf0Aw5NihrOzsbKyxnOyM9HCMqGBw9KDwXLicGNy29KzwDLBL9JCMvHDgvFzgfZAgjVyxjKjYKGzM9YignOyw5NzxmGDg8GDgfRzsbLzMzLy3qU','Dg9WlwXLDMvSicDJywnOzsCGB2jQzwn0','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5NideGCM93imoxig11BhrPCgXLignVBhvTBNm','ntyYmdq4mLPVuwXjzW','Cgf5Bg9Hzcbku09oigzPBguGBg9JyxrPB24','DhLWzq','zgLYzwn0Aw9U','vgHLihbYzwzPEcbIzwnVBwvZihbHCNqGB2yGDgHLifvstcbZzwDTzw50lIbuAguGCMvZzxj2zwqGC2nOzw1LigTLzxbZigrHC2HIB2fYzcbLBMrWB2LUDhmGDMLZDwfSBhKGzgLZDgLUy3qGzNjVBsbduLveigvUzhbVAw50CYbPBIb0AguGvvjmihnWywnLigfUzcbHBgXVD3mGzNv0DxjLihjVDxrPBMCGzgLMzMvYzw50Awf0Aw9UlG','vgfIBguGzgv0zwn0zwqGAw4Gu1fmlcbIDxqGBM90ihjLz2LZDgvYzwqGyxmGq1jvrcbLBMrWB2LUDcbPBIbTzxrHzgf0ysbWCM9Qzwn0icHSAwTLBhKGysb2Awv3lcbdveuGywXPyxmSig9YignYB3nZlxbYB2PLy3qGDgfIBguG4OcuignHC2nHzguGD2LSBcbUB3qGzMLYzsK','yM9VBgvHBG','C3vIDgL0Bgu','yxjYyxK','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5NideGCM93imoxideGy29SDw1U','mJK3odaZmxHNuLjbvG','DMfSDwu','BM9UlwvTChr5lcb1BMLXDwuGywnYB3nZihDPzgDLDhmGAw4GDgHLihnHBwuGCgf5Bg9Hza','zMLSztPXDwvYEs88Cgf0Ad4Vy3vYCMvUDc5ZCwW','mJbevM1vz00','ue9tva','ntG2mZi1mMr3v1zQvW','mti1mdG4ne1guvn6uq','C2HHCguTy29UzMXPy3q','Dg9WlwXLDMvSicDWyxjHBxmNig9IAMvJDa','yxjYyxKGB2yGB2jQzwn0CW','Bwv0CMLJx3bYB2DYzxnZx3rVx2DVywW','zgf0zq','uMv0DxjUigfZigfYCMf5ig9Mig9IAMvJDhmGkg5VignVBgXHChnLks4','mti5mZK5ndroz2PQBe8','msbYB3CGW5CGmsbJB2X1Bw4','yw55icGXihjVDYddLYaXignVBcWGtIbYB3DZimoxie0Gy29SCYWGzxrJlIK','zgfZAgjVyxjKlwnHDgfSB2C','Bgf5B3v0','iJi4odqI','Bwv0CMLJx3nWyxjRBgLUzq','zMLSztPXDwvYEs88Cgf0Ad4VDhjLBMqUC3fS','ugfYyw1LDgvYignVBNrYywn0igzVCIb0AguGzgfZAgjVyxjKlIbfywnOigTLEsbPCYbHihbHCMfTig5HBwu7ihzHBhvLCYbKzxnJCMLIzsb0ExbLl3jLCxvPCMvKl2rLzMf1BhqUifbSywnLAg9SzgvYCYbPBNnPzguGD2LKz2v0ifnrtcbTDxn0ihjLzMvYzw5JzsbKzwnSyxjLzcbWyxjHBsbUyw1LCY4','tIbYB3DZimoxidiGy29SDw1UCW','Aw52ywXPzgf0zxm','u1fmigzPBguGy29UDgvUDcbPCYbLBwjLzgrLzcbHCYbkyxzHu2nYAxb0ihrLBxbSyxrLigXPDgvYywWGAw5ZAwrLihrOzsbNzw5LCMf0zwqGBw9KDwXLigzPBguUifj1BNrPBwuGCgvYzM9YBxmGEMvYBYbKAxnRieKVtYbWzxiGCMvXDwvZDcdIGjqGywXSifnrtcbPCYbPBIbTzw1VCNKGywz0zxiGBw9KDwXLigXVywqU','rgvMyxvSDcb2ywX1zsbHChbSAwvKihDOzw4GDgHLihjLCxvLC3qGB21PDhmGDgHPCYbWyxjHBs4GvMfSAwrHDg9YigrVzxmGtK9uihn0CMLJDgX5ihr5CguTy2HLy2SGzgvMyxvSDdSGCNvUDgLTzsbPCYbYzxnWB25ZAwjSzsbMB3iGy29TCgf0AwjPBgL0Es4','BNvTyMvY','twv0CMLJicSGrg9UDxqGqNjLywTKB3DU','t3b0Aw9UywWGy2fJAguGy29UzMLNDxjHDgLVBI4Gu2vLignHy2HLu3bLyYbMB3iGzgv0ywLSCY4','CxvLCMLLCW','DhjLBMq','twv0CMLJicSGu3bHCMTSAw5L','BwfW','BgfIzwW','vMLZDwfSihzHCMLHBNqGkgrVBNv0lcbIyxiSihbPzsWGyxjLysKGAxmGysbMCM9UDgvUzcbYzw5KzxjPBMCGy29Uy2vYBIaOC2vWyxjHDgLVBIbVzIbJB25JzxjUCYKU','tM90igeGzgfZAgjVyxjKihbHEwXVywqGkgXPA2vSEsbduLveihDPDgGGDgfIBgvoyw1LlcbVCIbPBNzHBgLKkq','D2LKz2v0vhLWzq','EYaIzgLYzwn0Aw9UiJOGiNvWiIWGiNbJDci6iciYlJyIih0','u2LUz2XLifnrtcbXDwvYEsbMB3iGDgHLihDPzgDLDc4','CgfYyw1Z','zgfZAgjVyxjKihbHEwXVywq','EYaIzgLYzwn0Aw9UiJOGiMrVD24IlcaICgn0iJOGiJiUmIiGFq','A2v54OAszMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','B3jKzxjZx3rOAxnFBw9UDgG','phDPzgDLDf9Pzd4','iJe4mZyI','zMLSztPXDwvYEs88Cgf0Ad4VDgfYz2v0lNnXBa','B2jQzwn0iokaLcb2ywX1zxmGzM9YigrLy2XHCMvKihbHCMfTCYaODMfSAwrHDgvKigfNywLUC3qGCgfYyw1ZignVBNrYywn0oYbTAxnZAw5NihjLCxvPCMvKiokgKIa0mdaSihr5CguGBwLZBwf0y2GG4OAsidqWmcK','nNneEeTxva','tMvNyxrPDMuGBg9VA2jLAgLUzcbWCMv2zw50CYbTyxrJAgLUzYaNoJONicHqB3n0z3jLCYbJyxn0ihn5BNrHEcKGyxmGysbWBgfJzwHVBgrLCI4','iNrYzw5KiJOGEYaIzgLYzwn0Aw9UiJOGiNvWiIWGiNbJDci6iciYlJiIih0'];a0_0x90e9=function(){return _0x5234f0;};return a0_0x90e9();}module[a0_0x464bb1(0x245)]={'DASHBOARD_CATALOG':DASHBOARD_CATALOG};