tina4-nodejs 3.13.133 → 3.13.134
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -3
- package/README.md +2 -2
- package/package.json +1 -1
- package/packages/cli/dist/bin.js +3181 -3051
- package/packages/cli/src/commands/generate.ts +33 -22
- package/packages/cli/src/commands/lint.ts +77 -111
- package/packages/core/dist/index.js +3090 -2952
- package/packages/core/src/.tina4-metrics.json +15004 -0
- package/packages/core/src/aiClient.ts +199 -161
- package/packages/core/src/dispatchPipeline.ts +65 -67
- package/packages/core/src/docs.ts +52 -544
- package/packages/core/src/docsParser.ts +270 -0
- package/packages/core/src/docsScanner.ts +121 -0
- package/packages/core/src/docsSignatures.ts +165 -0
- package/packages/core/src/index.ts +2 -0
- package/packages/core/src/logger.ts +68 -82
- package/packages/core/src/mcp.ts +32 -60
- package/packages/core/src/messenger.ts +136 -157
- package/packages/core/src/middleware.ts +56 -60
- package/packages/core/src/plan.ts +78 -70
- package/packages/core/src/projectIndex.ts +15 -288
- package/packages/core/src/projectIndexExtractors.ts +126 -0
- package/packages/core/src/projectIndexStorage.ts +122 -0
- package/packages/core/src/push.ts +281 -0
- package/packages/core/src/server.ts +182 -183
- package/packages/frond/dist/index.js +607 -770
- package/packages/frond/src/engine.ts +670 -818
- package/packages/orm/dist/index.js +3100 -2965
- package/packages/orm/src/adapters/mongodb.ts +99 -144
- package/packages/orm/src/baseModel.ts +429 -515
- package/packages/orm/src/fakeData.ts +73 -61
- package/packages/orm/src/migration.ts +96 -126
- package/packages/orm/src/seeder.ts +6 -238
- package/packages/orm/src/seederTable.ts +101 -0
- package/packages/orm/src/seederTypes.ts +14 -0
- package/packages/orm/src/validation.ts +97 -80
- package/types/core/src/aiClient.d.ts +5 -0
- package/types/core/src/docsParser.d.ts +28 -0
- package/types/core/src/docsScanner.d.ts +1 -0
- package/types/core/src/docsSignatures.d.ts +11 -0
- package/types/core/src/index.d.ts +2 -0
- package/types/core/src/messenger.d.ts +8 -0
- package/types/core/src/projectIndexExtractors.d.ts +3 -0
- package/types/core/src/projectIndexStorage.d.ts +13 -0
- package/types/core/src/push.d.ts +45 -0
- package/types/frond/src/engine.d.ts +25 -0
- package/types/orm/src/fakeData.d.ts +3 -0
- package/types/orm/src/seeder.d.ts +3 -89
- package/types/orm/src/seederTable.d.ts +9 -0
- package/types/orm/src/seederTypes.d.ts +16 -0
|
@@ -183,158 +183,113 @@ function requireWriteFilter(filter: Record<string, unknown> | undefined, operati
|
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const [, cols, collection, whereClause, orderBy, limitStr, skipStr] = selectMatch;
|
|
196
|
-
|
|
197
|
-
// Projection
|
|
198
|
-
let projection: Record<string, unknown> | undefined;
|
|
199
|
-
if (cols && cols.trim() !== "*") {
|
|
200
|
-
projection = {};
|
|
201
|
-
for (const col of cols.split(",")) {
|
|
202
|
-
const name = col.trim().replace(/^["']|["']$/g, "");
|
|
203
|
-
if (name && name !== "*") projection[name] = 1;
|
|
204
|
-
}
|
|
186
|
+
function parseSelect(sql: string, params: unknown[]): MongoOperation | null {
|
|
187
|
+
const match = sql.match(/^SELECT\s+(.*?)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.*?))?(?:\s+ORDER\s+BY\s+(.*?))?(?:\s+LIMIT\s+(\d+))?(?:\s+OFFSET\s+(\d+))?$/is);
|
|
188
|
+
if (!match) return null;
|
|
189
|
+
const [, cols, collection, whereClause, orderBy, limitStr, skipStr] = match;
|
|
190
|
+
const projection: Record<string, unknown> = {};
|
|
191
|
+
if (cols && cols.trim() !== "*") {
|
|
192
|
+
for (const col of cols.split(",")) {
|
|
193
|
+
const name = col.trim().replace(/^["']|["']$/g, "");
|
|
194
|
+
if (name && name !== "*") projection[name] = 1;
|
|
205
195
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
196
|
+
}
|
|
197
|
+
const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
|
|
198
|
+
const sort: Record<string, 1 | -1> = {};
|
|
199
|
+
if (orderBy) {
|
|
200
|
+
for (const part of orderBy.split(",")) {
|
|
201
|
+
const trimmed = part.trim();
|
|
202
|
+
const desc = /DESC$/i.test(trimmed);
|
|
203
|
+
const col = trimmed.replace(/\s+(ASC|DESC)$/i, "").replace(/^["']|["']$/g, "").trim();
|
|
204
|
+
sort[col] = desc ? -1 : 1;
|
|
212
205
|
}
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
type: "find",
|
|
209
|
+
collection,
|
|
210
|
+
filter,
|
|
211
|
+
projection: Object.keys(projection).length > 0 ? projection : undefined,
|
|
212
|
+
limit: limitStr ? parseInt(limitStr, 10) : undefined,
|
|
213
|
+
skip: skipStr ? parseInt(skipStr, 10) : undefined,
|
|
214
|
+
sort: orderBy ? sort : undefined,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
213
217
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
218
|
+
function parseInsert(sql: string, params: unknown[]): MongoOperation | null {
|
|
219
|
+
const match = sql.match(/^INSERT\s+INTO\s+["']?(\w+)["']?\s*\(([^)]+)\)\s*VALUES\s*\(([^)]+)\)$/is);
|
|
220
|
+
if (!match) return null;
|
|
221
|
+
const [, collection, colsStr, valsStr] = match;
|
|
222
|
+
const cols = colsStr.split(",").map((column) => column.trim().replace(/^["']|["']$/g, ""));
|
|
223
|
+
const values = valsStr.split(",").map((value) => value.trim());
|
|
224
|
+
const document: Record<string, unknown> = {};
|
|
225
|
+
let paramIndex = 0;
|
|
226
|
+
for (let index = 0; index < cols.length; index++) {
|
|
227
|
+
const value = values[index];
|
|
228
|
+
if (value === "?") document[cols[index]] = params[paramIndex++];
|
|
229
|
+
else if (/^'.*'$/.test(value)) document[cols[index]] = value.slice(1, -1);
|
|
230
|
+
else if (/^-?\d+(\.\d+)?$/.test(value)) document[cols[index]] = Number(value);
|
|
231
|
+
else document[cols[index]] = value;
|
|
232
|
+
}
|
|
233
|
+
return { type: "insertOne", collection, document };
|
|
234
|
+
}
|
|
225
235
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
236
|
+
function parseSetClause(setClause: string, params: unknown[]): { document: Record<string, unknown>; consumed: number } {
|
|
237
|
+
const document: Record<string, unknown> = {};
|
|
238
|
+
let consumed = 0;
|
|
239
|
+
for (const part of setClause.split(",")) {
|
|
240
|
+
const trimmed = part.trim();
|
|
241
|
+
const parameter = trimmed.match(/^["']?(\w+)["']?\s*=\s*\?$/);
|
|
242
|
+
if (parameter) {
|
|
243
|
+
document[parameter[1]] = params[consumed++];
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const literal = trimmed.match(/^["']?(\w+)["']?\s*=\s*(?:'([^']*)'|(-?\d+(?:\.\d+)?))$/);
|
|
247
|
+
if (literal) document[literal[1]] = literal[2] !== undefined ? literal[2] : Number(literal[3]);
|
|
235
248
|
}
|
|
249
|
+
return { document, consumed };
|
|
250
|
+
}
|
|
236
251
|
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
const doc: Record<string, unknown> = {};
|
|
247
|
-
for (let i = 0; i < cols.length; i++) {
|
|
248
|
-
if (valPlaceholders[i] === "?") {
|
|
249
|
-
doc[cols[i]] = params[paramIndex++];
|
|
250
|
-
} else if (/^'.*'$/.test(valPlaceholders[i])) {
|
|
251
|
-
doc[cols[i]] = valPlaceholders[i].slice(1, -1);
|
|
252
|
-
} else if (/^-?\d+(\.\d+)?$/.test(valPlaceholders[i])) {
|
|
253
|
-
doc[cols[i]] = Number(valPlaceholders[i]);
|
|
254
|
-
} else {
|
|
255
|
-
doc[cols[i]] = valPlaceholders[i];
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return { type: "insertOne", collection, document: doc };
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
// ---- UPDATE ----
|
|
262
|
-
// WHERE is OPTIONAL in the grammar so a filterless UPDATE ("UPDATE t SET x=1")
|
|
263
|
-
// is RECOGNISED as an updateMany with an empty filter and reaches the
|
|
264
|
-
// fail-closed guard -- rather than failing the regex, returning null, and
|
|
265
|
-
// being silently acknowledged as a no-op (a silent wrong result).
|
|
266
|
-
const updateMatch = s.match(
|
|
267
|
-
/^UPDATE\s+["']?(\w+)["']?\s+SET\s+(.*?)(?:\s+WHERE\s+(.+))?$/is,
|
|
268
|
-
);
|
|
269
|
-
if (updateMatch) {
|
|
270
|
-
const [, collection, setClause, whereClause] = updateMatch;
|
|
271
|
-
|
|
272
|
-
// Parse SET clause
|
|
273
|
-
const setDoc: Record<string, unknown> = {};
|
|
274
|
-
let setParamIndex = 0;
|
|
275
|
-
for (const setPart of setClause.split(",")) {
|
|
276
|
-
const m = setPart.trim().match(/^["']?(\w+)["']?\s*=\s*\?$/);
|
|
277
|
-
if (m) {
|
|
278
|
-
setDoc[m[1]] = params[setParamIndex++];
|
|
279
|
-
} else {
|
|
280
|
-
const mLit = setPart.trim().match(/^["']?(\w+)["']?\s*=\s*(?:'([^']*)'|(-?\d+(?:\.\d+)?))$/);
|
|
281
|
-
if (mLit) {
|
|
282
|
-
setDoc[mLit[1]] = mLit[2] !== undefined ? mLit[2] : Number(mLit[3]);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
252
|
+
function parseUpdate(sql: string, params: unknown[]): MongoOperation | null {
|
|
253
|
+
const match = sql.match(/^UPDATE\s+["']?(\w+)["']?\s+SET\s+(.*?)(?:\s+WHERE\s+(.+))?$/is);
|
|
254
|
+
if (!match) return null;
|
|
255
|
+
const [, collection, setClause, whereClause] = match;
|
|
256
|
+
const set = parseSetClause(setClause, params);
|
|
257
|
+
const matchAll = isMatchAllWhere(whereClause?.trim());
|
|
258
|
+
const filter = whereClause ? parseWhereClause(whereClause.trim(), params, set.consumed).filter : {};
|
|
259
|
+
return { type: "updateMany", collection, filter, update: { $set: set.document }, matchAll };
|
|
260
|
+
}
|
|
286
261
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
/^DELETE\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is,
|
|
301
|
-
);
|
|
302
|
-
if (deleteMatch) {
|
|
303
|
-
const [, collection, whereClause] = deleteMatch;
|
|
304
|
-
// An explicit 1=1 tautology (truncate()'s WHERE) is an intentional match-all;
|
|
305
|
-
// a blank/absent WHERE is the mass-delete footgun the executor guard refuses.
|
|
306
|
-
const matchAll = isMatchAllWhere(whereClause?.trim());
|
|
307
|
-
const filter = whereClause
|
|
308
|
-
? parseWhereClause(whereClause.trim(), params).filter
|
|
309
|
-
: {};
|
|
310
|
-
return { type: "deleteMany", collection, filter, matchAll };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
// ---- CREATE TABLE (treated as createCollection) ----
|
|
314
|
-
const createTableMatch = s.match(/^CREATE\s+(?:TABLE|COLLECTION)\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
|
|
315
|
-
if (createTableMatch) {
|
|
316
|
-
// We handle this in createTable(); signal it as a "raw" skip
|
|
317
|
-
return { type: "raw", collection: createTableMatch[1] };
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
// ---- SELECT COUNT(*) ----
|
|
321
|
-
const countMatch = s.match(/^SELECT\s+COUNT\(\*\)\s+AS\s+(\w+)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
|
|
322
|
-
if (countMatch) {
|
|
323
|
-
const [, alias, collection, whereClause] = countMatch;
|
|
324
|
-
const filter = whereClause
|
|
325
|
-
? parseWhereClause(whereClause.trim(), params).filter
|
|
326
|
-
: {};
|
|
327
|
-
return {
|
|
328
|
-
type: "aggregate",
|
|
329
|
-
collection,
|
|
330
|
-
pipeline: [
|
|
331
|
-
{ $match: filter },
|
|
332
|
-
{ $count: alias },
|
|
333
|
-
],
|
|
334
|
-
};
|
|
335
|
-
}
|
|
262
|
+
function parseDelete(sql: string, params: unknown[]): MongoOperation | null {
|
|
263
|
+
const match = sql.match(/^DELETE\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
|
|
264
|
+
if (!match) return null;
|
|
265
|
+
const [, collection, whereClause] = match;
|
|
266
|
+
const matchAll = isMatchAllWhere(whereClause?.trim());
|
|
267
|
+
const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
|
|
268
|
+
return { type: "deleteMany", collection, filter, matchAll };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function parseCreate(sql: string): MongoOperation | null {
|
|
272
|
+
const match = sql.match(/^CREATE\s+(?:TABLE|COLLECTION)\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
|
|
273
|
+
return match ? { type: "raw", collection: match[1] } : null;
|
|
274
|
+
}
|
|
336
275
|
|
|
337
|
-
|
|
276
|
+
function parseCount(sql: string, params: unknown[]): MongoOperation | null {
|
|
277
|
+
const match = sql.match(/^SELECT\s+COUNT\(\*\)\s+AS\s+(\w+)\s+FROM\s+["']?(\w+)["']?(?:\s+WHERE\s+(.+))?$/is);
|
|
278
|
+
if (!match) return null;
|
|
279
|
+
const [, alias, collection, whereClause] = match;
|
|
280
|
+
const filter = whereClause ? parseWhereClause(whereClause.trim(), params).filter : {};
|
|
281
|
+
return { type: "aggregate", collection, pipeline: [{ $match: filter }, { $count: alias }] };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Parse a SQL string into a MongoOperation. Returns null if parsing is not supported. */
|
|
285
|
+
function parseSql(sql: string, params: unknown[] = []): MongoOperation | null {
|
|
286
|
+
const statement = sql.trim();
|
|
287
|
+
return parseSelect(statement, params)
|
|
288
|
+
?? parseInsert(statement, params)
|
|
289
|
+
?? parseUpdate(statement, params)
|
|
290
|
+
?? parseDelete(statement, params)
|
|
291
|
+
?? parseCreate(statement)
|
|
292
|
+
?? parseCount(statement, params);
|
|
338
293
|
}
|
|
339
294
|
|
|
340
295
|
export class MongodbAdapter implements DatabaseAdapter {
|