tina4-nodejs 3.13.43 → 3.13.45
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 +2 -1
- package/package.json +1 -1
- package/packages/core/src/graphql.ts +23 -14
- package/packages/core/src/mcp.ts +21 -2
- package/packages/core/src/queueBackends/kafkaBackend.ts +303 -167
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +97 -31
- package/packages/core/src/server.ts +12 -5
- package/packages/core/src/session.ts +11 -95
- package/packages/core/src/sessionHandlers/mongoClient.ts +238 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +25 -204
- package/packages/core/src/sessionHandlers/redisHandler.ts +69 -114
- package/packages/core/src/sessionHandlers/respClient.ts +171 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +11 -95
- package/packages/orm/src/adapters/firebird.ts +20 -2
- package/packages/orm/src/adapters/mssql.ts +24 -2
- package/packages/orm/src/adapters/mysql.ts +20 -2
- package/packages/orm/src/adapters/postgres.ts +40 -12
- package/packages/orm/src/adapters/sqlite.ts +16 -2
- package/packages/orm/src/autoCrud.ts +13 -0
- package/packages/orm/src/baseModel.ts +3 -1
- package/packages/orm/src/cachedDatabase.ts +1 -1
- package/packages/orm/src/database.ts +42 -11
- package/packages/orm/src/index.ts +1 -1
- package/packages/orm/src/migration.ts +124 -45
- package/packages/orm/src/types.ts +5 -3
package/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.
|
|
1
|
+
# CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.45)
|
|
2
2
|
|
|
3
3
|
> This file helps AI assistants (Claude, Copilot, Cursor, etc.) understand and work on this codebase effectively.
|
|
4
4
|
|
|
@@ -1239,6 +1239,7 @@ When adding new features, add a corresponding `test/<feature>.test.ts` file.
|
|
|
1239
1239
|
- **Don't break the test files** — run `npm test` before committing
|
|
1240
1240
|
- **Don't add unnecessary dependencies** — minimal footprint is a core principle
|
|
1241
1241
|
- **Parity across all frameworks** — Every new feature, fix, or optimization must be implemented with equivalent logic AND tests in all 4 Tina4 frameworks (Python, PHP, Ruby, Node.js). Never ship to one without shipping to all.
|
|
1242
|
+
- **NO mock testing. Mocks are not acceptable in any circumstances.** A test double (mock, stub, fake, spy, monkeypatch, script-introspection assertion, or any in-test object standing in for a real collaborator) may never substitute for a real dependency, under any justification. There is no "supplement" exception and no "hard to reproduce" exception. Any test that touches a dependency (a DB engine, MongoDB, Redis/Valkey/Memcached, RabbitMQ/Kafka, an HTTP/SMTP service, the filesystem, a socket) must exercise the REAL service; if a failure mode is hard to trigger, reproduce it for real, never simulate it. "Verified"/"green" requires a real run; a passing mock test is not verification. CI provisions the services; use them and add any that is missing. The only tests that need no live dependency are pure functions with no dependency and no double; that is not a mock test. (The MongoDB queue re-delivered every completed job for two releases because its queue tests were mock/script-introspection-based and never ran against a real Mongo.)
|
|
1242
1243
|
- **Don't use `url.parse()`** — use the WHATWG `URL` constructor instead (deprecated in Node 20+)
|
|
1243
1244
|
|
|
1244
1245
|
## Tina4 Maintainer Skill
|
package/package.json
CHANGED
|
@@ -510,8 +510,13 @@ export class GraphQL {
|
|
|
510
510
|
/**
|
|
511
511
|
* Decorator-style resolver registration.
|
|
512
512
|
*
|
|
513
|
+
* Resolvers may be synchronous OR async (return a Promise) — `execute()`
|
|
514
|
+
* awaits every resolver, so an `async` resolver's value is resolved before
|
|
515
|
+
* the field is serialized. Return a value directly, or `await` your data
|
|
516
|
+
* (e.g. an async DB driver) and the executor will await it for you.
|
|
517
|
+
*
|
|
513
518
|
* GraphQL.resolve("Query", "products", async (root, args) =>
|
|
514
|
-
* db.
|
|
519
|
+
* (await db.fetch("SELECT * FROM products")).records);
|
|
515
520
|
*
|
|
516
521
|
* GraphQL.resolve("Mutation", "createProduct", async (root, args) => {
|
|
517
522
|
* const p = new Product(args.input);
|
|
@@ -520,7 +525,7 @@ export class GraphQL {
|
|
|
520
525
|
* });
|
|
521
526
|
*
|
|
522
527
|
* GraphQL.resolve("Product", "reviews", async (product, args) =>
|
|
523
|
-
* db.
|
|
528
|
+
* (await db.fetch("SELECT * FROM reviews WHERE product_id = ?", [product.id])).records);
|
|
524
529
|
*
|
|
525
530
|
* Resolvers registered before any GraphQL instance exists accumulate
|
|
526
531
|
* in the class-level registry. `new GraphQL()` drains them into its
|
|
@@ -647,7 +652,7 @@ export class GraphQL {
|
|
|
647
652
|
/**
|
|
648
653
|
* Execute a GraphQL query string.
|
|
649
654
|
*/
|
|
650
|
-
execute(query: string, variables?: Record<string, unknown>, context?: Record<string, unknown>): GraphQLResult {
|
|
655
|
+
async execute(query: string, variables?: Record<string, unknown>, context?: Record<string, unknown>): Promise<GraphQLResult> {
|
|
651
656
|
const vars = variables ?? {};
|
|
652
657
|
const ctx = context ?? {};
|
|
653
658
|
const errors: Array<{ message: string; path?: string[] }> = [];
|
|
@@ -690,7 +695,7 @@ export class GraphQL {
|
|
|
690
695
|
|
|
691
696
|
const data: Record<string, unknown> = {};
|
|
692
697
|
// Top-level selections start at depth 1.
|
|
693
|
-
const errs = this.resolveSelectionsInto(op.selections, resolvers, null, vars, ctx, fragments, data, 1);
|
|
698
|
+
const errs = await this.resolveSelectionsInto(op.selections, resolvers, null, vars, ctx, fragments, data, 1);
|
|
694
699
|
errors.push(...errs);
|
|
695
700
|
|
|
696
701
|
const result: GraphQLResult = { data };
|
|
@@ -710,7 +715,7 @@ export class GraphQL {
|
|
|
710
715
|
* instead of recursing until the interpreter stack overflows. Top-level
|
|
711
716
|
* starts at depth 1; `maxDepth <= 0` disables the guard.
|
|
712
717
|
*/
|
|
713
|
-
private resolveSelectionsInto(
|
|
718
|
+
private async resolveSelectionsInto(
|
|
714
719
|
selections: ParsedSelection[],
|
|
715
720
|
resolvers: Map<string, QueryConfig>,
|
|
716
721
|
parent: unknown,
|
|
@@ -719,7 +724,7 @@ export class GraphQL {
|
|
|
719
724
|
fragments: Map<string, ParsedFragment>,
|
|
720
725
|
target: Record<string, unknown>,
|
|
721
726
|
depth: number,
|
|
722
|
-
): Array<{ message: string; path?: string[] }
|
|
727
|
+
): Promise<Array<{ message: string; path?: string[] }>> {
|
|
723
728
|
const errors: Array<{ message: string; path?: string[] }> = [];
|
|
724
729
|
|
|
725
730
|
if (this.maxDepth > 0 && depth > this.maxDepth) {
|
|
@@ -736,7 +741,7 @@ export class GraphQL {
|
|
|
736
741
|
errors.push({ message: `Fragment not found: ${sel.name}` });
|
|
737
742
|
continue;
|
|
738
743
|
}
|
|
739
|
-
const errs = this.resolveSelectionsInto(
|
|
744
|
+
const errs = await this.resolveSelectionsInto(
|
|
740
745
|
frag.selections, resolvers, parent, variables, context, fragments, target, depth + 1,
|
|
741
746
|
);
|
|
742
747
|
errors.push(...errs);
|
|
@@ -744,14 +749,14 @@ export class GraphQL {
|
|
|
744
749
|
}
|
|
745
750
|
|
|
746
751
|
if (sel.kind === "inline_fragment") {
|
|
747
|
-
const errs = this.resolveSelectionsInto(
|
|
752
|
+
const errs = await this.resolveSelectionsInto(
|
|
748
753
|
sel.selections, resolvers, parent, variables, context, fragments, target, depth + 1,
|
|
749
754
|
);
|
|
750
755
|
errors.push(...errs);
|
|
751
756
|
continue;
|
|
752
757
|
}
|
|
753
758
|
|
|
754
|
-
const [value, errs] = this.resolveField(sel, resolvers, parent, variables, context, fragments, depth);
|
|
759
|
+
const [value, errs] = await this.resolveField(sel, resolvers, parent, variables, context, fragments, depth);
|
|
755
760
|
errors.push(...errs);
|
|
756
761
|
const key = sel.alias ?? sel.name;
|
|
757
762
|
target[key] = value;
|
|
@@ -1010,7 +1015,7 @@ export class GraphQL {
|
|
|
1010
1015
|
return `(${parts.join(", ")})`;
|
|
1011
1016
|
}
|
|
1012
1017
|
|
|
1013
|
-
private resolveField(
|
|
1018
|
+
private async resolveField(
|
|
1014
1019
|
sel: ParsedField,
|
|
1015
1020
|
resolvers: Map<string, QueryConfig>,
|
|
1016
1021
|
parent: unknown,
|
|
@@ -1018,7 +1023,7 @@ export class GraphQL {
|
|
|
1018
1023
|
context: Record<string, unknown> = {},
|
|
1019
1024
|
fragments: Map<string, ParsedFragment> = new Map(),
|
|
1020
1025
|
depth: number = 1,
|
|
1021
|
-
): [unknown, Array<{ message: string; path?: string[] }>] {
|
|
1026
|
+
): Promise<[unknown, Array<{ message: string; path?: string[] }>]> {
|
|
1022
1027
|
const errors: Array<{ message: string; path?: string[] }> = [];
|
|
1023
1028
|
const name = sel.name;
|
|
1024
1029
|
const args = this.resolveArgs(sel.args, variables);
|
|
@@ -1046,7 +1051,11 @@ export class GraphQL {
|
|
|
1046
1051
|
// Inject sub-selections into context for DataLoader/eager-loading
|
|
1047
1052
|
const ctx = { ...context, __selections: sel.selections ?? [] };
|
|
1048
1053
|
try {
|
|
1049
|
-
|
|
1054
|
+
// Resolvers may be sync or async. Awaiting a plain value is a no-op;
|
|
1055
|
+
// awaiting a Promise (async resolver) resolves it before serialization.
|
|
1056
|
+
// A rejected Promise throws here, into the same catch below, so async
|
|
1057
|
+
// resolver errors are masked/detailed exactly like sync ones.
|
|
1058
|
+
value = await config.resolver(null, args, ctx);
|
|
1050
1059
|
} catch (e: unknown) {
|
|
1051
1060
|
// Log the real cause; only surface the detail to the client in debug
|
|
1052
1061
|
// mode — a resolver exception can carry internal state (DB errors,
|
|
@@ -1067,7 +1076,7 @@ export class GraphQL {
|
|
|
1067
1076
|
const result: Record<string, unknown>[] = [];
|
|
1068
1077
|
for (const item of value) {
|
|
1069
1078
|
const obj: Record<string, unknown> = {};
|
|
1070
|
-
const errs = this.resolveSelectionsInto(
|
|
1079
|
+
const errs = await this.resolveSelectionsInto(
|
|
1071
1080
|
sel.selections, new Map(), item, variables, context, fragments, obj, depth + 1,
|
|
1072
1081
|
);
|
|
1073
1082
|
errors.push(...errs);
|
|
@@ -1078,7 +1087,7 @@ export class GraphQL {
|
|
|
1078
1087
|
|
|
1079
1088
|
if (value !== null && value !== undefined) {
|
|
1080
1089
|
const obj: Record<string, unknown> = {};
|
|
1081
|
-
const errs = this.resolveSelectionsInto(
|
|
1090
|
+
const errs = await this.resolveSelectionsInto(
|
|
1082
1091
|
sel.selections, new Map(), value, variables, context, fragments, obj, depth + 1,
|
|
1083
1092
|
);
|
|
1084
1093
|
errors.push(...errs);
|
package/packages/core/src/mcp.ts
CHANGED
|
@@ -1231,7 +1231,17 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1231
1231
|
try {
|
|
1232
1232
|
const db = (globalThis as any).__tina4_db;
|
|
1233
1233
|
if (!db) return { error: "No database connection" };
|
|
1234
|
-
|
|
1234
|
+
// Use the real migration API (parity with the Python master, whose MCP
|
|
1235
|
+
// migration_status calls MigrationRunner(db).status()). Previously a stub.
|
|
1236
|
+
// Load orm via dynamic ESM import (the same mechanism server.ts uses for
|
|
1237
|
+
// autoMigrateOnStartup) — reqSibling()'s createRequire fails here because
|
|
1238
|
+
// @tina4/orm imports @tina4/core (no CJS "exports" main).
|
|
1239
|
+
const orm = await import("../../orm/src/index.js");
|
|
1240
|
+
if (typeof orm.status !== "function") {
|
|
1241
|
+
return { error: "Migration API not available (install @tina4/orm)" };
|
|
1242
|
+
}
|
|
1243
|
+
const st = await orm.status(db, { migrationsDir: path.join(projectRoot, "migrations") });
|
|
1244
|
+
return { completed: st.completed, pending: st.pending };
|
|
1235
1245
|
} catch (e) {
|
|
1236
1246
|
return { error: (e as Error).message };
|
|
1237
1247
|
}
|
|
@@ -1264,7 +1274,16 @@ export function registerDevTools(server: McpServer): void {
|
|
|
1264
1274
|
try {
|
|
1265
1275
|
const db = (globalThis as any).__tina4_db;
|
|
1266
1276
|
if (!db) return { error: "No database connection" };
|
|
1267
|
-
|
|
1277
|
+
// Run pending migrations for real (parity with the Python master's MCP
|
|
1278
|
+
// migration_run -> Migration(db).migrate()). Previously a stub. Load orm
|
|
1279
|
+
// via dynamic ESM import (server.ts's mechanism); reqSibling's createRequire
|
|
1280
|
+
// fails because @tina4/orm imports @tina4/core.
|
|
1281
|
+
const orm = await import("../../orm/src/index.js");
|
|
1282
|
+
if (typeof orm.migrate !== "function") {
|
|
1283
|
+
return { error: "Migration API not available (install @tina4/orm)" };
|
|
1284
|
+
}
|
|
1285
|
+
const result = await orm.migrate(db, { migrationsDir: path.join(projectRoot, "migrations") });
|
|
1286
|
+
return { applied: result.applied, skipped: result.skipped, failed: result.failed };
|
|
1268
1287
|
} catch (e) {
|
|
1269
1288
|
return { error: (e as Error).message };
|
|
1270
1289
|
}
|