@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,187 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Migrate Runner — orchestrator untuk command `payload migrate`.
5
+ *
6
+ * Tanggung jawab:
7
+ * 1. Resolve file config (.env) via cascade lookup standar platform
8
+ * 2. Baca SERVER_ADDRESS dan SERVER_PORT dari config → konstruksi apiBaseUrl
9
+ * dengan format `http://{host}:{port}/api/{project}`
10
+ * 3. Load file payload backend (RDF) dari --name (relative ke cwd/payload/
11
+ * atau absolute)
12
+ * 4. Jalankan migrasi RDF → UDF via BackendPayloadMigrator
13
+ * 5. Tulis output JSON ke directory --output (file name = basename tanpa ext +
14
+ * suffix `-app.json`)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const { resolveConfig, printDefaultConfigWarning } = require('../utils/config-resolver');
21
+ const { readEnvFile } = require('../utils/env-manager');
22
+ const { migrate } = require('./backend-payload-migrator');
23
+
24
+ const DEFAULT_APP_NAME = 'My Application';
25
+ const DEFAULT_APP_CODE_FALLBACK = 'my-app';
26
+ const DEFAULT_PLUGIN = 'vanilla-js-basic';
27
+ const DEFAULT_HOST = '127.0.0.1';
28
+ const DEFAULT_BACKEND_PORT = 3000;
29
+ const DEFAULT_FRONTEND_PORT = 8000;
30
+
31
+ function resolveInputPath(nameArg, cwd) {
32
+ if (path.isAbsolute(nameArg)) return nameArg;
33
+ const direct = path.resolve(cwd, nameArg);
34
+ if (fs.existsSync(direct)) return direct;
35
+ const inPayload = path.resolve(cwd, 'payload', nameArg);
36
+ if (fs.existsSync(inPayload)) return inPayload;
37
+ // Fallback: return cwd-resolved path so caller dapat tampilkan error path
38
+ return direct;
39
+ }
40
+
41
+ function buildOutputPath(outputArg, inputBaseName, cwd) {
42
+ const targetFileName = inputBaseName;
43
+
44
+ if (!outputArg) {
45
+ const defaultDir = path.resolve(cwd, 'frontend', 'payload');
46
+ return path.join(defaultDir, targetFileName);
47
+ }
48
+
49
+ const abs = path.isAbsolute(outputArg) ? outputArg : path.resolve(cwd, outputArg);
50
+
51
+ // Treat as file path bila berakhiran .json (case insensitive)
52
+ if (/\.json$/i.test(abs)) return abs;
53
+
54
+ // Else treat as directory
55
+ return path.join(abs, targetFileName);
56
+ }
57
+
58
+ function readServerConfig(envFilePath) {
59
+ const { data } = readEnvFile(envFilePath);
60
+ const host = (typeof data.SERVER_ADDRESS === 'string' && data.SERVER_ADDRESS.length > 0)
61
+ ? data.SERVER_ADDRESS
62
+ : DEFAULT_HOST;
63
+ const portRaw = (typeof data.SERVER_PORT === 'string' && data.SERVER_PORT.length > 0)
64
+ ? data.SERVER_PORT
65
+ : '';
66
+ const portNum = parseInt(portRaw, 10);
67
+ const port = Number.isFinite(portNum) && portNum > 0 ? portNum : DEFAULT_BACKEND_PORT;
68
+ return { host, port };
69
+ }
70
+
71
+ function buildApiBaseUrl(host, port, project) {
72
+ const projectSegment = project ? `/${project}` : '';
73
+ return `http://${host}:${port}/api${projectSegment}`;
74
+ }
75
+
76
+ async function run(args) {
77
+ const cwd = process.cwd();
78
+
79
+ if (!args.name || typeof args.name !== 'string') {
80
+ const err = new Error('Flag --name is required (backend payload file name, e.g. visitors.json)');
81
+ err.exitCode = 2;
82
+ throw err;
83
+ }
84
+ if (!args.project || typeof args.project !== 'string') {
85
+ const err = new Error('Flag --project is required (project name used in apiBaseUrl)');
86
+ err.exitCode = 2;
87
+ throw err;
88
+ }
89
+
90
+ const resolved = resolveConfig(args.config, cwd);
91
+ if (!resolved) {
92
+ const err = new Error('Flag --config is required, or set a default config via `restforge config set-default`');
93
+ err.exitCode = 2;
94
+ throw err;
95
+ }
96
+ if (resolved.source === 'default') {
97
+ printDefaultConfigWarning(resolved.defaultName);
98
+ }
99
+ if (!fs.existsSync(resolved.path)) {
100
+ const err = new Error(`Config file not found: ${resolved.path}`);
101
+ err.exitCode = 3;
102
+ throw err;
103
+ }
104
+
105
+ const { host, port: backendPort } = readServerConfig(resolved.path);
106
+ const project = args.project;
107
+ const apiBaseUrl = buildApiBaseUrl(host, backendPort, project);
108
+
109
+ const frontendPortArg = (typeof args.port === 'number' && Number.isFinite(args.port) && args.port > 0)
110
+ ? args.port
111
+ : DEFAULT_FRONTEND_PORT;
112
+
113
+ const appName = args.appName || DEFAULT_APP_NAME;
114
+ const appCode = args.appCode || project || DEFAULT_APP_CODE_FALLBACK;
115
+ const plugin = args.plugin || DEFAULT_PLUGIN;
116
+
117
+ const inputPath = resolveInputPath(args.name, cwd);
118
+ if (!fs.existsSync(inputPath)) {
119
+ const err = new Error(`Input payload file not found: ${inputPath}`);
120
+ err.exitCode = 3;
121
+ throw err;
122
+ }
123
+ let backendPayload;
124
+ try {
125
+ const content = fs.readFileSync(inputPath, 'utf8');
126
+ backendPayload = JSON.parse(content);
127
+ } catch (e) {
128
+ const err = new Error(`Failed to parse JSON from ${inputPath}: ${e.message}`);
129
+ err.exitCode = 3;
130
+ throw err;
131
+ }
132
+
133
+ const inputBaseName = path.basename(inputPath);
134
+ const outputPath = buildOutputPath(args.output, inputBaseName, cwd);
135
+
136
+ if (fs.existsSync(outputPath) && !args.overwrite) {
137
+ const err = new Error(`Output file already exists: ${outputPath}. Use --overwrite to replace.`);
138
+ err.exitCode = 1;
139
+ throw err;
140
+ }
141
+
142
+ const result = migrate([backendPayload], appName, appCode, plugin, apiBaseUrl, frontendPortArg);
143
+ if (!result.success) {
144
+ const msg = (result.errors && result.errors.length > 0)
145
+ ? result.errors.join('; ')
146
+ : 'Migration failed without details';
147
+ const err = new Error(msg);
148
+ err.exitCode = 1;
149
+ throw err;
150
+ }
151
+
152
+ const outputDir = path.dirname(outputPath);
153
+ if (!fs.existsSync(outputDir)) {
154
+ fs.mkdirSync(outputDir, { recursive: true });
155
+ }
156
+ fs.writeFileSync(outputPath, JSON.stringify(result.payload, null, 2), 'utf8');
157
+
158
+ const stdout = process.stdout;
159
+ stdout.write('============================================================\n');
160
+ stdout.write('PAYLOAD MIGRATE - RDF (backend) -> UDF (frontend)\n');
161
+ stdout.write('============================================================\n\n');
162
+ stdout.write(` Input : ${inputPath}\n`);
163
+ stdout.write(` Output : ${outputPath}\n`);
164
+ stdout.write(` Project : ${project}\n`);
165
+ stdout.write(` apiBaseUrl : ${apiBaseUrl}\n`);
166
+ stdout.write(` Backend port : ${backendPort}\n`);
167
+ stdout.write(` Frontend port: ${frontendPortArg}\n`);
168
+ stdout.write(` Pages : ${result.pageResults.length}\n\n`);
169
+ for (const pr of result.pageResults) {
170
+ stdout.write(` [OK] ${pr.pageId}: ${pr.fieldCount} field(s), ${pr.tableColCount} table column(s)\n`);
171
+ }
172
+ if (result.warnings && result.warnings.length > 0) {
173
+ stdout.write('\n Warnings:\n');
174
+ for (const w of result.warnings) {
175
+ stdout.write(` - ${w}\n`);
176
+ }
177
+ }
178
+ stdout.write('\n Migration completed successfully.\n');
179
+ }
180
+
181
+ module.exports = {
182
+ run,
183
+ resolveInputPath,
184
+ buildOutputPath,
185
+ readServerConfig,
186
+ buildApiBaseUrl
187
+ };
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/utils/naming.rs.
5
+ * Konversi naming convention: snake_case -> kebab-case / camelCase / PascalCase / Title Case.
6
+ */
7
+
8
+ function capitalize(s) {
9
+ if (!s || s.length === 0) return '';
10
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
11
+ }
12
+
13
+ function snakeToKebab(name) {
14
+ return String(name || '').replace(/_/g, '-');
15
+ }
16
+
17
+ function snakeToCamel(name) {
18
+ const parts = String(name || '').split('_');
19
+ if (parts.length === 0) return '';
20
+ const first = parts.shift();
21
+ return first + parts.map(capitalize).join('');
22
+ }
23
+
24
+ function snakeToPascal(name) {
25
+ return String(name || '').split('_').map(capitalize).join('');
26
+ }
27
+
28
+ function snakeToTitle(name) {
29
+ return String(name || '').split('_').map(capitalize).join(' ');
30
+ }
31
+
32
+ function toClassName(appCode) {
33
+ return String(appCode || '').replace(/_/g, '-').split('-').map(capitalize).join('');
34
+ }
35
+
36
+ module.exports = {
37
+ capitalize,
38
+ snakeToKebab,
39
+ snakeToCamel,
40
+ snakeToPascal,
41
+ snakeToTitle,
42
+ toClassName
43
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Port dari packages/designer/src/migrators/sql_parser.rs.
5
+ * Regex-based parser untuk SELECT/JOIN dari `datatablesQuery` string.
6
+ */
7
+
8
+ const SELECT_RE = /select\s+([\s\S]*?)\s+from\s+/i;
9
+ const FROM_RE = /\bfrom\s+([\s\S]*)/i;
10
+ const JOIN_SPLIT_RE = /\b(left\s+join|inner\s+join|right\s+join|cross\s+join|join)\b/i;
11
+ const JOIN_SPLIT_RE_GLOBAL = /\b(left\s+join|inner\s+join|right\s+join|cross\s+join|join)\b/ig;
12
+ const WHERE_SPLIT_RE = /\s+where\s+/i;
13
+ const ORDER_SPLIT_RE = /\s+order\s+/i;
14
+ const ON_CLAUSE_RE = /([\s\S]*?)\s+on\s+([\s\S]*)/i;
15
+ const JOIN_COND_RE = /(\w+)\.(\w+)\s*=\s*(\w+)\.(\w+)/;
16
+
17
+ function stripSchema(name) {
18
+ const idx = name.lastIndexOf('.');
19
+ return idx >= 0 ? name.slice(idx + 1) : name;
20
+ }
21
+
22
+ function parseTableRef(refstr) {
23
+ const afterWhere = refstr.split(WHERE_SPLIT_RE)[0].trim();
24
+ const cleaned = afterWhere.split(ORDER_SPLIT_RE)[0].trim();
25
+ const tokens = cleaned.split(/\s+/).filter(Boolean);
26
+ if (tokens.length === 0) return { tableName: '', alias: '' };
27
+ const tableName = stripSchema(tokens[0]);
28
+ const alias = tokens.length > 1 ? tokens[1] : '';
29
+ return { tableName, alias };
30
+ }
31
+
32
+ function parseJoinCondition(condition, mainAlias) {
33
+ const m = JOIN_COND_RE.exec(condition.trim());
34
+ if (!m) return { localColumn: '', remoteColumn: '' };
35
+ const alias1 = m[1];
36
+ const col1 = m[2];
37
+ const col2 = m[4];
38
+ if (alias1 === mainAlias) {
39
+ return { localColumn: col1, remoteColumn: col2 };
40
+ }
41
+ return { localColumn: col2, remoteColumn: col1 };
42
+ }
43
+
44
+ function parseDatatablesQuery(sql) {
45
+ const trimmed = String(sql || '').trim();
46
+ if (!trimmed) {
47
+ return { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
48
+ }
49
+ const sqlClean = trimmed.replace(/;+\s*$/, '');
50
+
51
+ const selectMatch = SELECT_RE.exec(sqlClean);
52
+ if (!selectMatch) {
53
+ return { selectColumns: [], joins: [], mainTable: '', mainAlias: '' };
54
+ }
55
+
56
+ const columnsStr = selectMatch[1];
57
+ const rawColumns = columnsStr.split(',');
58
+ const selectColumns = [];
59
+ for (let i = 0; i < rawColumns.length; i++) {
60
+ const col = rawColumns[i].trim();
61
+ if (!col) continue;
62
+ const parts = col.split('.');
63
+ let tableAlias = '';
64
+ let name;
65
+ if (parts.length >= 2) {
66
+ tableAlias = parts[parts.length - 2].trim();
67
+ name = parts[parts.length - 1].trim();
68
+ } else {
69
+ name = parts[0].trim();
70
+ }
71
+ selectColumns.push({ name, tableAlias, position: i + 1 });
72
+ }
73
+
74
+ const fromMatch = FROM_RE.exec(sqlClean);
75
+ if (!fromMatch) {
76
+ return { selectColumns, joins: [], mainTable: '', mainAlias: '' };
77
+ }
78
+ const fromClause = fromMatch[1];
79
+
80
+ // Python re.split with capture group returns [before, kw, body, kw, body, ...]
81
+ const parts = [];
82
+ let lastEnd = 0;
83
+ JOIN_SPLIT_RE_GLOBAL.lastIndex = 0;
84
+ let match;
85
+ while ((match = JOIN_SPLIT_RE_GLOBAL.exec(fromClause)) !== null) {
86
+ parts.push(fromClause.slice(lastEnd, match.index));
87
+ parts.push(match[0]);
88
+ lastEnd = match.index + match[0].length;
89
+ }
90
+ parts.push(fromClause.slice(lastEnd));
91
+
92
+ const mainRef = parseTableRef(parts[0] || '');
93
+ const mainTable = mainRef.tableName;
94
+ const mainAlias = mainRef.alias;
95
+
96
+ const joins = [];
97
+ let j = 1;
98
+ while (j + 1 < parts.length) {
99
+ const joinBody = parts[j + 1].trim();
100
+ const onMatch = ON_CLAUSE_RE.exec(joinBody);
101
+ if (!onMatch) {
102
+ j += 2;
103
+ continue;
104
+ }
105
+ const tableRef = onMatch[1];
106
+ const condition = onMatch[2];
107
+ const ref = parseTableRef(tableRef);
108
+ const cond = parseJoinCondition(condition, mainAlias);
109
+ joins.push({
110
+ tableName: ref.tableName,
111
+ tableAlias: ref.alias,
112
+ localColumn: cond.localColumn,
113
+ remoteColumn: cond.remoteColumn
114
+ });
115
+ j += 2;
116
+ }
117
+
118
+ return { selectColumns, joins, mainTable, mainAlias };
119
+ }
120
+
121
+ module.exports = {
122
+ parseDatatablesQuery,
123
+ stripSchema
124
+ };
@@ -155,10 +155,52 @@ class PayloadGenerator {
155
155
  }
156
156
 
157
157
  /**
158
- * Generate fieldValidation array from detailed column info
159
- * Only includes "special" fields: primary key (UUID/auto-generate), numeric, boolean, date/time
160
- * Regular string fields are excluded (treated as default)
158
+ * Enrich detailedColumns dengan flag `is_unique` berdasarkan output
159
+ * db.getConstraints(). Hanya constraint UNIQUE single-column yang ditandai;
160
+ * UNIQUE composite (multi-column) di-skip karena tidak dapat diatribusikan
161
+ * ke satu kolom secara semantik (uniqueness berlaku pada kombinasi kolom,
162
+ * bukan kolom individual).
163
+ *
164
+ * PRIMARY KEY tidak di-tag is_unique karena sudah di-handle terpisah via
165
+ * `isVarcharLikePk` / `isUuidNativeType` di generateFieldValidation.
166
+ *
167
+ * Pemanggil bertanggung jawab fetch constraints via `db.getConstraints(table)`
168
+ * sebelum memanggil method ini.
169
+ *
161
170
  * @param {Array} detailedColumns - Array from getDetailedColumnInfo()
171
+ * @param {Array} constraints - Array from getConstraints() with shape {name, type, columns}
172
+ * @returns {Array} New array dengan flag is_unique pada kolom yang relevan
173
+ */
174
+ enrichDetailedColumnsWithConstraints(detailedColumns, constraints) {
175
+ if (!Array.isArray(detailedColumns)) return [];
176
+ if (!Array.isArray(constraints) || constraints.length === 0) return detailedColumns;
177
+
178
+ const uniqueColumns = new Set();
179
+ for (const c of constraints) {
180
+ if (c.type === 'UNIQUE' && Array.isArray(c.columns) && c.columns.length === 1) {
181
+ uniqueColumns.add(c.columns[0]);
182
+ }
183
+ }
184
+
185
+ if (uniqueColumns.size === 0) return detailedColumns;
186
+
187
+ return detailedColumns.map(col => (
188
+ uniqueColumns.has(col.column_name) ? { ...col, is_unique: true } : col
189
+ ));
190
+ }
191
+
192
+ /**
193
+ * Generate fieldValidation array from detailed column info
194
+ * Includes: primary key (UUID/auto-generate), numeric, boolean, date/time, and
195
+ * string fields yang membawa constraint actionable dari DB introspection
196
+ * (NOT NULL -> required, character_maximum_length -> maxLength,
197
+ * UNIQUE single-column -> unique). Nullable string tanpa constraint tetap
198
+ * di-skip agar payload tidak bloat.
199
+ *
200
+ * Untuk derive `unique`, kolom harus punya flag `is_unique: true` yang
201
+ * di-inject oleh `enrichDetailedColumnsWithConstraints()` sebelum dipanggil.
202
+ *
203
+ * @param {Array} detailedColumns - Array from getDetailedColumnInfo() (opsional di-enrich dengan is_unique)
162
204
  * @param {Array} fieldNames - Array of selected field names
163
205
  * @param {string} primaryKey - Primary key field name
164
206
  * @returns {Array} fieldValidation array
@@ -211,14 +253,22 @@ class PayloadGenerator {
211
253
  }
212
254
 
213
255
  if (isVarcharLikePk) {
256
+ const constraints = {
257
+ primaryKey: true,
258
+ autoGenerate: true,
259
+ required: true
260
+ };
261
+ // Defense in depth: derive maxLength dari character_maximum_length agar
262
+ // user yang kirim PK manual (override autoGenerate) tetap divalidasi
263
+ // di application layer sebelum hit DB. UUID v7 generated value (36 char)
264
+ // selalu fit di varchar(>=36); maxLength tidak mengganggu autoGenerate.
265
+ if (col.character_maximum_length) {
266
+ constraints.maxLength = col.character_maximum_length;
267
+ }
214
268
  fieldValidation.push({
215
269
  name: fieldName,
216
270
  type: 'string',
217
- constraints: {
218
- primaryKey: true,
219
- autoGenerate: true,
220
- required: true
221
- }
271
+ constraints
222
272
  });
223
273
  continue;
224
274
  }
@@ -370,7 +420,46 @@ class PayloadGenerator {
370
420
  continue;
371
421
  }
372
422
 
373
- // --- String fields: SKIP (default behavior, tidak perlu di fieldValidation) ---
423
+ // --- String fields ---
424
+ // VARCHAR/TEXT/CHAR non-PK: derive validation actionable dari DB introspection.
425
+ // - required: dari is_nullable = 'NO' (NOT NULL di database)
426
+ // - maxLength: dari character_maximum_length
427
+ // - unique: dari flag is_unique yang di-inject via enrichDetailedColumnsWithConstraints()
428
+ // PK varchar sudah di-handle di branch isVarcharLikePk di atas (continue).
429
+ // Defensive guard `isPrimaryKey` mencegah duplicate entry bila PK varchar
430
+ // lolos branch isVarcharLikePk (mis. PK dengan nextval default).
431
+ // Nullable string tanpa constraint sama sekali tetap di-skip agar payload
432
+ // tidak bloat dengan entry kosong yang tidak menambah validasi runtime.
433
+ if (['character varying', 'varchar', 'text', 'character', 'char'].includes(dataType)
434
+ || ['varchar', 'text', 'bpchar'].includes(udtName)) {
435
+
436
+ if (isPrimaryKey) continue;
437
+
438
+ const entry = {
439
+ name: fieldName,
440
+ type: 'string',
441
+ constraints: {}
442
+ };
443
+
444
+ if (col.is_nullable === 'NO' || col.is_nullable === false) {
445
+ entry.constraints.required = true;
446
+ }
447
+
448
+ if (col.character_maximum_length) {
449
+ entry.constraints.maxLength = col.character_maximum_length;
450
+ }
451
+
452
+ if (col.is_unique === true) {
453
+ entry.constraints.unique = true;
454
+ }
455
+
456
+ if (Object.keys(entry.constraints).length === 0) {
457
+ continue;
458
+ }
459
+
460
+ fieldValidation.push(entry);
461
+ continue;
462
+ }
374
463
  }
375
464
 
376
465
  return fieldValidation;
@@ -605,7 +694,9 @@ class PayloadGenerator {
605
694
 
606
695
  // Generate fieldValidation for special fields
607
696
  const detailedColumns = await this.db.getDetailedColumnInfo(args.table);
608
- const fieldValidation = this.generateFieldValidation(detailedColumns, payloadData.fieldName, payloadData.primaryKey);
697
+ const constraints = await this.db.getConstraints(args.table);
698
+ const enrichedColumns = this.enrichDetailedColumnsWithConstraints(detailedColumns, constraints);
699
+ const fieldValidation = this.generateFieldValidation(enrichedColumns, payloadData.fieldName, payloadData.primaryKey);
609
700
  if (fieldValidation.length > 0) {
610
701
  payloadData.fieldValidation = fieldValidation;
611
702
  }
@@ -1056,6 +1147,9 @@ class SchemaValidator {
1056
1147
  const dbColumns = await this.db.getColumns(tableName);
1057
1148
  const columnTypes = await this.db.getColumnTypes(tableName);
1058
1149
  const detailedColumns = await this.db.getDetailedColumnInfo(tableName);
1150
+ const constraints = typeof this.db.getConstraints === 'function'
1151
+ ? await this.db.getConstraints(tableName)
1152
+ : [];
1059
1153
  const primaryKey = await this.db.getPrimaryKey(tableName) || oldPayload.primaryKey;
1060
1154
 
1061
1155
  // Bangun fieldName baru: pertahankan urutan field lama, tambahkan field baru di akhir.
@@ -1123,7 +1217,8 @@ class SchemaValidator {
1123
1217
  }
1124
1218
 
1125
1219
  // Re-generate fieldValidation
1126
- const fieldValidation = tempGenerator.generateFieldValidation(detailedColumns, newFieldName, primaryKey);
1220
+ const enrichedColumns = tempGenerator.enrichDetailedColumnsWithConstraints(detailedColumns, constraints);
1221
+ const fieldValidation = tempGenerator.generateFieldValidation(enrichedColumns, newFieldName, primaryKey);
1127
1222
  if (fieldValidation.length > 0) {
1128
1223
  updatedPayload.fieldValidation = fieldValidation;
1129
1224
  } else {
@@ -1 +1 @@
1
- const a0_0x15a923=a0_0x591c;function a0_0xe2e1(){const _0x390e68=['ue9tvcaVyxbPl21PBMKTAw52zw50B3j5l2rHC2GTAw5IB3vUzc9KyxnOyM9HCMq','tMvNyxrPDMuGBg9VA2jLAgLUzcbWCMv2zw50CYbTyxrJAgLUzYaNoJONicHqB3n0z3jLCYbJyxn0ihn5BNrHEcKGyxmGysbWBgfJzwHVBgrLCI4','C2nHBgfYihbYAw1PDgL2zq','zxHWB3j0CW','qwX3yxLZihDYyxaGyxmGEYbPDgvTCZOGwY4UlL0GFsbYzwDHCMrSzxnZig9MifnrtcbYzxn1BhqGC2HHCguU','iNzHBhvLiJOGiJy5nZaWiG','v2HLBIb0CNvLlcb0AguGCMvXDwvZDcbIB2r5ie1vu1qGAw5JBhvKzsb0AgLZihbHCMfTicHVDgHLCNDPC2uGndaWks4','ugfYyw0Gzgf0ysb0ExbLlIbwywXPzgf0zxmGCMvXDwvZDcbIB2r5igfUzcbZAgfWzxmGCNvUDgLTzsbWyxjHBwv0zxiGyMLUzgLUzY4','ndi1mZu4mgnVufH6Eq','yM9VBgvHBG','yxzNx2rHAwX5x3nHBgvZ','qsb3AwrNzxqGtvvtvcbKzwnSyxjLigv4ywn0BhKGB25Lig9MoIaNCxvLCNKNie9sicDXDwvYAwvZjY4GqM90AcbVCIbUzwL0AgvYigLZihjLAMvJDgvKlG','Aw5OzxjPDhmGq0fdsevFvfrmigvUDG','EYaIzgLYzwn0Aw9UiJOGiMrVD24IlcaICgn0iJOGiJiUmIiGFq','CgfYyw1Z','vgLTzs10BY1SAxzLigLUihnLy29UzhmUidaGzwzMzwn0AxzLBhKGzgLZywjSzxmGy2fJAguGzM9YihrOAxmGzw50CNKU','tIbYB3DZimoxie0Gy29SCW','BwfW','qsbWyxLSB2fKihDPDgGGyM90AcaND2LKz2v0CYCGyw5KicD0ywjSzu5HBwuNigLZihjLAMvJDgvKigj5ierHC2HIB2fYzfzHBgLKyxrVCI4GugLJAYbVBMuGC2HHCguU','ugfYyw0GBMfTzsbTDxn0ig1HDgnOihrOzsbWBgfJzwHVBgrLCIbYzwDLEcbGw2eTEKeTwL9Dw2eTEKeTwJaTov9DkMaGkgfSCgHHBNvTzxjPyYaRihvUzgvYC2nVCMuSig11C3qGC3rHCNqGD2L0AcbSzxr0zxiGB3iGDw5KzxjZy29YzsKU','q29SBgfWC2uGDg8GC2nHBgfYihbYAw1PDgL2zsaODgHLihzHBhvLig9MihrOzsbZAw5NBguGy29SDw1Uks4','tIbYB3DZimoxidiGy29SDw1UCW','y29SB3i','yxjYyxK','zMLSztPXDwvYEs88Cgf0Ad4VyNjLywTKB3DUlNnXBa','mtKYmJu4vgTUtKXz','vgfIBguGyxbWzwfYCYbPBIbtuuWGqu5eigLUig1LDgfKyxrHihbYB2PLy3qSigj1DcbTAxnZAw5NigzYB20GAw52ywXPzgf0zxmGkgnHy2HLihn0ywXLihjPC2SP','zMLSztPXDwvYEs88Cgf0Ad4VDhjLBMqUC3fS','B3jKzxjZx3rOAxnFBw9UDgG','A2v54OAszMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','iJi0mJaI','Cgf5Bg9Hzcbku09oigzPBguGBg9JyxrPB24','uMvZzxj2zwqGzM9YiensvuqGCgf5Bg9HzhmUieeGzgfZAgjVyxjKihbHEwXVywqGBxvZDcbKzwnSyxjLicD3AwrNzxrZjYbPBNn0zwfKlG','DgfIBgvoyw1L','rNjVBNrLBMqGzgv0zxjTAw5LCYbKB251Dc9WAwuGDMfYAwfUDcWGy29SB3iGCgvYignHDgvNB3j5lcbHBMqGBgfIzwWGB3jKzxiUieLMihbLCI1JyxrLz29YEsbWzxjJzw50ywDLigLZig5LzwrLzcbMB3iGDgHLigrVBNv0igfYyYWGzNjVBNrLBMqGy29TChv0zxmGAxqGzNjVBsbPDgvTC1TPxs52ywX1zsaVihn1BsHPDgvTC1SQxs52ywX1zsKUie5Vig5LzwqGDg8GC2vUzcaNCgn0jYbMCM9TigjHy2TLBMqGDw5SzxnZihrOzsbMAwD1CMuGAxmGysbZDgfIBguGyNvZAw5LC3mGy2fSy3vSyxrPB24GAw5KzxbLBMrLBNqGB2yGDMLZDwfSihjLBMrLCMLUzY4','BM9UlwvTChr5lcb1BMLXDwuGywnYB3nZihDPzgDLDhmGAw4GDgHLihnHBwuGCgf5Bg9Hza','EYbZDwnJzxnZoIbIB29SzwfUlcbKyxrHoIb7idX3AwrNzxrjzd46idXWzxjxAwrNzxrszxnWB25Zzt4Sic4UlIb9ih0','zgfZAc1HDxrOB3iTC3rHDhm','u2LUz2XLifnrtcbXDwvYEsbMB3iGDgHLihDPzgDLDc4','Bwv0CMLJx3bYB2DYzxnZx3rVx2DVywW','DhrS','BgvUz3rO','zMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','D2LKz2v0lNf1zxj5icHZAw5NDwXHCIK','twv0CMLJicSGu3bHCMTSAw5L','CgvYAw9K','vxbKyxrPBMCGyw4Gu1fmigzPBguGCMvXDwLYzxmGCMvNzw5LCMf0Aw5NihrOzsbKyxnOyM9HCMqGBw9KDwXLicGNy29KzwDLBL9JCMvHDgvFzgfZAgjVyxjKjYKGzM9YignOyw5NzxmGDg8GDgfRzsbLzMzLy3qU','yxjYyxKGB2yGB2jQzwn0CW','yw55icHTDxn0igjLignVBxbHDgLIBguGD2L0AcbKzwnSyxjLzcaNDhLWzsCP','uMv0DxjUigfZigfYCMf5ig9Mig9IAMvJDhmGkg5VignVBgXHChnLks4','z2vUzxjHDgLVBIb0Aw1LicHot1qGCNvUDgLTzsK','w3SGiMXHyMvSiJOGiLnOB2vZiIWGiNzHBhvLiJOGiJC2nJaIih0SihSGiMXHyMvSiJOGiKDHBwLUzYiSicj2ywX1zsi6iciYodiWiIb9lcb7icjSywjLBci6icjpDgHLCNmIlcaIDMfSDwuIoIaInduYntCIih1D','zgvMyxvSDa','tgf5B3v0igLZigeGzNjVBNrLBMqGCMvUzgvYAw5NignVBMnLCM4U','msbYB3CGW5CGmIbJB2X1Bw5Z','B2jQzwn0','nti2ndiWuNbOs1jp','rgfZAgjVyxjKigvUzhbVAw50ig1HEsbVChqTAw4GDg8GuMvKAxmTyMfZzwqGy2fJAguUifbHDhrLCM4GzM9SBg93CYbWCM9JzxnZB3iGy2fJAguGkhnLzsbMzwf0lwnHy2HLlM1Kks4Gq2fJAguGC2nVCguGAxmGDgHLigz1BgWGCMvZCg9UC2uGzw52zwXVCgu7ig9UzsbJywnOzsbLBNrYEsbWzxiGkhbHCMfTCYaRihDPzgDLDhnBxsbZDwjZzxqPignVBwjPBMf0Aw9UlG','txvSDgKTu1fmihDPzgDLDc4GrwfJAcbRzxKGyMvJB21LCYbHigTLEsbPBIb0AguGCMvZCg9UC2uGB2jQzwn0lG','zgfZAgjVyxjKihbHEwXVywq','Bwv0CMLJx2rVBNv0x2jYzwfRzg93BG','nZC0nZq0mfDpDgTZqG','msbYB3CGW5CGmsbJB2X1Bw4','iNnOB3bWAw5Nx2nHDgvNB3jPzxmIoIb7icjPDgvTCYi6ifT7icjUyw1LiJOGiKXHBMrZiIb9lcb7icjUyw1LiJOGiKHVDxnLCYiGFv0GFq','yxjYyxK8C3rYAw5NpIWGB3b0Aw9UywWG4Ocuihn1yNnLDcbVzIb3AwrNzxqGsurZihrVigv4zwn1DguUie9TAxqGDg8GzxHLy3v0zsbHBgWGzgvJBgfYzwqGD2LKz2v0CY4','Ahr0Chm6lY9Yzxn0zM9Yz2uUzgv2l2rVy3mVC2vYDMvYl3f1zxj5lwrHDgeVzgfZAgjVyxjK','BgfIzwW','zMLSztPXDwvYEs88Cgf0Ad4VDgfYz2v0lNnXBa','mteYmenpuxLLsa','sgvHzgXPBMuGBwv0CMLJihDPDgGGDhjLBMqGy2HPCcbHBMqGChjVz3jLC3mGyMfYigfNywLUC3qGysbWzxjPB2qGDgfYz2v0lIbtDwL0ywjSzsbMB3iGD2LKz2v0CYbSAwTLicDpCMrLCNmGvgHPCYbnB250AcCU','D2LKz2v0vhLWzq','rNjVBNrLBMqGy29TChv0zxmGDg9Fz29HBca9ihrHCMDLDcaTihzHBhvLigfUzcbWy3qGpsbYB3vUzcH2ywX1zsaVihrHCMDLDcaQideWmcKGzM9YihrOzsbWCM9NCMvZCYbIyxiUifzPC3vHBcb3Awr0AcbPCYbWCMvZzw50yxrPB25HBcbHBMqGBxvZDcbot1qGBgL2zsbPBIb0AguGyMfJA2vUzcbWyxLSB2fKlIbjzIbWCM9NCMvZCYbPBNzVBhzLCYbJB21WBgv4igj1C2LUzxnZihj1BgvZicHLlMCUigv4y2X1zguGD2vLA2vUzhmSihbYB3jHDgvKihDVCMTKyxLZksWGDxnLigeGC2LUz2XLig11BhrPlwnVBhvTBIbXDwvYEsbZBYaNCgn0jYbPCYbHihn0ywjSzsbIDxnPBMvZCYbMywn0ihjHDgHLCIb0AgfUihzPC3vHBcb3Awr0Ac4','tvvtvcbZDgfYDcb3AxrOicDKyxnOlsCGChjLzML4','Cgn0','nJG4mJq2yxv1ExPO','twv0CMLJicSGrg9UDxqGqNjLywTKB3DU','DMfSDwuGkg9Yign1CNjLBNqP','ntGYodb4yu1jy0i','phDPzgDLDf9Pzd4','zNjLztSGlNnXBcbYzwnVBw1LBMrLzcbMB3iGzwrPDg9YigHPz2HSAwDODa','m0PbqwnUtq','v2LKz2v0igLKzw50AwzPzxi7ihvZzwqGyxmGDgHLihjLC3bVBNnLigTLEsbPBIb0AguGzgfZAgjVyxjKigvUDMvSB3bLlG','DMfSDwu','ms4W','zMLSztPXDwvYEs88Cgf0Ad4VDMfSDwuUC3fS','mtm0mJy0mNPRAerkEq','vgHLihbYzwzPEcbIzwnVBwvZihbHCNqGB2yGDgHLifvstcbZzwDTzw50lIbuAguGCMvZzxj2zwqGC2nOzw1LigTLzxbZigrHC2HIB2fYzcbLBMrWB2LUDhmGDMLZDwfSBhKGzgLZDgLUy3qGzNjVBsbduLveigvUzhbVAw50CYbPBIb0AguGvvjmihnWywnLigfUzcbHBgXVD3mGzNv0DxjLihjVDxrPBMCGzgLMzMvYzw50Awf0Aw9UlG','CxvLCMLLCW','q29SBgfWC2uGDg8GB2jQzwn0ihDOB3nLigTLExmGyxjLifnrtcbJB2X1Bw4GBMfTzxmGkgXVD2vYy2fZzwqPlG','Bgf5B3v0','vgfIBguGzgv0zwn0zwqGAw4Gu1fmlcbIDxqGBM90ihjLz2LZDgvYzwqGyxmGq1jvrcbLBMrWB2LUDcbPBIbTzxrHzgf0ysbWCM9Qzwn0icHSAwTLBhKGysb2Awv3lcbdveuGywXPyxmSig9YignYB3nZlxbYB2PLy3qGDgfIBguG4OcuignHC2nHzguGD2LSBcbUB3qGzMLYzsK','zNjVBNrLBMqTy29Uy2vYBG','zgLYzwn0Aw9U'];a0_0xe2e1=function(){return _0x390e68;};return a0_0xe2e1();}function a0_0x591c(_0x522681,_0xd00375){_0x522681=_0x522681-0x1dd;const _0xe2e103=a0_0xe2e1();let _0x591cc5=_0xe2e103[_0x522681];if(a0_0x591c['xXiwze']===undefined){var _0x1359da=function(_0x2e4df6){const _0xd5c81='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x3da22b='',_0x575846='';for(let _0x4170a5=0x0,_0x4100e3,_0x2c4b48,_0x57a74d=0x0;_0x2c4b48=_0x2e4df6['charAt'](_0x57a74d++);~_0x2c4b48&&(_0x4100e3=_0x4170a5%0x4?_0x4100e3*0x40+_0x2c4b48:_0x2c4b48,_0x4170a5++%0x4)?_0x3da22b+=String['fromCharCode'](0xff&_0x4100e3>>(-0x2*_0x4170a5&0x6)):0x0){_0x2c4b48=_0xd5c81['indexOf'](_0x2c4b48);}for(let _0x1d6749=0x0,_0x119ff5=_0x3da22b['length'];_0x1d6749<_0x119ff5;_0x1d6749++){_0x575846+='%'+('00'+_0x3da22b['charCodeAt'](_0x1d6749)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x575846);};a0_0x591c['krLJYG']=_0x1359da,a0_0x591c['nAzaeG']={},a0_0x591c['xXiwze']=!![];}const _0x53f8e0=_0xe2e103[0x0],_0x1fbe7e=_0x522681+_0x53f8e0,_0x32ceea=a0_0x591c['nAzaeG'][_0x1fbe7e];return!_0x32ceea?(_0x591cc5=a0_0x591c['krLJYG'](_0x591cc5),a0_0x591c['nAzaeG'][_0x1fbe7e]=_0x591cc5):_0x591cc5=_0x32ceea,_0x591cc5;}(function(_0x293a3a,_0x273f8b){const _0x336c97=a0_0x591c,_0x2f826f=_0x293a3a();while(!![]){try{const _0x31a85e=parseInt(_0x336c97(0x1fb))/0x1+parseInt(_0x336c97(0x206))/0x2+parseInt(_0x336c97(0x201))/0x3*(-parseInt(_0x336c97(0x1e9))/0x4)+parseInt(_0x336c97(0x1ee))/0x5+-parseInt(_0x336c97(0x216))/0x6+-parseInt(_0x336c97(0x1f5))/0x7*(parseInt(_0x336c97(0x1fe))/0x8)+parseInt(_0x336c97(0x227))/0x9;if(_0x31a85e===_0x273f8b)break;else _0x2f826f['push'](_0x2f826f['shift']());}catch(_0x706318){_0x2f826f['push'](_0x2f826f['shift']());}}}(a0_0xe2e1,0xe1a7a));const FORBIDDEN_FRONTEND_FIELDS=[a0_0x15a923(0x1f7),a0_0x15a923(0x20a),'title','subtitle',a0_0x15a923(0x224)],ALLOWED_PARAM_TYPES=['string','number',a0_0x15a923(0x217),'date'],FRONTEND_CONCERN_REASONS={'widgetType':'Visual\x20variant\x20(donut,\x20bar,\x20pie,\x20area)\x20is\x20a\x20frontend\x20rendering\x20concern\x20(separation\x20of\x20concerns).','layout':a0_0x15a923(0x1e6),'title':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','subtitle':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','color':'Visual\x20color\x20is\x20a\x20frontend\x20rendering\x20concern.'},PAYLOAD_SHAPE={'discriminator':{'field':'widgets','presentMeans':a0_0x15a923(0x1ec),'absentMeans':'Not\x20a\x20dashboard\x20payload\x20(likely\x20CRUD\x20with\x20tableName,\x20or\x20invalid)','conflictsWith':'tableName','conflictRationale':a0_0x15a923(0x220)},'topLevelAllowed':[{'name':'widgets','type':a0_0x15a923(0x225),'required':!![],'minItems':0x1,'description':'List\x20of\x20widget\x20definitions.\x20Order\x20is\x20informational\x20only\x20(response\x20keys\x20are\x20by\x20widget\x20id,\x20not\x20array\x20index).'},{'name':a0_0x15a923(0x21c),'type':a0_0x15a923(0x1e8),'required':![],'description':'Parameter\x20contract\x20for\x20the\x20dashboard.\x20Each\x20key\x20is\x20a\x20param\x20name;\x20values\x20describe\x20type/required/default.\x20Placeholders\x20inside\x20widget\x20SQL\x20must\x20reference\x20declared\x20param\x20names.'},{'name':'cache','type':a0_0x15a923(0x1e8),'required':![],'description':'Optional\x20cache\x20configuration.\x20See\x20cacheSpec\x20for\x20details.'}],'topLevelForbidden':[{'name':a0_0x15a923(0x22f),'category':'shape-conflict','reason':a0_0x15a923(0x22e)},...FORBIDDEN_FRONTEND_FIELDS[a0_0x15a923(0x21f)](_0x3da22b=>({'name':_0x3da22b,'category':a0_0x15a923(0x20c),'reason':FRONTEND_CONCERN_REASONS[_0x3da22b]}))]},WIDGET_SPEC={'requiredFields':[{'name':'id','type':'string','constraint':a0_0x15a923(0x231),'description':a0_0x15a923(0x202)}],'exclusiveQueryFields':{'rule':a0_0x15a923(0x219),'options':[{'name':'query','type':'string','format':a0_0x15a923(0x238),'description':a0_0x15a923(0x234),'responseShape':'Always\x20{\x20items:\x20[...]\x20}\x20regardless\x20of\x20SQL\x20result\x20shape.'},{'name':a0_0x15a923(0x208),'type':'object','format':a0_0x15a923(0x22b),'minKeys':0x1,'description':a0_0x15a923(0x1eb),'responseShape':'Per-key\x20based\x20on\x20scalarCollapseRules\x20below.'}]},'forbiddenFields':FORBIDDEN_FRONTEND_FIELDS},PARAM_SPEC={'container':'top-level\x20\x27params\x27\x20object','keyConvention':a0_0x15a923(0x221),'perEntryFields':[{'name':'type','required':!![],'allowedValues':ALLOWED_PARAM_TYPES,'description':a0_0x15a923(0x215)},{'name':'required','required':![],'type':a0_0x15a923(0x217),'default':![],'description':a0_0x15a923(0x214)},{'name':a0_0x15a923(0x1e5),'required':![],'type':a0_0x15a923(0x1e1),'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':a0_0x15a923(0x239),'rule':a0_0x15a923(0x212),'exampleSqlShape':'any\x20(1\x20row\x20×\x201\x20col,\x20N\x20rows\x20×\x20M\x20cols,\x20etc.)','exampleResponse':a0_0x15a923(0x1f0)},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x201\x20row\x20×\x201\x20column','rule':a0_0x15a923(0x222),'exampleSqlShape':'1\x20row\x20×\x201\x20col,\x20output\x20column\x20\x27value\x27','exampleResponse':a0_0x15a923(0x213)},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x201\x20row\x20×\x20multiple\x20columns','rule':a0_0x15a923(0x209),'exampleSqlShape':'1\x20row\x20×\x202\x20cols,\x20output\x20columns\x20\x27direction\x27,\x20\x27pct\x27','exampleResponse':'\x22trend\x22:\x20{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}'},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x20N\x20rows','rule':a0_0x15a923(0x1e2),'exampleSqlShape':a0_0x15a923(0x21e),'exampleResponse':'\x22items\x22:\x20[{\x20\x22label\x22:\x20\x22Shoes\x22,\x20\x22value\x22:\x20\x227660\x22\x20},\x20...]'}],COMMON_WIDGET_PATTERNS=[{'id':a0_0x15a923(0x1ed),'name':a0_0x15a923(0x1fc),'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':a0_0x15a923(0x1ff),'queries':{'value':a0_0x15a923(0x205),'trend':'file:query/<path>/trend.sql','items':a0_0x15a923(0x226)}},'sqlShapesPerKey':[{'key':a0_0x15a923(0x203),'shape':a0_0x15a923(0x1ef),'outputColumns':['value'],'collapseRule':a0_0x15a923(0x210)},{'key':'trend','shape':'1\x20row\x20×\x202\x20columns','outputColumns':[a0_0x15a923(0x20d),'pct'],'collapseRule':a0_0x15a923(0x1e8)},{'key':'items','shape':a0_0x15a923(0x223),'outputColumns':[a0_0x15a923(0x1f3),a0_0x15a923(0x203)],'collapseRule':'array\x20of\x20objects'}],'responseShape':{'value':'\x2269700\x22','trend':'{\x20\x22direction\x22:\x20\x22up\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}','items':a0_0x15a923(0x1e4)},'referenceWidgetId':'expected_earnings','socNotes':a0_0x15a923(0x230)},{'id':'metric_sparkline','name':a0_0x15a923(0x1dd),'useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20sparkline\x20mini-chart\x20for\x20short\x20windows\x20(7\x20days,\x2012\x20months,\x20etc.).\x20Suitable\x20for\x20widgets\x20like\x20\x27Average\x20Daily\x20Sales\x27.','payloadShape':{'id':'<widget_id>','queries':{'value':'file:query/<path>/value.sql','trend':a0_0x15a923(0x229),'points':'file:query/<path>/points.sql'}},'sqlShapesPerKey':[{'key':a0_0x15a923(0x203),'shape':a0_0x15a923(0x1ef),'outputColumns':[a0_0x15a923(0x203)],'collapseRule':a0_0x15a923(0x210)},{'key':'trend','shape':a0_0x15a923(0x1e7),'outputColumns':[a0_0x15a923(0x20d),'pct'],'collapseRule':'object'},{'key':'points','shape':a0_0x15a923(0x223),'outputColumns':[a0_0x15a923(0x1de),'value'],'collapseRule':a0_0x15a923(0x1e0)}],'responseShape':{'value':a0_0x15a923(0x22c),'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_0x15a923(0x218),'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_0x15a923(0x235),'name':'Metric\x20+\x20Progress\x20to\x20Goal','useCase':a0_0x15a923(0x1f6),'payloadShape':{'id':a0_0x15a923(0x1ff),'queries':{'value':'file:query/<path>/current.sql','trend':a0_0x15a923(0x229),'target':a0_0x15a923(0x1f4)}},'sqlShapesPerKey':[{'key':a0_0x15a923(0x203),'shape':a0_0x15a923(0x1ef),'outputColumns':[a0_0x15a923(0x1fd)],'collapseRule':'scalar\x20primitive'},{'key':'trend','shape':a0_0x15a923(0x1e7),'outputColumns':[a0_0x15a923(0x20d),a0_0x15a923(0x1fa)],'collapseRule':a0_0x15a923(0x1e8)},{'key':'target','shape':'1\x20row\x20×\x201\x20column','outputColumns':['target'],'collapseRule':a0_0x15a923(0x210)}],'responseShape':{'value':'\x221836\x22','trend':a0_0x15a923(0x21b),'target':'\x222884\x22'},'referenceWidgetId':a0_0x15a923(0x22a),'socNotes':a0_0x15a923(0x1f8)}],NAMING_CONVENTION={'dashboardName':{'constraint':a0_0x15a923(0x1f9),'minLength':0x6,'maxLength':0x32,'regex':'^dash-[a-zA-Z0-9_-]+$','examples':['dash-sales','dash-inbound',a0_0x15a923(0x233)],'rationale':a0_0x15a923(0x207)}},URL_PATTERN={'method':'POST','path':'/api/{project}/{name}/dashboard','exampleFull':a0_0x15a923(0x20e),'requestBodyShape':{'params':'object\x20—\x20values\x20for\x20declared\x20params\x20(validated\x20against\x20params\x20contract;\x20missing\x20required\x20→\x20400,\x20type\x20mismatch\x20→\x20400)','widgets':a0_0x15a923(0x1f1)},'responseShape':{'envelope':a0_0x15a923(0x232),'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_0x15a923(0x22d),'fileExtensionPolicy':a0_0x15a923(0x200),'resolvedAt':a0_0x15a923(0x1e3),'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':a0_0x15a923(0x1df)},PLACEHOLDER_CONVENTION={'format':':paramName','regex':'(?<!:):([a-zA-Z_][a-zA-Z0-9_]*)','regexNotes':a0_0x15a923(0x20f),'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':'SELECT\x20*\x20FROM\x20stock_inbound\x20WHERE\x20EXTRACT(YEAR\x20FROM\x20inbound_date)\x20=\x20:year','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_0x15a923(0x1ea),'fields':[{'name':'enabled','type':a0_0x15a923(0x217),'required':!![],'description':'Toggle\x20cache\x20feature\x20for\x20this\x20dashboard.'},{'name':a0_0x15a923(0x236),'type':'number','required':![],'constraint':'>=\x200\x20(seconds)','default':a0_0x15a923(0x21a),'description':a0_0x15a923(0x21d)},{'name':'invalidates','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':[a0_0x15a923(0x228),'Table\x20declared\x20in\x20invalidates,\x20but\x20not\x20detected\x20in\x20any\x20widget\x20SQL\x20(typo\x20or\x20dead\x20entry)'],'warningOn':[a0_0x15a923(0x20b)]}},DOCUMENTATION_URL=a0_0x15a923(0x1f2),DASHBOARD_CATALOG={'schemaVersion':a0_0x15a923(0x204),'source':'dashboard-catalog','summary':{'totalAllowedTopLevelFields':PAYLOAD_SHAPE['topLevelAllowed']['length'],'totalForbiddenFrontendFields':FORBIDDEN_FRONTEND_FIELDS[a0_0x15a923(0x237)],'totalParamTypes':ALLOWED_PARAM_TYPES[a0_0x15a923(0x237)],'totalScalarCollapseRules':SCALAR_COLLAPSE_RULES['length'],'totalCommonWidgetPatterns':COMMON_WIDGET_PATTERNS[a0_0x15a923(0x237)]},'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_0x15a923(0x211)]={'DASHBOARD_CATALOG':DASHBOARD_CATALOG};
1
+ const a0_0x2f22d8=a0_0x59f4;(function(_0x4d57a8,_0x2c85dd){const _0x327969=a0_0x59f4,_0x12ed23=_0x4d57a8();while(!![]){try{const _0x398936=-parseInt(_0x327969(0x1be))/0x1+-parseInt(_0x327969(0x1cf))/0x2+parseInt(_0x327969(0x1b6))/0x3*(-parseInt(_0x327969(0x19f))/0x4)+-parseInt(_0x327969(0x18f))/0x5*(-parseInt(_0x327969(0x1b7))/0x6)+parseInt(_0x327969(0x1c8))/0x7*(-parseInt(_0x327969(0x1d8))/0x8)+-parseInt(_0x327969(0x1b3))/0x9*(parseInt(_0x327969(0x1cb))/0xa)+parseInt(_0x327969(0x18c))/0xb*(parseInt(_0x327969(0x18d))/0xc);if(_0x398936===_0x2c85dd)break;else _0x12ed23['push'](_0x12ed23['shift']());}catch(_0xde8b61){_0x12ed23['push'](_0x12ed23['shift']());}}}(a0_0x50b8,0xa259f));function a0_0x50b8(){const _0x17f5a3=['phDPzgDLDf9Pzd4','zMLSztPXDwvYEs88Cgf0Ad4VDhjLBMqUC3fS','zMLSztPYzwXHDgL2zs9WyxrOl3rVl3f1zxj5lNnXBa','iJy5nZaWiG','zgfZAc1PBMjVDw5K','Cg9PBNrZ','iJi4odqI','iNrYzw5KiJOGEYaIzgLYzwn0Aw9UiJOGiNvWiIWGiNbJDci6iciYlJiIih0','DMfSDwuGkg9Yign1CNjLBNqP','q29SBgfWC2uGDg8GC2nHBgfYihbYAw1PDgL2zsaODgHLihzHBhvLig9MihrOzsbZAw5NBguGy29SDw1Uks4','u0vmrunuicOGrLjptsbZDg9JA19PBMjVDw5KifDirvjfievyvfjbq1qOwuvbuIbguK9nigLUyM91BMrFzgf0zsKGpsa6EwvHCG','DMfSDwu','AxrLBxm','zgfZAc1ZywXLCW','Bgf5B3v0','vMLZDwfSignVBg9YigLZigeGzNjVBNrLBMqGCMvUzgvYAw5NignVBMnLCM4U','sgvHzgXPBMuGBwv0CMLJihDPDgGGDhjLBMqGy2HPCcbHBMqGyNjLywTKB3DUigfJCM9ZCYbJyxrLz29YAwvZlIbtDwL0ywjSzsbMB3iGD2LKz2v0CYbSAwTLicDfEhbLy3rLzcbfyxjUAw5NCYCGDgHHDcbZAg93ihrVDgfSihzHBhvLlcbWzxjJzw50ywDLignOyw5NzsWGyw5KihbLCI1JyxrLz29YEsbJB250CMLIDxrPB24U','zMLSztPXDwvYEs88Cgf0Ad4VyNjLywTKB3DUlNnXBa','zNjLztSGlNnXBcbYzwnVBw1LBMrLzcbMB3iGzwrPDg9YigHPz2HSAwDODa','D2LKz2v0CW','twv0CMLJicSGuhjVz3jLC3mGDg8Gr29HBa','vgHLihbYzwzPEcbIzwnVBwvZihbHCNqGB2yGDgHLifvstcbZzwDTzw50lIbuAguGCMvZzxj2zwqGC2nOzw1LigTLzxbZigrHC2HIB2fYzcbLBMrWB2LUDhmGDMLZDwfSBhKGzgLZDgLUy3qGzNjVBsbduLveigvUzhbVAw50CYbPBIb0AguGvvjmihnWywnLigfUzcbHBgXVD3mGzNv0DxjLihjVDxrPBMCGzgLMzMvYzw50Awf0Aw9UlG','mtm2mdDtqxfVzwi','mJq3odb0wgvwsNm','v2LKz2v0igLKzw50AwzPzxi7ihvZzwqGyxmGDgHLihjLC3bVBNnLigTLEsbPBIb0AguGzgfZAgjVyxjKigvUDMvSB3bLlG','mtiWv05ZD21g','uMvZzxj2zwqGzM9YiensvuqGCgf5Bg9HzhmUieeGzgfZAgjVyxjKihbHEwXVywqGBxvZDcbKzwnSyxjLicD3AwrNzxrZjYbPBNn0zwfKlG','zNjVBNrLBMqTy29Uy2vYBG','yxzNx2rHAwX5x3nHBgvZ','DhjLBMq','EYaIzgLYzwn0Aw9UiJOGiNvWiIWGiNbJDci6iciYlJiIih0','DgfIBgvoyw1L','zMLSztPXDwvYEs88Cgf0Ad4Vy3vYCMvUDc5ZCwW','rNjVBNrLBMqGzgv0zxjTAw5LCYbKB251Dc9WAwuGDMfYAwfUDcWGy29SB3iGCgvYignHDgvNB3j5lcbHBMqGBgfIzwWGB3jKzxiUieLMihbLCI1JyxrLz29YEsbWzxjJzw50ywDLigLZig5LzwrLzcbMB3iGDgHLigrVBNv0igfYyYWGzNjVBNrLBMqGy29TChv0zxmGAxqGzNjVBsbPDgvTC1TPxs52ywX1zsaVihn1BsHPDgvTC1SQxs52ywX1zsKUie5Vig5LzwqGDg8GC2vUzcaNCgn0jYbMCM9TigjHy2TLBMqGDw5SzxnZihrOzsbMAwD1CMuGAxmGysbZDgfIBguGyNvZAw5LC3mGy2fSy3vSyxrPB24GAw5KzxbLBMrLBNqGB2yGDMLZDwfSihjLBMrLCMLUzY4','xMrHC2GTw2eTEKeTwJaTov8TxsSK','twv0CMLJicSGu3bHCMTSAw5L','tIbYB3DZimoxidiGy29SDw1UCW','vgfIBguGyxbWzwfYCYbPBIbtuuWGqu5eigLUig1LDgfKyxrHihbYB2PLy3qSigj1DcbTAxnZAw5NigzYB20GAw52ywXPzgf0zxmGkgnHy2HLihn0ywXLihjPC2SP','rgv0zxjTAw5LzcbIEsbZy2fSyxjdB2XSyxbZzvj1BgvZlIbgywLSzwqGD2LKz2v0CYbWCM9KDwnLihSGzxjYB3i6icCUlI4Nih0GyMXVy2SGD2L0Acb0B3aTBgv2zwWGC3vJy2vZCYbZDgLSBcb0CNvLicHVBMuGD2LKz2v0igzHAwX1CMuGzg9LCYbot1qGzMfPBcb0AguGzgfZAgjVyxjKks4','y2fJAgu','iJi0mJaI','mZi5oti2mfLcv09Jyq','Dg9WlwXLDMvSicDWyxjHBxmNig9IAMvJDa','msbYB3CGW5CGmIbJB2XZlcbVDxrWDxqGy29SDw1UCYaNzgLYzwn0Aw9UjYWGj3bJDcC','iNzHBhvLiJOGiJy5nZaWiG','ugfYyw0Gzgf0ysb0ExbLlIbwywXPzgf0zxmGCMvXDwvZDcbIB2r5igfUzcbZAgfWzxmGCNvUDgLTzsbWyxjHBwv0zxiGyMLUzgLUzY4','Bwv0CMLJx2rVBNv0x2jYzwfRzg93BG','yw55icGXihjVDYddLYaXignVBcWGtIbYB3DZimoxie0Gy29SCYWGzxrJlIK','zMLSztPXDwvYEs88Cgf0Ad4VDMfSDwuUC3fS','EYbZDwnJzxnZoIbIB29SzwfUlcbKyxrHoIb7idX3AwrNzxrjzd46idXWzxjxAwrNzxrszxnWB25Zzt4Sic4UlIb9ih0','iNnOB3bWAw5Nx2nHDgvNB3jPzxmIoIb7icjPDgvTCYi6ifT7icjUyw1LiJOGiKXHBMrZiIb9lcb7icjUyw1LiJOGiKHVDxnLCYiGFv0GFq','yxjYyxKGB2yGB2jQzwn0CW','BgvUz3rO','yxjYyxK8C3rYAw5NpIWGB3b0Aw9UywWG4Ocuihn1yNnLDcbVzIb3AwrNzxqGsurZihrVigv4zwn1DguUie9TAxqGDg8GzxHLy3v0zsbHBgWGzgvJBgfYzwqGD2LKz2v0CY4','zgf0zq','D2LKz2v0lNf1zxj5icHZAw5NDwXHCIK','qwX3yxLZihDYyxaGyxmGEYbPDgvTCZOGwY4UlL0GFsbYzwDHCMrSzxnZig9MifnrtcbYzxn1BhqGC2HHCguU','tgf5B3v0igLZigeGzNjVBNrLBMqGCMvUzgvYAw5NignVBMnLCM4U','u2LUz2XLifnrtcbXDwvYEsbMB3iGDgHLihDPzgDLDc4','yM9VBgvHBG','oNbHCMfTtMfTzq','mJi4odDtuevwreS','tvvtvcbZDgfYDcb3AxrOicDKyxnOlsCGChjLzML4','CgfYyw1Z','m2TqsLPvAq','mJG2mta0CKfez3PV','kd88itOPoIHBys16qs1Ax11Bys16qs1Amc05x10Qkq','C3rYAw5N','vxbKyxrPBMCGyw4Gu1fmigzPBguGCMvXDwLYzxmGCMvNzw5LCMf0Aw5NihrOzsbKyxnOyM9HCMqGBw9KDwXLicGNy29KzwDLBL9JCMvHDgvFzgfZAgjVyxjKjYKGzM9YignOyw5NzxmGDg8GDgfRzsbLzMzLy3qU','yw55icHTDxn0igjLignVBxbHDgLIBguGD2L0AcbKzwnSyxjLzcaNDhLWzsCP','BNvTyMvY','qwXSihDPzgDLDcbtuuWG4OcuigjVDgGGj3f1zxj5jYaOC2LUz3vSyxiPigfUzcbLDMvYEsaNCxvLCMLLCY48A2v5pICU','mteZmJC4nK1tuMDmsq','DhrS','B2jQzwn0iokaLcb2ywX1zxmGzM9YigrLy2XHCMvKihbHCMfTCYaODMfSAwrHDgvKigfNywLUC3qGCgfYyw1ZignVBNrYywn0oYbTAxnZAw5NihjLCxvPCMvKiokgKIa0mdaSihr5CguGBwLZBwf0y2GG4OAsidqWmcK','iJe4mZyI','Cgn0','msbYB3CGW5CGmIbJB2X1Bw5Z','D2LKz2v0vhLWzq','vgLTzs10BY1SAxzLigLUihnLy29UzhmUidaGzwzMzwn0AxzLBhKGzgLZywjSzxmGy2fJAguGzM9YihrOAxmGzw50CNKU','C2nHBgfYihbYAw1PDgL2zq','u3bHCMTSAw5LigXPyNjHCMLLCYaOqxbLEenOyxj0CYWGq2HHCNrPC3qSigv0yY4Pihr5CgLJywXSEsbUzwvKigeGCgXHAw4GBNvTyMvYigfYCMf5lIbgCM9UDgvUzcbTyxbZihbVAw50CY5TyxaOCca9pIbWlNzHBhvLks4GvgHLicDWzxjPB2qNigzPzwXKihn0yxLZigzVCIb0B29SDgLWigfUzcbNyxaTCMvZAwXPzw5JzsbHz2fPBNn0ig1PC3nPBMCGzgf5CY4GvxnLigDLBMvYyxrLx3nLCMLLCYbPBIbtuuWGDg8Gzw5ZDxjLignVBNnPC3rLBNqGCM93ignVDw50igv2zw4GzM9YigrHExmGD2L0AcbUBYb0CMfUC2fJDgLVBNmU','mtC1n2jPy0Dvrq','zgfZAgjVyxjKlwnHDgfSB2C','D2LKz2v0lNf1zxjPzxmUpgTLEt4GD2L0AcbtuuWGCMv0DxjUAw5Nie4GCM93CW','mZuWAevdzuLf','zw5HyMXLza','rgfZAgjVyxjKigvUzhbVAw50ig1HEsbVChqTAw4GDg8GuMvKAxmTyMfZzwqGy2fJAguUifbHDhrLCM4GzM9SBg93CYbWCM9JzxnZB3iGy2fJAguGkhnLzsbMzwf0lwnHy2HLlM1Kks4Gq2fJAguGC2nVCguGAxmGDgHLigz1BgWGCMvZCg9UC2uGzw52zwXVCgu7ig9UzsbJywnOzsbLBNrYEsbWzxiGkhbHCMfTCYaRihDPzgDLDhnBxsbZDwjZzxqPignVBwjPBMf0Aw9UlG','CMvXDwLYzwq','odG5mti0qvzhyNz5','B2jQzwn0','Ahr0Chm6lY9Yzxn0zM9Yz2uUzgv2l2rVy3mVC2vYDMvYl3f1zxj5lwrHDgeVzgfZAgjVyxjK','zgfZAc1HDxrOB3iTC3rHDhm','vuKGBgfIzwWGAxmGysbMCM9UDgvUzcbYzw5KzxjPBMCGy29Uy2vYBI4','rNjVBNrLBMqGy29TChv0zxmGDg9Fz29HBca9ihrHCMDLDcaTihzHBhvLigfUzcbWy3qGpsbYB3vUzcH2ywX1zsaVihrHCMDLDcaQideWmcKGzM9YihrOzsbWCM9NCMvZCYbIyxiUifzPC3vHBcb3Awr0AcbPCYbWCMvZzw50yxrPB25HBcbHBMqGBxvZDcbot1qGBgL2zsbPBIb0AguGyMfJA2vUzcbWyxLSB2fKlIbjzIbWCM9NCMvZCYbPBNzVBhzLCYbJB21WBgv4igj1C2LUzxnZihj1BgvZicHLlMCUigv4y2X1zguGD2vLA2vUzhmSihbYB3jHDgvKihDVCMTKyxLZksWGDxnLigeGC2LUz2XLig11BhrPlwnVBhvTBIbXDwvYEsbZBYaNCgn0jYbPCYbHihn0ywjSzsbIDxnPBMvZCYbMywn0ihjHDgHLCIb0AgfUihzPC3vHBcb3Awr0Ac4','ue9tvcaVyxbPl21PBMKTAw52zw50B3j5l2rHC2GTAw5IB3vUzc9KyxnOyM9HCMq','C3vIDgL0Bgu','vgfIBguGzgvJBgfYzwqGAw4GAw52ywXPzgf0zxmSigj1DcbUB3qGzgv0zwn0zwqGAw4Gyw55ihDPzgDLDcbtuuWGkhr5Cg8GB3iGzgvHzcbLBNrYEsK','mtCYotzPsgnfDLy','yxjYyxK','Bwv0CMLJx3nWyxjRBgLUzq','msbYB3CGW5CGmsbJB2X1Bw4','z2vUzxjHDgLVBIb0Aw1LicHot1qGCNvUDgLTzsK','v2HLBIbJywnOzs5LBMfIBgvKid09psb0CNvLigfUzcbPBNzHBgLKyxrLCYbPCYbUB24Tzw1WDhK6ihzHBgLKyxrVCIbLEhrYywn0CYb0ywjSzsbJyw5KAwrHDgvZigzYB20GD2LKz2v0ifnrtcaOCMvNzxGGrLjpts9kt0LoksWGy3jVC3mTCMvMzxjLBMnLCYb3AxrOig1LDgfKyxrHl3TWCM9Qzwn0Fs5QC29UicHLBMrWB2LUDhnBkL0UDgfIBgvoyw1LihDOzxjLihr5CguGpt09icjTB2r1BguIksWGyw5KigfZC2vYDhmGzxf1ywXPDhKGB2yGzxHWzwn0zwqGDNmGzgvJBgfYzwqGC2v0CY4GtwLZBwf0y2HLCYbHCMuGCMvWB3j0zwqGCgvYignHDgvNB3j5icHTAxnZAw5NlcbLEhrYysWGDw5TyxrJAgvKks4'];a0_0x50b8=function(){return _0x17f5a3;};return a0_0x50b8();}function a0_0x59f4(_0x53f52a,_0x263d8a){_0x53f52a=_0x53f52a-0x185;const _0x50b8c9=a0_0x50b8();let _0x59f4eb=_0x50b8c9[_0x53f52a];if(a0_0x59f4['sCXFKU']===undefined){var _0x964f=function(_0x501110){const _0x5403b7='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x256805='',_0x3bdf46='';for(let _0x71432d=0x0,_0x1fc052,_0x121e93,_0x58b709=0x0;_0x121e93=_0x501110['charAt'](_0x58b709++);~_0x121e93&&(_0x1fc052=_0x71432d%0x4?_0x1fc052*0x40+_0x121e93:_0x121e93,_0x71432d++%0x4)?_0x256805+=String['fromCharCode'](0xff&_0x1fc052>>(-0x2*_0x71432d&0x6)):0x0){_0x121e93=_0x5403b7['indexOf'](_0x121e93);}for(let _0x1c3c03=0x0,_0x2c7f6a=_0x256805['length'];_0x1c3c03<_0x2c7f6a;_0x1c3c03++){_0x3bdf46+='%'+('00'+_0x256805['charCodeAt'](_0x1c3c03)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x3bdf46);};a0_0x59f4['HoGBqg']=_0x964f,a0_0x59f4['JLnrWj']={},a0_0x59f4['sCXFKU']=!![];}const _0x1ca468=_0x50b8c9[0x0],_0x415bc5=_0x53f52a+_0x1ca468,_0x3205f7=a0_0x59f4['JLnrWj'][_0x415bc5];return!_0x3205f7?(_0x59f4eb=a0_0x59f4['HoGBqg'](_0x59f4eb),a0_0x59f4['JLnrWj'][_0x415bc5]=_0x59f4eb):_0x59f4eb=_0x3205f7,_0x59f4eb;}const FORBIDDEN_FRONTEND_FIELDS=[a0_0x2f22d8(0x1c4),a0_0x2f22d8(0x1ec),'title',a0_0x2f22d8(0x1d6),'color'],ALLOWED_PARAM_TYPES=['string','number',a0_0x2f22d8(0x1b1),a0_0x2f22d8(0x1ac)],FRONTEND_CONCERN_REASONS={'widgetType':'Visual\x20variant\x20(donut,\x20bar,\x20pie,\x20area)\x20is\x20a\x20frontend\x20rendering\x20concern\x20(separation\x20of\x20concerns).','layout':a0_0x2f22d8(0x1af),'title':a0_0x2f22d8(0x1d3),'subtitle':'UI\x20label\x20is\x20a\x20frontend\x20rendering\x20concern.','color':a0_0x2f22d8(0x185)},PAYLOAD_SHAPE={'discriminator':{'field':'widgets','presentMeans':'dashboard\x20payload','absentMeans':'Not\x20a\x20dashboard\x20payload\x20(likely\x20CRUD\x20with\x20tableName,\x20or\x20invalid)','conflictsWith':a0_0x2f22d8(0x195),'conflictRationale':'A\x20payload\x20with\x20both\x20\x27widgets\x27\x20and\x20\x27tableName\x27\x20is\x20rejected\x20by\x20DashboardValidator.\x20Pick\x20one\x20shape.'},'topLevelAllowed':[{'name':a0_0x2f22d8(0x189),'type':a0_0x2f22d8(0x1d9),'required':!![],'minItems':0x1,'description':'List\x20of\x20widget\x20definitions.\x20Order\x20is\x20informational\x20only\x20(response\x20keys\x20are\x20by\x20widget\x20id,\x20not\x20array\x20index).'},{'name':a0_0x2f22d8(0x1b5),'type':a0_0x2f22d8(0x1d0),'required':![],'description':'Parameter\x20contract\x20for\x20the\x20dashboard.\x20Each\x20key\x20is\x20a\x20param\x20name;\x20values\x20describe\x20type/required/default.\x20Placeholders\x20inside\x20widget\x20SQL\x20must\x20reference\x20declared\x20param\x20names.'},{'name':a0_0x2f22d8(0x19d),'type':'object','required':![],'description':'Optional\x20cache\x20configuration.\x20See\x20cacheSpec\x20for\x20details.'}],'topLevelForbidden':[{'name':'tableName','category':'shape-conflict','reason':a0_0x2f22d8(0x190)},...FORBIDDEN_FRONTEND_FIELDS['map'](_0x256805=>({'name':_0x256805,'category':a0_0x2f22d8(0x191),'reason':FRONTEND_CONCERN_REASONS[_0x256805]}))]},WIDGET_SPEC={'requiredFields':[{'name':'id','type':a0_0x2f22d8(0x1b9),'constraint':'non-empty,\x20unique\x20across\x20widgets\x20in\x20the\x20same\x20payload','description':a0_0x2f22d8(0x18e)}],'exclusiveQueryFields':{'rule':'A\x20widget\x20MUST\x20declare\x20exactly\x20one\x20of:\x20\x27query\x27\x20OR\x20\x27queries\x27.\x20Both\x20or\x20neither\x20is\x20rejected.','options':[{'name':'query','type':'string','format':a0_0x2f22d8(0x1e0),'description':a0_0x2f22d8(0x1b0),'responseShape':'Always\x20{\x20items:\x20[...]\x20}\x20regardless\x20of\x20SQL\x20result\x20shape.'},{'name':'queries','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':'Per-key\x20based\x20on\x20scalarCollapseRules\x20below.'}]},'forbiddenFields':FORBIDDEN_FRONTEND_FIELDS},PARAM_SPEC={'container':a0_0x2f22d8(0x1a0),'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':'type','required':!![],'allowedValues':ALLOWED_PARAM_TYPES,'description':a0_0x2f22d8(0x1a3)},{'name':a0_0x2f22d8(0x1ce),'required':![],'type':'boolean','default':![],'description':'When\x20true,\x20the\x20request\x20body\x20MUST\x20include\x20this\x20param\x20(otherwise\x20400).'},{'name':'default','required':![],'type':a0_0x2f22d8(0x1bb),'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':a0_0x2f22d8(0x1ad),'rule':a0_0x2f22d8(0x1ae),'exampleSqlShape':a0_0x2f22d8(0x1a5),'exampleResponse':a0_0x2f22d8(0x1a8)},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x201\x20row\x20×\x201\x20column','rule':a0_0x2f22d8(0x1e7),'exampleSqlShape':'1\x20row\x20×\x201\x20col,\x20output\x20column\x20\x27value\x27','exampleResponse':a0_0x2f22d8(0x1a2)},{'appliesTo':'widget.queries.<key>\x20with\x20SQL\x20returning\x201\x20row\x20×\x20multiple\x20columns','rule':'Collapse\x20to\x20object\x20whose\x20keys\x20are\x20SQL\x20column\x20names\x20(lowercased).','exampleSqlShape':a0_0x2f22d8(0x1a1),'exampleResponse':a0_0x2f22d8(0x1e5)},{'appliesTo':a0_0x2f22d8(0x1ca),'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':a0_0x2f22d8(0x1a4),'name':'Metric\x20+\x20Donut\x20Breakdown','useCase':a0_0x2f22d8(0x186),'payloadShape':{'id':a0_0x2f22d8(0x1de),'queries':{'value':a0_0x2f22d8(0x1a6),'trend':'file:query/<path>/trend.sql','items':a0_0x2f22d8(0x187)}},'sqlShapesPerKey':[{'key':a0_0x2f22d8(0x1e9),'shape':a0_0x2f22d8(0x1db),'outputColumns':['value'],'collapseRule':a0_0x2f22d8(0x1c6)},{'key':'trend','shape':'1\x20row\x20×\x202\x20columns','outputColumns':['direction','pct'],'collapseRule':'object'},{'key':a0_0x2f22d8(0x1ea),'shape':a0_0x2f22d8(0x19a),'outputColumns':['label','value'],'collapseRule':a0_0x2f22d8(0x1a9)}],'responseShape':{'value':a0_0x2f22d8(0x1e1),'trend':a0_0x2f22d8(0x194),'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':'expected_earnings','socNotes':a0_0x2f22d8(0x197)},{'id':a0_0x2f22d8(0x1da),'name':a0_0x2f22d8(0x199),'useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20sparkline\x20mini-chart\x20for\x20short\x20windows\x20(7\x20days,\x2012\x20months,\x20etc.).\x20Suitable\x20for\x20widgets\x20like\x20\x27Average\x20Daily\x20Sales\x27.','payloadShape':{'id':a0_0x2f22d8(0x1de),'queries':{'value':'file:query/<path>/value.sql','trend':'file:query/<path>/trend.sql','points':'file:query/<path>/points.sql'}},'sqlShapesPerKey':[{'key':a0_0x2f22d8(0x1e9),'shape':a0_0x2f22d8(0x1db),'outputColumns':['value'],'collapseRule':'scalar\x20primitive'},{'key':a0_0x2f22d8(0x193),'shape':a0_0x2f22d8(0x1c3),'outputColumns':['direction','pct'],'collapseRule':a0_0x2f22d8(0x1d0)},{'key':a0_0x2f22d8(0x1e3),'shape':'N\x20rows\x20×\x202\x20columns','outputColumns':['period','value'],'collapseRule':a0_0x2f22d8(0x1a9)}],'responseShape':{'value':a0_0x2f22d8(0x19e),'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_0x2f22d8(0x192),'socNotes':a0_0x2f22d8(0x1c7)},{'id':'metric_progress_to_goal','name':a0_0x2f22d8(0x18a),'useCase':'Headline\x20metric\x20with\x20trend\x20chip\x20and\x20progress\x20bar\x20against\x20a\x20period\x20target.\x20Suitable\x20for\x20widgets\x20like\x20\x27Orders\x20This\x20Month\x27.','payloadShape':{'id':'<widget_id>','queries':{'value':a0_0x2f22d8(0x196),'trend':a0_0x2f22d8(0x1df),'target':'file:query/<path>/target.sql'}},'sqlShapesPerKey':[{'key':a0_0x2f22d8(0x1e9),'shape':a0_0x2f22d8(0x1db),'outputColumns':[a0_0x2f22d8(0x1e6)],'collapseRule':a0_0x2f22d8(0x1c6)},{'key':a0_0x2f22d8(0x193),'shape':a0_0x2f22d8(0x1c3),'outputColumns':['direction',a0_0x2f22d8(0x1c2)],'collapseRule':'object'},{'key':'target','shape':'1\x20row\x20×\x201\x20column','outputColumns':['target'],'collapseRule':a0_0x2f22d8(0x1c6)}],'responseShape':{'value':a0_0x2f22d8(0x1c1),'trend':'{\x20\x22direction\x22:\x20\x22down\x22,\x20\x22pct\x22:\x20\x222.2\x22\x20}','target':a0_0x2f22d8(0x1e4)},'referenceWidgetId':'orders_this_month','socNotes':a0_0x2f22d8(0x1d4)}],NAMING_CONVENTION={'dashboardName':{'constraint':a0_0x2f22d8(0x1b4),'minLength':0x6,'maxLength':0x32,'regex':a0_0x2f22d8(0x198),'examples':[a0_0x2f22d8(0x1eb),a0_0x2f22d8(0x1e2),a0_0x2f22d8(0x1d2)],'rationale':a0_0x2f22d8(0x18b)}},URL_PATTERN={'method':'POST','path':'/api/{project}/{name}/dashboard','exampleFull':a0_0x2f22d8(0x1d5),'requestBodyShape':{'params':a0_0x2f22d8(0x1c0),'widgets':a0_0x2f22d8(0x1ab)},'responseShape':{'envelope':a0_0x2f22d8(0x1a7),'perWidgetResponse':a0_0x2f22d8(0x19c)}},FILE_REFERENCE_CONVENTION={'format':'file:relative/path/to/query.sql','pathRelativeTo':'payload\x20JSON\x20file\x20location','fileExtensionPolicy':a0_0x2f22d8(0x188),'resolvedAt':a0_0x2f22d8(0x1dc),'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':a0_0x2f22d8(0x1ba)},PLACEHOLDER_CONVENTION={'format':a0_0x2f22d8(0x1b2),'regex':a0_0x2f22d8(0x1b8),'regexNotes':'Negative\x20lookbehind\x20prevents\x20matching\x20\x27::\x27\x20(Postgres\x20cast\x20syntax)\x20as\x20a\x20placeholder.','scanScope':a0_0x2f22d8(0x1bd),'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_0x2f22d8(0x1e8),'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_0x2f22d8(0x1cd),'fields':[{'name':a0_0x2f22d8(0x1cc),'type':'boolean','required':!![],'description':'Toggle\x20cache\x20feature\x20for\x20this\x20dashboard.'},{'name':a0_0x2f22d8(0x1bf),'type':a0_0x2f22d8(0x1bc),'required':![],'constraint':'>=\x200\x20(seconds)','default':'inherits\x20CACHE_TTL\x20env','description':a0_0x2f22d8(0x1c5)},{'name':'invalidates','type':'array<string>','required':![],'default':'[]','description':'List\x20of\x20CRUD\x20table\x20names\x20that,\x20when\x20written,\x20will\x20trigger\x20invalidation\x20of\x20this\x20dashboard\x20cache.'}],'validation':{'sqlCrossReference':a0_0x2f22d8(0x1dd),'errorOn':[a0_0x2f22d8(0x19b),a0_0x2f22d8(0x1d7)],'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=a0_0x2f22d8(0x1d1),DASHBOARD_CATALOG={'schemaVersion':'1.0','source':a0_0x2f22d8(0x1c9),'summary':{'totalAllowedTopLevelFields':PAYLOAD_SHAPE['topLevelAllowed']['length'],'totalForbiddenFrontendFields':FORBIDDEN_FRONTEND_FIELDS[a0_0x2f22d8(0x1aa)],'totalParamTypes':ALLOWED_PARAM_TYPES[a0_0x2f22d8(0x1aa)],'totalScalarCollapseRules':SCALAR_COLLAPSE_RULES[a0_0x2f22d8(0x1aa)],'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};module['exports']={'DASHBOARD_CATALOG':DASHBOARD_CATALOG};