tina4-nodejs 3.13.43 → 3.13.44

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 CHANGED
@@ -1,4 +1,4 @@
1
- # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.43)
1
+ # CLAUDE.md - AI Developer Guide for tina4-nodejs (v3.13.44)
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
@@ -3,7 +3,7 @@
3
3
 
4
4
 
5
5
 
6
- "version": "3.13.43",
6
+ "version": "3.13.44",
7
7
 
8
8
  "type": "module",
9
9
  "description": "Tina4 for Node.js/TypeScript \u2014 54 built-in features, zero dependencies",
@@ -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.fetchAll("SELECT * FROM products"));
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.fetchAll("SELECT * FROM reviews WHERE product_id = ?", [product.id]));
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
- value = config.resolver(null, args, ctx);
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);
@@ -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
- return { info: "Migration status not yet implemented for Node.js" };
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
- return { info: "Migration run not yet implemented for Node.js" };
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
  }