@njeybe/nativ 2.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 (140) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/LICENSE +21 -0
  3. package/README.md +334 -0
  4. package/bin/cli.js +31 -0
  5. package/dist/commands/bench.d.ts +8 -0
  6. package/dist/commands/bench.js +52 -0
  7. package/dist/commands/db.d.ts +22 -0
  8. package/dist/commands/db.js +415 -0
  9. package/dist/commands/doctor.d.ts +38 -0
  10. package/dist/commands/doctor.js +169 -0
  11. package/dist/commands/hook.d.ts +31 -0
  12. package/dist/commands/hook.js +105 -0
  13. package/dist/commands/init.d.ts +3 -0
  14. package/dist/commands/init.js +144 -0
  15. package/dist/commands/mcp.d.ts +5 -0
  16. package/dist/commands/mcp.js +16 -0
  17. package/dist/commands/setup.d.ts +17 -0
  18. package/dist/commands/setup.js +51 -0
  19. package/dist/commands/status.d.ts +5 -0
  20. package/dist/commands/status.js +75 -0
  21. package/dist/commands/task.d.ts +83 -0
  22. package/dist/commands/task.js +807 -0
  23. package/dist/commands/test-gen.d.ts +14 -0
  24. package/dist/commands/test-gen.js +52 -0
  25. package/dist/commands/triage.d.ts +70 -0
  26. package/dist/commands/triage.js +343 -0
  27. package/dist/commands/update.d.ts +14 -0
  28. package/dist/commands/update.js +112 -0
  29. package/dist/commands/validate.d.ts +1 -0
  30. package/dist/commands/validate.js +89 -0
  31. package/dist/commands/verify.d.ts +8 -0
  32. package/dist/commands/verify.js +65 -0
  33. package/dist/commands/worktree.d.ts +47 -0
  34. package/dist/commands/worktree.js +396 -0
  35. package/dist/core/benchmark.d.ts +57 -0
  36. package/dist/core/benchmark.js +772 -0
  37. package/dist/core/enforcement.d.ts +80 -0
  38. package/dist/core/enforcement.js +271 -0
  39. package/dist/core/legacy-templates.d.ts +24 -0
  40. package/dist/core/legacy-templates.js +60 -0
  41. package/dist/core/lock-manager.d.ts +54 -0
  42. package/dist/core/lock-manager.js +260 -0
  43. package/dist/core/root-resolver.d.ts +32 -0
  44. package/dist/core/root-resolver.js +149 -0
  45. package/dist/core/setup-assets.d.ts +123 -0
  46. package/dist/core/setup-assets.js +438 -0
  47. package/dist/core/telemetry.d.ts +156 -0
  48. package/dist/core/telemetry.js +416 -0
  49. package/dist/core/test-generator-types.d.ts +227 -0
  50. package/dist/core/test-generator-types.js +179 -0
  51. package/dist/core/test-generator.d.ts +34 -0
  52. package/dist/core/test-generator.js +1690 -0
  53. package/dist/core/tier1-liaison.d.ts +139 -0
  54. package/dist/core/tier1-liaison.js +320 -0
  55. package/dist/core/verifier.d.ts +52 -0
  56. package/dist/core/verifier.js +249 -0
  57. package/dist/core/version.d.ts +5 -0
  58. package/dist/core/version.js +14 -0
  59. package/dist/core/worktree-merge.d.ts +30 -0
  60. package/dist/core/worktree-merge.js +98 -0
  61. package/dist/db/data-engine.d.ts +46 -0
  62. package/dist/db/data-engine.js +802 -0
  63. package/dist/db/diff.d.ts +39 -0
  64. package/dist/db/diff.js +420 -0
  65. package/dist/db/env-parser.d.ts +86 -0
  66. package/dist/db/env-parser.js +549 -0
  67. package/dist/db/introspector.d.ts +21 -0
  68. package/dist/db/introspector.js +835 -0
  69. package/dist/db/types.d.ts +217 -0
  70. package/dist/db/types.js +9 -0
  71. package/dist/governor/circuit-breaker.d.ts +100 -0
  72. package/dist/governor/circuit-breaker.js +231 -0
  73. package/dist/governor/evaluator.d.ts +67 -0
  74. package/dist/governor/evaluator.js +851 -0
  75. package/dist/governor/index.d.ts +6 -0
  76. package/dist/governor/index.js +6 -0
  77. package/dist/governor/rules/api-rules.d.ts +2 -0
  78. package/dist/governor/rules/api-rules.js +140 -0
  79. package/dist/governor/rules/db-rules.d.ts +2 -0
  80. package/dist/governor/rules/db-rules.js +180 -0
  81. package/dist/governor/store.d.ts +33 -0
  82. package/dist/governor/store.js +84 -0
  83. package/dist/governor/types.d.ts +64 -0
  84. package/dist/governor/types.js +1 -0
  85. package/dist/index.d.ts +3 -0
  86. package/dist/index.js +358 -0
  87. package/dist/mcp/server.d.ts +3 -0
  88. package/dist/mcp/server.js +309 -0
  89. package/dist/providers/claude-api.d.ts +5 -0
  90. package/dist/providers/claude-api.js +75 -0
  91. package/dist/providers/claude-cli.d.ts +13 -0
  92. package/dist/providers/claude-cli.js +134 -0
  93. package/dist/providers/gemini.d.ts +8 -0
  94. package/dist/providers/gemini.js +78 -0
  95. package/dist/providers/index.d.ts +5 -0
  96. package/dist/providers/index.js +5 -0
  97. package/dist/providers/registry.d.ts +38 -0
  98. package/dist/providers/registry.js +121 -0
  99. package/dist/providers/types.d.ts +74 -0
  100. package/dist/providers/types.js +25 -0
  101. package/dist/runner/agent-supervisor.d.ts +363 -0
  102. package/dist/runner/agent-supervisor.js +1672 -0
  103. package/dist/scanner/context-builder.d.ts +2 -0
  104. package/dist/scanner/context-builder.js +29 -0
  105. package/dist/scanner/detector.d.ts +2 -0
  106. package/dist/scanner/detector.js +760 -0
  107. package/dist/scanner/types.d.ts +80 -0
  108. package/dist/scanner/types.js +50 -0
  109. package/dist/server/studio-server.d.ts +39 -0
  110. package/dist/server/studio-server.js +1931 -0
  111. package/dist/server/studio-ui.d.ts +15 -0
  112. package/dist/server/studio-ui.js +5321 -0
  113. package/package.json +70 -0
  114. package/plugin/.claude-plugin/plugin.json +18 -0
  115. package/plugin/.mcp.json +10 -0
  116. package/plugin/agents/architect.md +28 -0
  117. package/plugin/agents/verifier.md +28 -0
  118. package/plugin/agents/worker.md +26 -0
  119. package/plugin/hooks/hooks.json +28 -0
  120. package/plugin/skills/nativ/SKILL.md +75 -0
  121. package/templates/.claudeignore +45 -0
  122. package/templates/AGENTS.md +67 -0
  123. package/templates/CLAUDE.md +32 -0
  124. package/templates/GEMINI.md +66 -0
  125. package/templates/claude-agents/architect.md +28 -0
  126. package/templates/claude-agents/verifier.md +28 -0
  127. package/templates/claude-agents/worker.md +26 -0
  128. package/templates/dot-ai/api_contracts.json +66 -0
  129. package/templates/dot-ai/context.md +38 -0
  130. package/templates/dot-ai/db_schema.json +39 -0
  131. package/templates/dot-ai/master_plan.json +28 -0
  132. package/templates/dot-ai/subagents/backend.md +11 -0
  133. package/templates/dot-ai/subagents/database.md +10 -0
  134. package/templates/dot-ai/subagents/db-migration.md +11 -0
  135. package/templates/dot-ai/subagents/devops-agent.md +11 -0
  136. package/templates/dot-ai/subagents/flutter-developer.md +11 -0
  137. package/templates/dot-ai/subagents/frontend.md +126 -0
  138. package/templates/dot-ai/subagents/qa-tester.md +13 -0
  139. package/templates/dot-ai/subagents/security-auditor.md +11 -0
  140. package/templates/dot-ai/ui_specs.md +78 -0
@@ -0,0 +1,802 @@
1
+ import { detectEngine, maskConnectionString, MASK, parseFirestoreUrl, sqlitePathFromUrl } from './env-parser.js';
2
+ /** Validates entity and identifier names to guard against SQL injection in DDL/identifiers. */
3
+ function assertValidIdentifier(name, label = 'Identifier') {
4
+ if (!name || typeof name !== 'string' || !/^[a-zA-Z0-9_.-]+$/.test(name.trim())) {
5
+ throw new Error(`Invalid ${label}: "${name}" contains disallowed characters`);
6
+ }
7
+ }
8
+ /** Removes raw credentials or passwords from driver error messages. */
9
+ function sanitizeError(err, url) {
10
+ let message = err instanceof Error ? err.message : String(err);
11
+ if (!message)
12
+ message = 'Unknown database error';
13
+ message = message.split(url).join(maskConnectionString(url));
14
+ try {
15
+ const password = new URL(url).password;
16
+ if (password) {
17
+ message = message.split(password).join(MASK);
18
+ const decoded = decodeURIComponent(password);
19
+ if (decoded)
20
+ message = message.split(decoded).join(MASK);
21
+ }
22
+ }
23
+ catch {
24
+ // Non-URL (e.g. SQLite path)
25
+ }
26
+ return new Error(message);
27
+ }
28
+ /** Recursively serializes driver-specific objects (BSON ObjectId, Dates, Buffers, Timestamps) to pure JSON. */
29
+ export function serializeValue(val) {
30
+ if (val === null || val === undefined)
31
+ return null;
32
+ if (typeof val === 'bigint')
33
+ return Number(val);
34
+ if (val instanceof Date)
35
+ return val.toISOString();
36
+ if (val instanceof Uint8Array || Buffer.isBuffer(val)) {
37
+ const buf = Buffer.from(val);
38
+ if (buf.length <= 512 * 1024) {
39
+ return `data:application/octet-stream;base64,${buf.toString('base64')}`;
40
+ }
41
+ return `[Binary data ${buf.length} bytes]`;
42
+ }
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ const anyVal = val;
45
+ if (anyVal._bsontype === 'ObjectId' || (anyVal.constructor && anyVal.constructor.name === 'ObjectId')) {
46
+ return anyVal.toString();
47
+ }
48
+ if (anyVal._bsontype === 'Decimal128') {
49
+ return parseFloat(anyVal.toString());
50
+ }
51
+ if (typeof anyVal.toDate === 'function') {
52
+ try {
53
+ return anyVal.toDate().toISOString();
54
+ }
55
+ catch {
56
+ // Ignore
57
+ }
58
+ }
59
+ if (Array.isArray(val)) {
60
+ return val.map(serializeValue);
61
+ }
62
+ if (typeof val === 'object') {
63
+ const out = {};
64
+ for (const [k, v] of Object.entries(val)) {
65
+ out[k] = serializeValue(v);
66
+ }
67
+ return out;
68
+ }
69
+ return val;
70
+ }
71
+ function serializeRow(row) {
72
+ const result = {};
73
+ for (const [key, value] of Object.entries(row)) {
74
+ result[key] = serializeValue(value);
75
+ }
76
+ return result;
77
+ }
78
+ // ─── PostgreSQL Driver ─────────────────────────────────────────────────────────
79
+ async function postgresFetch(url, entity, options) {
80
+ assertValidIdentifier(entity, 'table name');
81
+ const pg = (await import('pg')).default;
82
+ const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 8000, statement_timeout: 15000 });
83
+ await client.connect();
84
+ try {
85
+ // 1. Detect primary key
86
+ const pkRes = await client.query(`SELECT kcu.column_name
87
+ FROM information_schema.table_constraints tc
88
+ JOIN information_schema.key_column_usage kcu
89
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name
90
+ WHERE tc.table_schema = current_schema() AND tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = $1
91
+ LIMIT 1`, [entity]);
92
+ const primaryKey = pkRes.rows[0]?.column_name ?? 'id';
93
+ // 2. Detect searchable text columns
94
+ const colRes = await client.query(`SELECT column_name, data_type
95
+ FROM information_schema.columns
96
+ WHERE table_schema = current_schema() AND table_name = $1`, [entity]);
97
+ const allCols = colRes.rows.map((r) => r.column_name);
98
+ // 3. Build search clause
99
+ const search = options.search?.trim();
100
+ let whereClause = '';
101
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+ const params = [];
103
+ if (search && allCols.length > 0) {
104
+ const searchConditions = allCols.map((col) => `"${col.replace(/"/g, '""')}"::text ILIKE $1`);
105
+ whereClause = ` WHERE (${searchConditions.join(' OR ')})`;
106
+ params.push(`%${search}%`);
107
+ }
108
+ // 4. Count total matching rows
109
+ const countQuery = `SELECT COUNT(*) AS total FROM "${entity.replace(/"/g, '""')}"${whereClause}`;
110
+ const countRes = await client.query(countQuery, params);
111
+ const totalCount = parseInt(countRes.rows[0]?.total || '0', 10);
112
+ // 5. Pagination and sorting
113
+ const limit = Math.max(1, Math.min(100, options.limit ?? 25));
114
+ const page = Math.max(1, options.page ?? 1);
115
+ const offset = (page - 1) * limit;
116
+ const totalPages = Math.ceil(totalCount / limit) || 1;
117
+ let sortCol = primaryKey;
118
+ if (options.sort && allCols.includes(options.sort)) {
119
+ sortCol = options.sort;
120
+ }
121
+ const order = options.order === 'desc' ? 'DESC' : 'ASC';
122
+ const dataParams = [...params, limit, offset];
123
+ const dataQuery = `SELECT * FROM "${entity.replace(/"/g, '""')}"${whereClause} ORDER BY "${sortCol.replace(/"/g, '""')}" ${order} LIMIT $${dataParams.length - 1} OFFSET $${dataParams.length}`;
124
+ const dataRes = await client.query(dataQuery, dataParams);
125
+ return {
126
+ entity,
127
+ entityType: 'table',
128
+ primaryKey,
129
+ page,
130
+ limit,
131
+ totalCount,
132
+ totalPages,
133
+ rows: dataRes.rows.map(serializeRow),
134
+ };
135
+ }
136
+ finally {
137
+ await client.end().catch(() => undefined);
138
+ }
139
+ }
140
+ async function postgresInsert(url, entity, record) {
141
+ assertValidIdentifier(entity, 'table name');
142
+ const pg = (await import('pg')).default;
143
+ const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 8000, statement_timeout: 15000 });
144
+ await client.connect();
145
+ try {
146
+ const keys = Object.keys(record);
147
+ if (keys.length === 0) {
148
+ const res = await client.query(`INSERT INTO "${entity.replace(/"/g, '""')}" DEFAULT VALUES RETURNING *`);
149
+ const row = res.rows[0] ?? {};
150
+ const insertedId = row.id ?? 1;
151
+ return { success: true, insertedId, record: serializeRow(row) };
152
+ }
153
+ for (const k of keys)
154
+ assertValidIdentifier(k, 'column name');
155
+ const cols = keys.map((k) => `"${k.replace(/"/g, '""')}"`).join(', ');
156
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(', ');
157
+ const values = keys.map((k) => record[k]);
158
+ const query = `INSERT INTO "${entity.replace(/"/g, '""')}" (${cols}) VALUES (${placeholders}) RETURNING *`;
159
+ const res = await client.query(query, values);
160
+ const row = res.rows[0] ?? {};
161
+ const insertedId = row.id ?? row._id ?? 'unknown';
162
+ return { success: true, insertedId, record: serializeRow(row) };
163
+ }
164
+ finally {
165
+ await client.end().catch(() => undefined);
166
+ }
167
+ }
168
+ async function postgresUpdate(url, entity, primaryKey, updates) {
169
+ assertValidIdentifier(entity, 'table name');
170
+ const pg = (await import('pg')).default;
171
+ const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 8000, statement_timeout: 15000 });
172
+ await client.connect();
173
+ try {
174
+ const pkRes = await client.query(`SELECT kcu.column_name
175
+ FROM information_schema.table_constraints tc
176
+ JOIN information_schema.key_column_usage kcu
177
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name
178
+ WHERE tc.table_schema = current_schema() AND tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = $1
179
+ LIMIT 1`, [entity]);
180
+ const pkCol = pkRes.rows[0]?.column_name ?? 'id';
181
+ const keys = Object.keys(updates).filter((k) => k !== pkCol);
182
+ if (keys.length === 0) {
183
+ throw new Error('No updatable columns provided');
184
+ }
185
+ for (const k of keys)
186
+ assertValidIdentifier(k, 'column name');
187
+ const setClause = keys.map((k, i) => `"${k.replace(/"/g, '""')}" = $${i + 1}`).join(', ');
188
+ const values = keys.map((k) => updates[k]);
189
+ values.push(primaryKey);
190
+ const query = `UPDATE "${entity.replace(/"/g, '""')}" SET ${setClause} WHERE "${pkCol.replace(/"/g, '""')}" = $${values.length} RETURNING *`;
191
+ const res = await client.query(query, values);
192
+ if (res.rowCount === 0) {
193
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
194
+ }
195
+ return { success: true, updatedCount: res.rowCount ?? 1, record: serializeRow(res.rows[0]) };
196
+ }
197
+ finally {
198
+ await client.end().catch(() => undefined);
199
+ }
200
+ }
201
+ async function postgresDelete(url, entity, primaryKey) {
202
+ assertValidIdentifier(entity, 'table name');
203
+ const pg = (await import('pg')).default;
204
+ const client = new pg.Client({ connectionString: url, connectionTimeoutMillis: 8000, statement_timeout: 15000 });
205
+ await client.connect();
206
+ try {
207
+ const pkRes = await client.query(`SELECT kcu.column_name
208
+ FROM information_schema.table_constraints tc
209
+ JOIN information_schema.key_column_usage kcu
210
+ ON kcu.constraint_name = tc.constraint_name AND kcu.table_schema = tc.table_schema AND kcu.table_name = tc.table_name
211
+ WHERE tc.table_schema = current_schema() AND tc.constraint_type = 'PRIMARY KEY' AND tc.table_name = $1
212
+ LIMIT 1`, [entity]);
213
+ const pkCol = pkRes.rows[0]?.column_name ?? 'id';
214
+ const query = `DELETE FROM "${entity.replace(/"/g, '""')}" WHERE "${pkCol.replace(/"/g, '""')}" = $1`;
215
+ const res = await client.query(query, [primaryKey]);
216
+ if (res.rowCount === 0) {
217
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
218
+ }
219
+ return { success: true, deletedCount: res.rowCount ?? 1 };
220
+ }
221
+ finally {
222
+ await client.end().catch(() => undefined);
223
+ }
224
+ }
225
+ // ─── MySQL Driver ─────────────────────────────────────────────────────────────
226
+ async function mysqlFetch(url, entity, options) {
227
+ assertValidIdentifier(entity, 'table name');
228
+ const mysql = await import('mysql2/promise');
229
+ const conn = await mysql.createConnection({ uri: url, connectTimeout: 8000 });
230
+ try {
231
+ // 1. Detect primary key
232
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
233
+ const [pkRows] = (await conn.query(`SELECT COLUMN_NAME
234
+ FROM information_schema.KEY_COLUMN_USAGE
235
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
236
+ LIMIT 1`, [entity]));
237
+ const primaryKey = pkRows[0]?.COLUMN_NAME ?? 'id';
238
+ // 2. Detect column names
239
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
240
+ const [colRows] = (await conn.query(`SELECT COLUMN_NAME
241
+ FROM information_schema.COLUMNS
242
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ?`, [entity]));
243
+ const allCols = colRows.map((r) => r.COLUMN_NAME);
244
+ // 3. Search clause
245
+ const search = options.search?.trim();
246
+ let whereClause = '';
247
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
248
+ const params = [];
249
+ if (search && allCols.length > 0) {
250
+ const searchConditions = allCols.map((col) => `\`${col.replace(/`/g, '``')}\` LIKE ?`);
251
+ whereClause = ` WHERE (${searchConditions.join(' OR ')})`;
252
+ for (let i = 0; i < allCols.length; i++)
253
+ params.push(`%${search}%`);
254
+ }
255
+ // 4. Count total
256
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
257
+ const [countRows] = (await conn.query(`SELECT COUNT(*) AS total FROM \`${entity.replace(/`/g, '``')}\`${whereClause}`, params));
258
+ const totalCount = parseInt(countRows[0]?.total || '0', 10);
259
+ const limit = Math.max(1, Math.min(100, options.limit ?? 25));
260
+ const page = Math.max(1, options.page ?? 1);
261
+ const offset = (page - 1) * limit;
262
+ const totalPages = Math.ceil(totalCount / limit) || 1;
263
+ let sortCol = primaryKey;
264
+ if (options.sort && allCols.includes(options.sort)) {
265
+ sortCol = options.sort;
266
+ }
267
+ const order = options.order === 'desc' ? 'DESC' : 'ASC';
268
+ const dataParams = [...params, limit, offset];
269
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
270
+ const [dataRows] = (await conn.query(`SELECT * FROM \`${entity.replace(/`/g, '``')}\`${whereClause} ORDER BY \`${sortCol.replace(/`/g, '``')}\` ${order} LIMIT ? OFFSET ?`, dataParams));
271
+ return {
272
+ entity,
273
+ entityType: 'table',
274
+ primaryKey,
275
+ page,
276
+ limit,
277
+ totalCount,
278
+ totalPages,
279
+ rows: dataRows.map(serializeRow),
280
+ };
281
+ }
282
+ finally {
283
+ await conn.end().catch(() => undefined);
284
+ }
285
+ }
286
+ async function mysqlInsert(url, entity, record) {
287
+ assertValidIdentifier(entity, 'table name');
288
+ const mysql = await import('mysql2/promise');
289
+ const conn = await mysql.createConnection({ uri: url, connectTimeout: 8000 });
290
+ try {
291
+ const keys = Object.keys(record);
292
+ for (const k of keys)
293
+ assertValidIdentifier(k, 'column name');
294
+ const cols = keys.map((k) => `\`${k.replace(/`/g, '``')}\``).join(', ');
295
+ const placeholders = keys.map(() => '?').join(', ');
296
+ const values = keys.map((k) => record[k]);
297
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
298
+ const [res] = (await conn.query(`INSERT INTO \`${entity.replace(/`/g, '``')}\` (${cols}) VALUES (${placeholders})`, values));
299
+ const insertedId = res.insertId ?? 1;
300
+ return { success: true, insertedId, record: serializeRow({ ...record, id: insertedId }) };
301
+ }
302
+ finally {
303
+ await conn.end().catch(() => undefined);
304
+ }
305
+ }
306
+ async function mysqlUpdate(url, entity, primaryKey, updates) {
307
+ assertValidIdentifier(entity, 'table name');
308
+ const mysql = await import('mysql2/promise');
309
+ const conn = await mysql.createConnection({ uri: url, connectTimeout: 8000 });
310
+ try {
311
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
312
+ const [pkRows] = (await conn.query(`SELECT COLUMN_NAME
313
+ FROM information_schema.KEY_COLUMN_USAGE
314
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
315
+ LIMIT 1`, [entity]));
316
+ const pkCol = pkRows[0]?.COLUMN_NAME ?? 'id';
317
+ const keys = Object.keys(updates).filter((k) => k !== pkCol);
318
+ if (keys.length === 0)
319
+ throw new Error('No updatable columns provided');
320
+ for (const k of keys)
321
+ assertValidIdentifier(k, 'column name');
322
+ const setClause = keys.map((k) => `\`${k.replace(/`/g, '``')}\` = ?`).join(', ');
323
+ const values = [...keys.map((k) => updates[k]), primaryKey];
324
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
325
+ const [res] = (await conn.query(`UPDATE \`${entity.replace(/`/g, '``')}\` SET ${setClause} WHERE \`${pkCol.replace(/`/g, '``')}\` = ?`, values));
326
+ if (res.affectedRows === 0) {
327
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
328
+ }
329
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
330
+ const [updatedRows] = (await conn.query(`SELECT * FROM \`${entity.replace(/`/g, '``')}\` WHERE \`${pkCol.replace(/`/g, '``')}\` = ? LIMIT 1`, [primaryKey]));
331
+ return { success: true, updatedCount: res.affectedRows, record: serializeRow(updatedRows[0] || updates) };
332
+ }
333
+ finally {
334
+ await conn.end().catch(() => undefined);
335
+ }
336
+ }
337
+ async function mysqlDelete(url, entity, primaryKey) {
338
+ assertValidIdentifier(entity, 'table name');
339
+ const mysql = await import('mysql2/promise');
340
+ const conn = await mysql.createConnection({ uri: url, connectTimeout: 8000 });
341
+ try {
342
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
343
+ const [pkRows] = (await conn.query(`SELECT COLUMN_NAME
344
+ FROM information_schema.KEY_COLUMN_USAGE
345
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = ? AND CONSTRAINT_NAME = 'PRIMARY'
346
+ LIMIT 1`, [entity]));
347
+ const pkCol = pkRows[0]?.COLUMN_NAME ?? 'id';
348
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
349
+ const [res] = (await conn.query(`DELETE FROM \`${entity.replace(/`/g, '``')}\` WHERE \`${pkCol.replace(/`/g, '``')}\` = ?`, [primaryKey]));
350
+ if (res.affectedRows === 0) {
351
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
352
+ }
353
+ return { success: true, deletedCount: res.affectedRows };
354
+ }
355
+ finally {
356
+ await conn.end().catch(() => undefined);
357
+ }
358
+ }
359
+ // ─── SQLite Driver (node:sqlite) ──────────────────────────────────────────────
360
+ async function sqliteFetch(url, entity, options) {
361
+ assertValidIdentifier(entity, 'table name');
362
+ const { DatabaseSync } = await import('node:sqlite');
363
+ const filePath = sqlitePathFromUrl(url);
364
+ const db = filePath === ':memory:' ? new DatabaseSync(':memory:') : new DatabaseSync(filePath, { readOnly: true });
365
+ try {
366
+ const ident = `"${entity.replace(/"/g, '""')}"`;
367
+ const cols = db.prepare(`PRAGMA table_info(${ident})`).all();
368
+ const pkCol = cols.find((c) => Number(c.pk) > 0)?.name ?? 'id';
369
+ const allCols = cols.map((c) => c.name);
370
+ const search = options.search?.trim();
371
+ let whereClause = '';
372
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
+ const params = [];
374
+ if (search && allCols.length > 0) {
375
+ const conditions = allCols.map((c) => `"${c.replace(/"/g, '""')}" LIKE ?`);
376
+ whereClause = ` WHERE (${conditions.join(' OR ')})`;
377
+ for (let i = 0; i < allCols.length; i++)
378
+ params.push(`%${search}%`);
379
+ }
380
+ const countRow = db.prepare(`SELECT COUNT(*) AS total FROM ${ident}${whereClause}`).get(...params);
381
+ const totalCount = Number(countRow?.total ?? 0);
382
+ const limit = Math.max(1, Math.min(100, options.limit ?? 25));
383
+ const page = Math.max(1, options.page ?? 1);
384
+ const offset = (page - 1) * limit;
385
+ const totalPages = Math.ceil(totalCount / limit) || 1;
386
+ let sortCol = pkCol;
387
+ if (options.sort && allCols.includes(options.sort)) {
388
+ sortCol = options.sort;
389
+ }
390
+ const order = options.order === 'desc' ? 'DESC' : 'ASC';
391
+ const dataRows = db
392
+ .prepare(`SELECT * FROM ${ident}${whereClause} ORDER BY "${sortCol.replace(/"/g, '""')}" ${order} LIMIT ? OFFSET ?`)
393
+ .all(...params, limit, offset);
394
+ return {
395
+ entity,
396
+ entityType: 'table',
397
+ primaryKey: pkCol,
398
+ page,
399
+ limit,
400
+ totalCount,
401
+ totalPages,
402
+ rows: dataRows.map(serializeRow),
403
+ };
404
+ }
405
+ finally {
406
+ db.close();
407
+ }
408
+ }
409
+ async function sqliteInsert(url, entity, record) {
410
+ assertValidIdentifier(entity, 'table name');
411
+ const { DatabaseSync } = await import('node:sqlite');
412
+ const filePath = sqlitePathFromUrl(url);
413
+ const db = filePath === ':memory:' ? new DatabaseSync(':memory:') : new DatabaseSync(filePath);
414
+ try {
415
+ const ident = `"${entity.replace(/"/g, '""')}"`;
416
+ const keys = Object.keys(record);
417
+ for (const k of keys)
418
+ assertValidIdentifier(k, 'column name');
419
+ let insertedId = 1;
420
+ let insertedRow = record;
421
+ if (keys.length === 0) {
422
+ const res = db.prepare(`INSERT INTO ${ident} DEFAULT VALUES RETURNING *`).get();
423
+ if (res) {
424
+ insertedRow = res;
425
+ insertedId = res.id ?? 1;
426
+ }
427
+ }
428
+ else {
429
+ const cols = keys.map((k) => `"${k.replace(/"/g, '""')}"`).join(', ');
430
+ const placeholders = keys.map(() => '?').join(', ');
431
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
432
+ const values = keys.map((k) => record[k]);
433
+ try {
434
+ const res = db.prepare(`INSERT INTO ${ident} (${cols}) VALUES (${placeholders}) RETURNING *`).get(...values);
435
+ if (res) {
436
+ insertedRow = res;
437
+ insertedId = res.id ?? 1;
438
+ }
439
+ }
440
+ catch {
441
+ // Fallback for older SQLite versions without RETURNING
442
+ const info = db.prepare(`INSERT INTO ${ident} (${cols}) VALUES (${placeholders})`).run(...values);
443
+ insertedId = Number(info.lastInsertRowid ?? 1);
444
+ insertedRow = { ...record, id: insertedId };
445
+ }
446
+ }
447
+ return { success: true, insertedId, record: serializeRow(insertedRow) };
448
+ }
449
+ finally {
450
+ db.close();
451
+ }
452
+ }
453
+ async function sqliteUpdate(url, entity, primaryKey, updates) {
454
+ assertValidIdentifier(entity, 'table name');
455
+ const { DatabaseSync } = await import('node:sqlite');
456
+ const filePath = sqlitePathFromUrl(url);
457
+ const db = filePath === ':memory:' ? new DatabaseSync(':memory:') : new DatabaseSync(filePath);
458
+ try {
459
+ const ident = `"${entity.replace(/"/g, '""')}"`;
460
+ const cols = db.prepare(`PRAGMA table_info(${ident})`).all();
461
+ const pkCol = cols.find((c) => Number(c.pk) > 0)?.name ?? 'id';
462
+ const keys = Object.keys(updates).filter((k) => k !== pkCol);
463
+ if (keys.length === 0)
464
+ throw new Error('No updatable columns provided');
465
+ for (const k of keys)
466
+ assertValidIdentifier(k, 'column name');
467
+ const setClause = keys.map((k) => `"${k.replace(/"/g, '""')}" = ?`).join(', ');
468
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
469
+ const values = [...keys.map((k) => updates[k]), primaryKey];
470
+ const stmt = db.prepare(`UPDATE ${ident} SET ${setClause} WHERE "${pkCol.replace(/"/g, '""')}" = ?`);
471
+ const info = stmt.run(...values);
472
+ const changes = Number(info.changes ?? 0);
473
+ if (changes === 0) {
474
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
475
+ }
476
+ const updatedRow = db.prepare(`SELECT * FROM ${ident} WHERE "${pkCol.replace(/"/g, '""')}" = ? LIMIT 1`).get(primaryKey);
477
+ return { success: true, updatedCount: changes, record: serializeRow(updatedRow || updates) };
478
+ }
479
+ finally {
480
+ db.close();
481
+ }
482
+ }
483
+ async function sqliteDelete(url, entity, primaryKey) {
484
+ assertValidIdentifier(entity, 'table name');
485
+ const { DatabaseSync } = await import('node:sqlite');
486
+ const filePath = sqlitePathFromUrl(url);
487
+ const db = filePath === ':memory:' ? new DatabaseSync(':memory:') : new DatabaseSync(filePath);
488
+ try {
489
+ const ident = `"${entity.replace(/"/g, '""')}"`;
490
+ const cols = db.prepare(`PRAGMA table_info(${ident})`).all();
491
+ const pkCol = cols.find((c) => Number(c.pk) > 0)?.name ?? 'id';
492
+ const stmt = db.prepare(`DELETE FROM ${ident} WHERE "${pkCol.replace(/"/g, '""')}" = ?`);
493
+ const info = stmt.run(primaryKey);
494
+ const changes = Number(info.changes ?? 0);
495
+ if (changes === 0) {
496
+ throw new Error(`Record with primary key "${primaryKey}" was not found in ${entity}`);
497
+ }
498
+ return { success: true, deletedCount: changes };
499
+ }
500
+ finally {
501
+ db.close();
502
+ }
503
+ }
504
+ // ─── MongoDB Driver ────────────────────────────────────────────────────────────
505
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
506
+ function resolveMongoId(id, ObjectIdClass) {
507
+ if (ObjectIdClass && typeof id === 'string' && ObjectIdClass.isValid(id)) {
508
+ try {
509
+ return new ObjectIdClass(id);
510
+ }
511
+ catch {
512
+ // fallback to raw id
513
+ }
514
+ }
515
+ return id;
516
+ }
517
+ async function mongoFetch(url, entity, options) {
518
+ assertValidIdentifier(entity, 'collection name');
519
+ const { MongoClient } = await import('mongodb');
520
+ const client = new MongoClient(url, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000, appName: 'nativ-data' });
521
+ await client.connect();
522
+ try {
523
+ const db = client.db();
524
+ const col = db.collection(entity);
525
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
526
+ const filter = {};
527
+ const search = options.search?.trim();
528
+ if (search) {
529
+ // Find sampled docs to get string keys for search
530
+ const sample = await col.find({}).limit(10).toArray();
531
+ const keys = new Set();
532
+ for (const d of sample) {
533
+ for (const [k, v] of Object.entries(d)) {
534
+ if (typeof v === 'string')
535
+ keys.add(k);
536
+ }
537
+ }
538
+ if (keys.size > 0) {
539
+ filter.$or = [...keys].map((k) => ({ [k]: { $regex: search, $options: 'i' } }));
540
+ }
541
+ }
542
+ const totalCount = await col.countDocuments(filter);
543
+ const limit = Math.max(1, Math.min(100, options.limit ?? 25));
544
+ const page = Math.max(1, options.page ?? 1);
545
+ const offset = (page - 1) * limit;
546
+ const totalPages = Math.ceil(totalCount / limit) || 1;
547
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
548
+ const sortObj = {};
549
+ const sortCol = options.sort || '_id';
550
+ sortObj[sortCol] = options.order === 'desc' ? -1 : 1;
551
+ const docs = await col.find(filter).sort(sortObj).skip(offset).limit(limit).toArray();
552
+ return {
553
+ entity,
554
+ entityType: 'collection',
555
+ primaryKey: '_id',
556
+ page,
557
+ limit,
558
+ totalCount,
559
+ totalPages,
560
+ rows: docs.map((d) => serializeRow(d)),
561
+ };
562
+ }
563
+ finally {
564
+ await client.close().catch(() => undefined);
565
+ }
566
+ }
567
+ async function mongoInsert(url, entity, record) {
568
+ assertValidIdentifier(entity, 'collection name');
569
+ const { MongoClient } = await import('mongodb');
570
+ const client = new MongoClient(url, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000, appName: 'nativ-data' });
571
+ await client.connect();
572
+ try {
573
+ const db = client.db();
574
+ const col = db.collection(entity);
575
+ const res = await col.insertOne(record);
576
+ const insertedId = String(res.insertedId);
577
+ return { success: true, insertedId, record: serializeRow({ ...record, _id: res.insertedId }) };
578
+ }
579
+ finally {
580
+ await client.close().catch(() => undefined);
581
+ }
582
+ }
583
+ async function mongoUpdate(url, entity, primaryKey, updates) {
584
+ assertValidIdentifier(entity, 'collection name');
585
+ const { MongoClient, ObjectId } = await import('mongodb');
586
+ const client = new MongoClient(url, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000, appName: 'nativ-data' });
587
+ await client.connect();
588
+ try {
589
+ const db = client.db();
590
+ const col = db.collection(entity);
591
+ const filterId = resolveMongoId(primaryKey, ObjectId);
592
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
593
+ const { _id, id, ...cleanUpdates } = updates;
594
+ if (Object.keys(cleanUpdates).length === 0)
595
+ throw new Error('No updatable fields provided');
596
+ const res = await col.updateOne({ _id: filterId }, { $set: cleanUpdates });
597
+ if (res.matchedCount === 0) {
598
+ throw new Error(`Document with _id "${primaryKey}" was not found in collection ${entity}`);
599
+ }
600
+ const updatedDoc = await col.findOne({ _id: filterId });
601
+ return { success: true, updatedCount: res.modifiedCount, record: serializeRow(updatedDoc || cleanUpdates) };
602
+ }
603
+ finally {
604
+ await client.close().catch(() => undefined);
605
+ }
606
+ }
607
+ async function mongoDelete(url, entity, primaryKey) {
608
+ assertValidIdentifier(entity, 'collection name');
609
+ const { MongoClient, ObjectId } = await import('mongodb');
610
+ const client = new MongoClient(url, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000, appName: 'nativ-data' });
611
+ await client.connect();
612
+ try {
613
+ const db = client.db();
614
+ const col = db.collection(entity);
615
+ const filterId = resolveMongoId(primaryKey, ObjectId);
616
+ const res = await col.deleteOne({ _id: filterId });
617
+ if (res.deletedCount === 0) {
618
+ throw new Error(`Document with _id "${primaryKey}" was not found in collection ${entity}`);
619
+ }
620
+ return { success: true, deletedCount: res.deletedCount };
621
+ }
622
+ finally {
623
+ await client.close().catch(() => undefined);
624
+ }
625
+ }
626
+ // ─── Firestore Driver ─────────────────────────────────────────────────────────
627
+ async function firestoreFetch(url, entity, options) {
628
+ assertValidIdentifier(entity, 'collection name');
629
+ const config = parseFirestoreUrl(url);
630
+ const { Firestore } = await import('@google-cloud/firestore');
631
+ const firestore = new Firestore({
632
+ projectId: config.projectId,
633
+ ...(config.databaseId ? { databaseId: config.databaseId } : {}),
634
+ ...(config.emulatorHost ? { host: config.emulatorHost, ssl: false } : {}),
635
+ });
636
+ const col = firestore.collection(entity);
637
+ const countSnap = await col.count().get().catch(() => null);
638
+ const totalCount = countSnap ? countSnap.data().count : 0;
639
+ const limit = Math.max(1, Math.min(100, options.limit ?? 25));
640
+ const page = Math.max(1, options.page ?? 1);
641
+ const offset = (page - 1) * limit;
642
+ const totalPages = Math.ceil(totalCount / limit) || 1;
643
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
644
+ let query = col;
645
+ if (options.sort && options.sort !== 'id') {
646
+ query = query.orderBy(options.sort, options.order === 'desc' ? 'desc' : 'asc');
647
+ }
648
+ if (offset > 0) {
649
+ query = query.offset(offset);
650
+ }
651
+ query = query.limit(limit);
652
+ const snapshot = await query.get();
653
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
654
+ const rows = snapshot.docs.map((d) => serializeRow({ id: d.id, ...d.data() }));
655
+ return {
656
+ entity,
657
+ entityType: 'collection',
658
+ primaryKey: 'id',
659
+ page,
660
+ limit,
661
+ totalCount,
662
+ totalPages,
663
+ rows,
664
+ };
665
+ }
666
+ async function firestoreInsert(url, entity, record) {
667
+ assertValidIdentifier(entity, 'collection name');
668
+ const config = parseFirestoreUrl(url);
669
+ const { Firestore } = await import('@google-cloud/firestore');
670
+ const firestore = new Firestore({
671
+ projectId: config.projectId,
672
+ ...(config.databaseId ? { databaseId: config.databaseId } : {}),
673
+ ...(config.emulatorHost ? { host: config.emulatorHost, ssl: false } : {}),
674
+ });
675
+ const col = firestore.collection(entity);
676
+ const { id, ...data } = record;
677
+ const docRef = id ? col.doc(String(id)) : col.doc();
678
+ await docRef.set(data);
679
+ return { success: true, insertedId: docRef.id, record: serializeRow({ id: docRef.id, ...data }) };
680
+ }
681
+ async function firestoreUpdate(url, entity, primaryKey, updates) {
682
+ assertValidIdentifier(entity, 'collection name');
683
+ const config = parseFirestoreUrl(url);
684
+ const { Firestore } = await import('@google-cloud/firestore');
685
+ const firestore = new Firestore({
686
+ projectId: config.projectId,
687
+ ...(config.databaseId ? { databaseId: config.databaseId } : {}),
688
+ ...(config.emulatorHost ? { host: config.emulatorHost, ssl: false } : {}),
689
+ });
690
+ const col = firestore.collection(entity);
691
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
692
+ const { id, ...data } = updates;
693
+ if (Object.keys(data).length === 0)
694
+ throw new Error('No updatable fields provided');
695
+ const docRef = col.doc(String(primaryKey));
696
+ await docRef.update(data);
697
+ const updatedSnap = await docRef.get();
698
+ return { success: true, updatedCount: 1, record: serializeRow({ id: docRef.id, ...updatedSnap.data() }) };
699
+ }
700
+ async function firestoreDelete(url, entity, primaryKey) {
701
+ assertValidIdentifier(entity, 'collection name');
702
+ const config = parseFirestoreUrl(url);
703
+ const { Firestore } = await import('@google-cloud/firestore');
704
+ const firestore = new Firestore({
705
+ projectId: config.projectId,
706
+ ...(config.databaseId ? { databaseId: config.databaseId } : {}),
707
+ ...(config.emulatorHost ? { host: config.emulatorHost, ssl: false } : {}),
708
+ });
709
+ const col = firestore.collection(entity);
710
+ const docRef = col.doc(String(primaryKey));
711
+ await docRef.delete();
712
+ return { success: true, deletedCount: 1 };
713
+ }
714
+ // ─── Unified Data Engine Facade ───────────────────────────────────────────────
715
+ export async function fetchData(url, entity, options = {}) {
716
+ const engine = detectEngine(url);
717
+ try {
718
+ switch (engine) {
719
+ case 'postgresql':
720
+ return await postgresFetch(url, entity, options);
721
+ case 'mysql':
722
+ return await mysqlFetch(url, entity, options);
723
+ case 'sqlite':
724
+ return await sqliteFetch(url, entity, options);
725
+ case 'mongodb':
726
+ return await mongoFetch(url, entity, options);
727
+ case 'firestore':
728
+ return await firestoreFetch(url, entity, options);
729
+ default:
730
+ throw new Error(`Unsupported database engine for live data queries: "${engine}"`);
731
+ }
732
+ }
733
+ catch (err) {
734
+ throw sanitizeError(err, url);
735
+ }
736
+ }
737
+ export async function insertRecord(url, entity, record) {
738
+ const engine = detectEngine(url);
739
+ try {
740
+ switch (engine) {
741
+ case 'postgresql':
742
+ return await postgresInsert(url, entity, record);
743
+ case 'mysql':
744
+ return await mysqlInsert(url, entity, record);
745
+ case 'sqlite':
746
+ return await sqliteInsert(url, entity, record);
747
+ case 'mongodb':
748
+ return await mongoInsert(url, entity, record);
749
+ case 'firestore':
750
+ return await firestoreInsert(url, entity, record);
751
+ default:
752
+ throw new Error(`Unsupported database engine for insertions: "${engine}"`);
753
+ }
754
+ }
755
+ catch (err) {
756
+ throw sanitizeError(err, url);
757
+ }
758
+ }
759
+ export async function updateRecord(url, entity, primaryKey, updates) {
760
+ const engine = detectEngine(url);
761
+ try {
762
+ switch (engine) {
763
+ case 'postgresql':
764
+ return await postgresUpdate(url, entity, primaryKey, updates);
765
+ case 'mysql':
766
+ return await mysqlUpdate(url, entity, primaryKey, updates);
767
+ case 'sqlite':
768
+ return await sqliteUpdate(url, entity, primaryKey, updates);
769
+ case 'mongodb':
770
+ return await mongoUpdate(url, entity, primaryKey, updates);
771
+ case 'firestore':
772
+ return await firestoreUpdate(url, entity, primaryKey, updates);
773
+ default:
774
+ throw new Error(`Unsupported database engine for updates: "${engine}"`);
775
+ }
776
+ }
777
+ catch (err) {
778
+ throw sanitizeError(err, url);
779
+ }
780
+ }
781
+ export async function deleteRecord(url, entity, primaryKey) {
782
+ const engine = detectEngine(url);
783
+ try {
784
+ switch (engine) {
785
+ case 'postgresql':
786
+ return await postgresDelete(url, entity, primaryKey);
787
+ case 'mysql':
788
+ return await mysqlDelete(url, entity, primaryKey);
789
+ case 'sqlite':
790
+ return await sqliteDelete(url, entity, primaryKey);
791
+ case 'mongodb':
792
+ return await mongoDelete(url, entity, primaryKey);
793
+ case 'firestore':
794
+ return await firestoreDelete(url, entity, primaryKey);
795
+ default:
796
+ throw new Error(`Unsupported database engine for deletions: "${engine}"`);
797
+ }
798
+ }
799
+ catch (err) {
800
+ throw sanitizeError(err, url);
801
+ }
802
+ }