@gonvex/cli 0.1.32 → 0.3.1

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 (36) hide show
  1. package/README.md +30 -15
  2. package/dist/browser.d.ts +1 -1
  3. package/dist/browser.js +1 -1
  4. package/dist/browser.js.map +1 -1
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.js +274 -555
  7. package/dist/index.js.map +1 -1
  8. package/dist/manifest-types.d.ts +261 -0
  9. package/dist/manifest-types.js +3 -0
  10. package/dist/manifest-types.js.map +1 -0
  11. package/dist/module-artifact.d.ts +26 -0
  12. package/dist/module-artifact.js +1585 -0
  13. package/dist/module-artifact.js.map +1 -0
  14. package/dist/react.d.ts +2 -2
  15. package/dist/react.js +1 -1
  16. package/dist/react.js.map +1 -1
  17. package/dist/templates/vite-react/README.md +5 -2
  18. package/dist/templates/vite-react/gonvex/_build/module.js +407 -0
  19. package/dist/templates/vite-react/gonvex/_generated/api.ts +185 -3
  20. package/dist/templates/vite-react/gonvex/_generated/client.ts +1 -1
  21. package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/schema.ts +2 -1
  22. package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/tables.ts +1 -1
  23. package/dist/templates/vite-react/gonvex/_generated/manifest.json +201 -66
  24. package/dist/templates/vite-react/gonvex/_generated/module.json +120 -0
  25. package/dist/templates/vite-react/gonvex/_generated/react.ts +2 -2
  26. package/dist/templates/vite-react/gonvex/_generated/schema.ts +4 -34
  27. package/dist/templates/vite-react/gonvex/_generated/tenant/schema.ts +1 -30
  28. package/dist/templates/vite-react/gonvex/_generated/tenant/tables.ts +0 -30
  29. package/dist/templates/vite-react/gonvex/index.ts +1 -0
  30. package/dist/templates/vite-react/gonvex/messages.ts +47 -0
  31. package/dist/templates/vite-react/migrations/0001_messages.sql +9 -0
  32. package/dist/templates/vite-react/package.json +1 -0
  33. package/dist/templates/vite-react/src/App.tsx +5 -5
  34. package/package.json +4 -3
  35. package/dist/templates/vite-react/gonvex/messages.go +0 -38
  36. package/dist/templates/vite-react/gonvex/schema.go +0 -14
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { createInterface } from "node:readline/promises";
8
8
  import { dirname, join, relative, resolve } from "node:path";
9
9
  import { Writable } from "node:stream";
10
10
  import { fileURLToPath } from "node:url";
11
+ import { buildModuleArtifact, detectProjectLanguage, moduleManifestFunctions, moduleSourceFiles, isModuleSchema, } from "./module-artifact.js";
11
12
  const defaultRuntimeURL = "http://localhost:8080";
12
13
  const runtimeSyncRetryMs = 5000;
13
14
  const runtimeStateCheckMs = 2500;
@@ -440,10 +441,14 @@ async function runCodegen(argv) {
440
441
  });
441
442
  const backendDir = join(projectRoot, "gonvex");
442
443
  await mkdir(backendDir, { recursive: true });
443
- const files = await goFiles(backendDir);
444
- const manifest = await buildManifest(projectRoot, files, settings.projectID);
444
+ const sources = await collectBackendSources(projectRoot);
445
+ const manifest = await buildManifest(projectRoot, sources, settings.projectID);
445
446
  await writeBindings(projectRoot, manifest);
446
- console.log(`[gonvex] generated ${Object.keys(manifest.functions).length} function binding(s) without runtime sync`);
447
+ console.log(`[gonvex] generated ${Object.keys(manifest.functions).length} TypeScript function binding(s)`);
448
+ if (manifest.module) {
449
+ const fileCount = Object.keys(manifest.module.files).length;
450
+ console.log(`[gonvex] built ${manifest.module.language} module artifact ${manifest.module.hash.slice(0, 12)} from ${fileCount} file(s)`);
451
+ }
447
452
  }
448
453
  async function runEnv(argv) {
449
454
  const parsedArgs = parseEnvCommandArgs(argv);
@@ -530,7 +535,7 @@ async function runEnv(argv) {
530
535
  throw new Error(`unknown env command ${action}`);
531
536
  }
532
537
  export async function runAuth(argv) {
533
- const optionsWithValues = ["--project", "--runtime-url", "--project-id", "--key", "--origin", "--callback-path", "--signup-mode", "--tenant", "--email", "--owner", "--role", "--user"];
538
+ const optionsWithValues = ["--project", "--runtime-url", "--project-id", "--key", "--origin", "--callback-path", "--signup-mode", "--tenant", "--email", "--owner", "--role", "--member", "--account"];
534
539
  const positional = positionalArgs(argv, optionsWithValues);
535
540
  const action = positional[0];
536
541
  if (!action || action === "help") {
@@ -655,20 +660,20 @@ export async function runAuth(argv) {
655
660
  throw new Error("Google auth production readiness check failed");
656
661
  return;
657
662
  }
658
- if (action === "users" || action === "accounts") {
659
- const usersEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/users`;
660
- const payload = await runtimeJSON(await fetch(usersEndpoint, { headers: projectAuthHeaders(settings) }));
661
- const users = payload.users ?? [];
663
+ if (action === "accounts") {
664
+ const accountsEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/accounts`;
665
+ const payload = await runtimeJSON(await fetch(accountsEndpoint, { headers: projectAuthHeaders(settings) }));
666
+ const accounts = payload.accounts ?? [];
662
667
  if (argv.includes("--json")) {
663
- console.log(JSON.stringify(users, null, 2));
668
+ console.log(JSON.stringify(accounts, null, 2));
664
669
  return;
665
670
  }
666
- if (users.length === 0) {
671
+ if (accounts.length === 0) {
667
672
  console.log(`[gonvex] no app accounts for ${settings.projectID}`);
668
673
  return;
669
674
  }
670
- for (const user of users) {
671
- console.log(`${user.id}\t${user.email ?? ""}\t${user.name ?? ""}\t${user.provider}`);
675
+ for (const account of accounts) {
676
+ console.log(`${account.id}\t${account.email ?? ""}\t${account.name ?? ""}\t${account.provider}`);
672
677
  }
673
678
  return;
674
679
  }
@@ -711,7 +716,7 @@ export async function runAuth(argv) {
711
716
  return;
712
717
  }
713
718
  for (const member of payload.members ?? [])
714
- console.log(`${member.userId}\t${member.email}\t${member.role}\t${member.name}`);
719
+ console.log(`${member.memberId}\t${member.email}\t${member.role}\t${member.name}`);
715
720
  for (const invitation of payload.invitations ?? [])
716
721
  console.log(`invited\t${invitation.email}\t${invitation.role}\t${invitation.expiresAt ?? ""}`);
717
722
  return;
@@ -729,41 +734,41 @@ export async function runAuth(argv) {
729
734
  return;
730
735
  }
731
736
  if (operation === "remove" || operation === "rm") {
732
- const user = valueFor(argv, "--user");
737
+ const member = valueFor(argv, "--member");
733
738
  const email = valueFor(argv, "--email");
734
- if (!user && !email)
735
- throw new Error("--user is required to remove a member, or --email to revoke an invitation");
736
- const target = user ? `user=${encodeURIComponent(user)}` : `email=${encodeURIComponent(email)}`;
739
+ if (!member && !email)
740
+ throw new Error("--member is required to remove a member, or --email to revoke an invitation");
741
+ const target = member ? `member=${encodeURIComponent(member)}` : `email=${encodeURIComponent(email)}`;
737
742
  await runtimeJSON(await fetch(`${membershipEndpoint}&${target}`, {
738
743
  method: "DELETE", headers: projectAuthHeaders(settings),
739
744
  }));
740
- console.log(user
741
- ? `[gonvex] removed ${user} from tenant ${tenant}`
745
+ console.log(member
746
+ ? `[gonvex] removed ${member} from tenant ${tenant}`
742
747
  : `[gonvex] revoked the invitation for ${email} from tenant ${tenant}`);
743
748
  return;
744
749
  }
745
750
  throw new Error(`unknown auth memberships command ${operation}`);
746
751
  }
747
- if (action === "user") {
752
+ if (action === "account") {
748
753
  const operation = positional[1];
749
- const user = valueFor(argv, "--user") ?? positional[2];
750
- if (!operation || !user)
751
- throw new Error("usage: gonvex auth user <disable|enable|delete> <user-id>");
752
- const userEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/users/${encodeURIComponent(user)}`;
754
+ const account = valueFor(argv, "--account") ?? positional[2];
755
+ if (!operation || !account)
756
+ throw new Error("usage: gonvex auth account <disable|enable|delete> <account-id>");
757
+ const accountEndpoint = `${settings.runtimeURL}/dev/projects/${encodeURIComponent(settings.projectID)}/auth/accounts/${encodeURIComponent(account)}`;
753
758
  if (operation === "disable" || operation === "enable") {
754
- await runtimeJSON(await fetch(userEndpoint, {
759
+ await runtimeJSON(await fetch(accountEndpoint, {
755
760
  method: "PATCH", headers: { ...projectAuthHeaders(settings), "content-type": "application/json" },
756
761
  body: JSON.stringify({ disabled: operation === "disable" }),
757
762
  }));
758
- console.log(`[gonvex] ${operation}d app account ${user}`);
763
+ console.log(`[gonvex] ${operation}d app account ${account}`);
759
764
  return;
760
765
  }
761
766
  if (operation === "delete" || operation === "remove") {
762
- await runtimeJSON(await fetch(userEndpoint, { method: "DELETE", headers: projectAuthHeaders(settings) }));
763
- console.log(`[gonvex] deleted app account ${user}`);
767
+ await runtimeJSON(await fetch(accountEndpoint, { method: "DELETE", headers: projectAuthHeaders(settings) }));
768
+ console.log(`[gonvex] deleted app account ${account}`);
764
769
  return;
765
770
  }
766
- throw new Error(`unknown auth user command ${operation}`);
771
+ throw new Error(`unknown auth account command ${operation}`);
767
772
  }
768
773
  printAuthHelp();
769
774
  throw new Error(`unknown auth command ${action}`);
@@ -867,8 +872,8 @@ async function wireViteReactGoogleAuth(root) {
867
872
  if (main.includes('from "../gonvex/auth"') && app.includes("GoogleSignInButton"))
868
873
  return true;
869
874
  const providerImport = 'import { GonvexProvider } from "../gonvex/_generated/react";';
870
- const reactImport = 'import { useMutation, useQuery } from "../gonvex/_generated/react";';
871
- const appStart = 'export default function App(props: { runtimeURL: string }) {\n const messages = useQuery<Message[]>(api.messages.list, {}) ?? [];';
875
+ const reactImport = 'import { useLiveQuery, useReducer } from "../gonvex/_generated/react";';
876
+ const appStart = 'export default function App(props: { runtimeURL: string }) {\n const messages = useLiveQuery<Message[]>(api.messages.list, {}) ?? [];';
872
877
  if (!main.includes(providerImport) || !main.includes("<GonvexProvider client={gonvex}>") || !app.includes(reactImport) || !app.includes(appStart)) {
873
878
  return false;
874
879
  }
@@ -889,9 +894,9 @@ async function wireViteReactGoogleAuth(root) {
889
894
  "",
890
895
  "function AuthenticatedApp(props: { runtimeURL: string }) {",
891
896
  " const auth = useGonvexAuth();",
892
- ' const messages = useQuery<Message[]>(api.messages.list, {}) ?? [];',
897
+ ' const messages = useLiveQuery<Message[]>(api.messages.list, {}) ?? [];',
893
898
  ].join("\n"))
894
- .replace('<div className="status">Connected to {props.runtimeURL}</div>', '<div className="status"><span>Connected to {props.runtimeURL} as {auth.user?.email}</span><GoogleSignInButton /></div>');
899
+ .replace('<div className="status">Connected to {props.runtimeURL}</div>', '<div className="status"><span>Connected to {props.runtimeURL} as {auth.account?.email}</span><GoogleSignInButton /></div>');
895
900
  await writeFile(mainPath, main);
896
901
  await writeFile(appPath, app);
897
902
  return true;
@@ -905,10 +910,17 @@ async function watchProject(root, settings, once, signal, initialState) {
905
910
  let lastSyncSucceeded = initialState?.lastSyncSucceeded ?? false;
906
911
  let lastRuntimeCheck = initialState?.lastRuntimeCheck ?? 0;
907
912
  while (!signal?.aborted) {
908
- const files = await goFiles(backendDir);
913
+ const sources = await collectBackendSources(root);
909
914
  // Watch migrations too: editing or adding one must trigger a re-sync,
910
- // otherwise a new migration sits unapplied until an unrelated .go edit.
911
- const fingerprint = await filesFingerprint([...files, ...await migrationFiles(join(root, "migrations"))]);
915
+ // otherwise a new migration sits unapplied until an unrelated module edit.
916
+ // gonvex.json selects the TypeScript entrypoint and bundle destination. It
917
+ // belongs in the fingerprint, while gonvex/_build is excluded from source
918
+ // collection so writing the generated ESM cannot trigger a rebuild loop.
919
+ const configPath = join(root, "gonvex.json");
920
+ const fingerprintFiles = [...sources.moduleFiles, ...await migrationFiles(join(root, "migrations"))];
921
+ if (existsSync(configPath))
922
+ fingerprintFiles.push(configPath);
923
+ const fingerprint = await filesFingerprint(fingerprintFiles);
912
924
  const now = Date.now();
913
925
  const shouldBuild = fingerprint !== lastFingerprint;
914
926
  const shouldRetryRuntimeSync = !once && !lastSyncSucceeded && lastManifest !== null && now - lastSyncAttempt > runtimeSyncRetryMs;
@@ -917,7 +929,7 @@ async function watchProject(root, settings, once, signal, initialState) {
917
929
  lastFingerprint = fingerprint;
918
930
  let manifest;
919
931
  if (shouldBuild) {
920
- manifest = await buildManifest(root, files, settings.projectID);
932
+ manifest = await buildManifest(root, sources, settings.projectID);
921
933
  }
922
934
  else {
923
935
  manifest = lastManifest;
@@ -931,7 +943,12 @@ async function watchProject(root, settings, once, signal, initialState) {
931
943
  }
932
944
  lastSyncAttempt = now;
933
945
  try {
934
- await syncRuntime(settings, manifest);
946
+ const observedSchema = await syncRuntime(settings, manifest);
947
+ if (observedSchema) {
948
+ manifest.schema = observedSchema;
949
+ lastManifest = manifest;
950
+ await writeBindings(root, manifest);
951
+ }
935
952
  lastSyncSucceeded = true;
936
953
  lastRuntimeCheck = now;
937
954
  console.log(`[gonvex] synced project ${settings.projectID || "(key-inferred)"} to ${settings.runtimeURL}`);
@@ -940,7 +957,7 @@ async function watchProject(root, settings, once, signal, initialState) {
940
957
  lastSyncSucceeded = false;
941
958
  const detail = error instanceof Error ? error.message : String(error);
942
959
  const valkeyHint = isLocalRuntimeURL(settings.runtimeURL)
943
- ? " Local runtimes require a reachable VALKEY_URL (or REDIS_URL), for example VALKEY_URL=redis://127.0.0.1:6380/0."
960
+ ? " Local runtimes require a reachable VALKEY_URL, for example VALKEY_URL=redis://127.0.0.1:6380/0."
944
961
  : "";
945
962
  console.error(`[gonvex] runtime sync failed: ${detail}.${valkeyHint}`);
946
963
  }
@@ -954,7 +971,12 @@ async function watchProject(root, settings, once, signal, initialState) {
954
971
  const inSync = await runtimeHasManifest(settings, manifest);
955
972
  if (!inSync) {
956
973
  lastSyncAttempt = now;
957
- await syncRuntime(settings, manifest);
974
+ const observedSchema = await syncRuntime(settings, manifest);
975
+ if (observedSchema) {
976
+ manifest.schema = observedSchema;
977
+ lastManifest = manifest;
978
+ await writeBindings(root, manifest);
979
+ }
958
980
  lastSyncSucceeded = true;
959
981
  console.log(`[gonvex] runtime state was missing; re-synced project ${settings.projectID || "(key-inferred)"}`);
960
982
  }
@@ -970,415 +992,56 @@ async function watchProject(root, settings, once, signal, initialState) {
970
992
  }
971
993
  return { lastFingerprint, lastManifest, lastSyncAttempt, lastSyncSucceeded, lastRuntimeCheck };
972
994
  }
973
- async function buildManifest(root, files, projectID) {
974
- const functions = {};
975
- const schema = emptySchemaDefinition();
976
- let packageName = "app";
977
- for (const file of files) {
978
- Object.assign(functions, await parseRegistrations(root, file));
979
- mergeSchemaDefinition(schema, await parseSchema(file));
980
- if (packageName === "app") {
981
- packageName = await detectPackageName(file);
982
- }
983
- }
984
- const bundle = await buildSourceBundle(root, files, projectID, packageName);
995
+ // Backend sources are collected once per build so the fingerprint, the
996
+ // manifest, and the shipped artifact all agree on the same file set.
997
+ async function collectBackendSources(root) {
998
+ const backendDir = join(root, "gonvex");
999
+ const config = await loadConfig(root);
1000
+ const module = await moduleSourceFiles(backendDir);
1001
+ await detectProjectLanguage(backendDir, config.language);
985
1002
  return {
986
- project: projectID,
987
- generatedAt: new Date().toISOString(),
988
- functions,
989
- schema,
990
- bundle,
1003
+ moduleFiles: module,
1004
+ config,
991
1005
  };
992
1006
  }
993
- async function buildSourceBundle(root, files, projectID, packageName) {
994
- const backendDir = join(root, "gonvex");
995
- const encodedFiles = {};
996
- for (const file of files) {
997
- const source = await readFile(file);
998
- const rel = relative(backendDir, file).replace(/\\/g, "/");
999
- encodedFiles[`app/${rel}`] = Buffer.from(source).toString("base64");
1000
- }
1001
- // Versioned SQL migrations ship in the bundle under migrations/, which is
1002
- // where the runtime looks for them. Without this the runtime sees no
1003
- // migrations at all and silently applies only the declarative schema.
1004
- for (const file of await migrationFiles(join(root, "migrations"))) {
1005
- const source = await readFile(file);
1006
- const rel = relative(join(root, "migrations"), file).replace(/\\/g, "/");
1007
- encodedFiles[`migrations/${rel}`] = Buffer.from(source).toString("base64");
1008
- }
1009
- const hash = createHash("sha256");
1010
- for (const path of Object.keys(encodedFiles).sort()) {
1011
- hash.update(`${path}:${encodedFiles[path]};`);
1012
- }
1007
+ async function buildManifest(root, sources, projectID) {
1008
+ const module = await buildModuleArtifact({
1009
+ root,
1010
+ backendDir: join(root, "gonvex"),
1011
+ files: sources.moduleFiles,
1012
+ migrations: await migrationFiles(join(root, "migrations")),
1013
+ entrypoint: sources.config.module?.entrypoint,
1014
+ bundle: sources.config.module?.bundle,
1015
+ });
1013
1016
  return {
1014
- hash: hash.digest("hex"),
1015
- modulePath: `gonvexapp/${sanitizeProjectID(projectID)}`,
1016
- packageName,
1017
- files: encodedFiles,
1017
+ project: projectID,
1018
+ generatedAt: new Date().toISOString(),
1019
+ functions: moduleManifestFunctions(module),
1020
+ schema: emptySchemaDefinition(),
1021
+ module,
1022
+ ...(Object.keys(module.visibility).length > 0 ? { visibility: module.visibility } : {}),
1018
1023
  };
1019
1024
  }
1020
- async function detectPackageName(file) {
1021
- const source = await readFile(file, "utf8");
1022
- const match = source.match(/^package\s+([A-Za-z_][A-Za-z0-9_]*)/m);
1023
- return match?.[1] ?? "app";
1024
- }
1025
- function sanitizeProjectID(projectID) {
1026
- const trimmed = projectID.trim();
1027
- if (!trimmed)
1028
- return "project";
1029
- return trimmed.replace(/[^a-zA-Z0-9._-]+/g, "-");
1030
- }
1031
- async function parseRegistrations(root, file) {
1032
- const source = await readFile(file, "utf8");
1033
- const pattern = /app\.(Query|Mutation|Action|HTTP|PublicHTTP|InternalMutation|LiveGrid|Sync)\(\s*"([^"]+)"\s*,\s*([A-Za-z_][A-Za-z0-9_]*)/g;
1034
- const entries = {};
1035
- for (const match of source.matchAll(pattern)) {
1036
- const entry = {
1037
- kind: functionKind(match[1]),
1038
- handler: match[3],
1039
- file: relative(root, file),
1040
- };
1041
- const openParen = source.indexOf("(", match.index);
1042
- const closeParen = findClosingParen(source, openParen);
1043
- if (openParen >= 0 && closeParen > openParen) {
1044
- const dependencies = parseFunctionDependencies(source.slice(openParen + 1, closeParen));
1045
- if (Object.keys(dependencies).length > 0)
1046
- entry.dependencies = dependencies;
1047
- if (match[1] === "Sync") {
1048
- entry.sync = parseSyncDefinition(source.slice(openParen + 1, closeParen));
1049
- }
1050
- }
1051
- entries[match[2]] = entry;
1052
- }
1053
- return entries;
1054
- }
1055
- function parseSyncDefinition(callBody) {
1056
- const match = /(?:gonvex\.)?SyncTable\s*\(/.exec(callBody);
1057
- if (!match)
1058
- return undefined;
1059
- const openParen = callBody.indexOf("(", match.index);
1060
- const closeParen = findClosingParen(callBody, openParen);
1061
- if (closeParen < 0)
1062
- return undefined;
1063
- const table = stringArgs(callBody.slice(openParen + 1, closeParen))[0];
1064
- if (!table)
1065
- return undefined;
1066
- const definition = { table, key: "id", columns: [], mode: "eager" };
1067
- let cursor = closeParen + 1;
1068
- while (cursor < callBody.length) {
1069
- const chain = chainedGoMethod(callBody, cursor, "Key|Columns|EqualArg|ExcludeWhenSet|VisibilityDependsOn|OrderBy|Eager|Progressive|Budget");
1070
- if (!chain)
1071
- break;
1072
- const chainOpen = chain.openParen;
1073
- const chainClose = findClosingParen(callBody, chainOpen);
1074
- if (chainClose < 0)
1075
- break;
1076
- const method = chain.method;
1077
- const body = callBody.slice(chainOpen + 1, chainClose);
1078
- const values = stringArgs(body);
1079
- if (method === "Key" && values[0])
1080
- definition.key = values[0];
1081
- if (method === "Columns")
1082
- definition.columns = values;
1083
- if (method === "EqualArg" && values[0]) {
1084
- (definition.equalFilters ??= {})[values[0]] = values[1] ?? values[0];
1085
- }
1086
- if (method === "ExcludeWhenSet")
1087
- definition.excludeWhenSet = values;
1088
- if (method === "VisibilityDependsOn")
1089
- definition.visibilityTables = values;
1090
- if (method === "OrderBy" && values[0]) {
1091
- definition.orderBy = values[0];
1092
- definition.orderDirection = values[1]?.toLowerCase() === "asc" ? "asc" : "desc";
1093
- }
1094
- if (method === "Eager")
1095
- definition.mode = "eager";
1096
- if (method === "Progressive")
1097
- definition.mode = "progressive";
1098
- if (method === "Budget") {
1099
- const numbers = body.split(",").map((value) => Number(value.trim()));
1100
- if (Number.isFinite(numbers[0]) && numbers[0] > 0)
1101
- definition.maxRows = numbers[0];
1102
- if (Number.isFinite(numbers[1]) && numbers[1] > 0)
1103
- definition.maxBytes = numbers[1];
1104
- }
1105
- cursor = chainClose + 1;
1106
- }
1107
- if (!definition.columns.includes(definition.key))
1108
- definition.columns.push(definition.key);
1109
- return definition;
1110
- }
1111
- function parseFunctionDependencies(callBody) {
1112
- const dependencies = {};
1113
- const pattern = /(?:gonvex\.)?(Reads|Writes|ReadsEphemeral|WritesEphemeral|ShareByPermissions|ShareByVisibility|ShareResultFrom|OptimisticMutation|OptimisticProjection)\s*\(/g;
1114
- let match;
1115
- while ((match = pattern.exec(callBody)) !== null) {
1116
- const option = match[1];
1117
- const openParen = callBody.indexOf("(", match.index);
1118
- const closeParen = findClosingParen(callBody, openParen);
1119
- if (closeParen < 0)
1120
- break;
1121
- if (option === "ReadsEphemeral") {
1122
- dependencies.readsEphemeral = true;
1123
- pattern.lastIndex = closeParen + 1;
1124
- continue;
1125
- }
1126
- if (option === "WritesEphemeral") {
1127
- dependencies.writesEphemeral = true;
1128
- pattern.lastIndex = closeParen + 1;
1129
- continue;
1130
- }
1131
- if (option === "ShareByPermissions") {
1132
- dependencies.shareByPermissions = true;
1133
- pattern.lastIndex = closeParen + 1;
1134
- continue;
1135
- }
1136
- if (option === "ShareByVisibility") {
1137
- dependencies.shareByVisibility = stringArgs(callBody.slice(openParen + 1, closeParen))[0];
1138
- pattern.lastIndex = closeParen + 1;
1139
- continue;
1140
- }
1141
- if (option === "ShareResultFrom") {
1142
- const values = stringArgs(callBody.slice(openParen + 1, closeParen));
1143
- dependencies.shareResultFrom = values[0];
1144
- dependencies.shareResultField = values[1];
1145
- pattern.lastIndex = closeParen + 1;
1146
- continue;
1147
- }
1148
- if (option === "OptimisticMutation") {
1149
- const entity = stringArgs(callBody.slice(openParen + 1, closeParen))[0];
1150
- const definition = { entity: entity?.trim() ?? "", rowIdPath: [], fieldsPath: [] };
1151
- let cursor = closeParen + 1;
1152
- while (cursor < callBody.length) {
1153
- const chain = chainedGoMethod(callBody, cursor, "RowIDArg|FieldsArg");
1154
- if (!chain)
1155
- break;
1156
- const chainClose = findClosingParen(callBody, chain.openParen);
1157
- if (chainClose < 0)
1158
- break;
1159
- const path = cleanParsedOptimisticPath(stringArgs(callBody.slice(chain.openParen + 1, chainClose))[0] ?? "");
1160
- if (chain.method === "RowIDArg")
1161
- definition.rowIdPath = path;
1162
- if (chain.method === "FieldsArg")
1163
- definition.fieldsPath = path;
1164
- cursor = chainClose + 1;
1165
- }
1166
- if (definition.entity)
1167
- dependencies.optimisticMutation = definition;
1168
- pattern.lastIndex = cursor;
1169
- continue;
1170
- }
1171
- if (option === "OptimisticProjection") {
1172
- const entity = stringArgs(callBody.slice(openParen + 1, closeParen))[0];
1173
- const definition = { entity: entity?.trim() ?? "", key: "id", resultPath: [] };
1174
- let cursor = closeParen + 1;
1175
- while (cursor < callBody.length) {
1176
- const chain = chainedGoMethod(callBody, cursor, "Key|ResultPath");
1177
- if (!chain)
1178
- break;
1179
- const chainClose = findClosingParen(callBody, chain.openParen);
1180
- if (chainClose < 0)
1181
- break;
1182
- const value = stringArgs(callBody.slice(chain.openParen + 1, chainClose))[0] ?? "";
1183
- if (chain.method === "Key" && value.trim())
1184
- definition.key = value.trim();
1185
- if (chain.method === "ResultPath")
1186
- definition.resultPath = cleanParsedOptimisticPath(value);
1187
- cursor = chainClose + 1;
1188
- }
1189
- if (definition.entity)
1190
- dependencies.optimisticProjection = definition;
1191
- pattern.lastIndex = cursor;
1192
- continue;
1193
- }
1194
- const tables = stringArgs(callBody.slice(openParen + 1, closeParen));
1195
- const start = option === "Reads"
1196
- ? (dependencies.reads ??= []).push(...tables.map((table) => ({ table }))) - tables.length
1197
- : (dependencies.writes ??= []).push(...tables.map((table) => ({ table }))) - tables.length;
1198
- let cursor = closeParen + 1;
1199
- while (cursor < callBody.length) {
1200
- const chain = chainedGoMethod(callBody, cursor, "Columns|Filters|OrdersBy|Windowed|Predicate");
1201
- if (!chain)
1202
- break;
1203
- const chainOpen = chain.openParen;
1204
- const chainClose = findClosingParen(callBody, chainOpen);
1205
- if (chainClose < 0)
1206
- break;
1207
- const method = chain.method;
1208
- const values = stringArgs(callBody.slice(chainOpen + 1, chainClose));
1209
- if (option === "Reads") {
1210
- for (const dependency of (dependencies.reads ?? []).slice(start)) {
1211
- if (method === "Columns" && values.length > 0)
1212
- dependency.columns = values;
1213
- if (method === "Filters" && values.length > 0)
1214
- dependency.filters = values;
1215
- if (method === "OrdersBy" && values.length > 0)
1216
- dependency.ordersBy = values;
1217
- if (method === "Windowed")
1218
- dependency.windowed = true;
1219
- if (method === "Predicate" && values[0])
1220
- dependency.predicate = values[0];
1221
- }
1222
- }
1223
- else if (method === "Columns") {
1224
- for (const dependency of (dependencies.writes ?? []).slice(start)) {
1225
- if (values.length > 0)
1226
- dependency.columns = values;
1227
- }
1228
- }
1229
- cursor = chainClose + 1;
1230
- }
1231
- pattern.lastIndex = cursor;
1232
- }
1233
- return dependencies;
1234
- }
1235
- function chainedGoMethod(source, start, methods) {
1236
- let cursor = skipGoTrivia(source, start);
1237
- if (source[cursor] !== ".")
1238
- return undefined;
1239
- cursor = skipGoTrivia(source, cursor + 1);
1240
- const match = new RegExp(`^(${methods})\\b`).exec(source.slice(cursor));
1241
- if (!match)
1242
- return undefined;
1243
- const method = match[1];
1244
- cursor = skipGoTrivia(source, cursor + match[0].length);
1245
- if (source[cursor] !== "(")
1246
- return undefined;
1247
- return { method, openParen: cursor };
1248
- }
1249
- function skipGoTrivia(source, start) {
1250
- let cursor = start;
1251
- while (cursor < source.length) {
1252
- const char = source[cursor];
1253
- const next = source[cursor + 1] ?? "";
1254
- if (/\s/.test(char)) {
1255
- cursor += 1;
1256
- continue;
1257
- }
1258
- if (char === "/" && next === "/") {
1259
- const newline = source.indexOf("\n", cursor + 2);
1260
- return newline < 0 ? source.length : skipGoTrivia(source, newline + 1);
1261
- }
1262
- if (char === "/" && next === "*") {
1263
- const close = source.indexOf("*/", cursor + 2);
1264
- return close < 0 ? source.length : skipGoTrivia(source, close + 2);
1265
- }
1266
- break;
1267
- }
1268
- return cursor;
1269
- }
1270
- function findClosingParen(source, openParen) {
1271
- if (openParen < 0 || source[openParen] !== "(")
1272
- return -1;
1273
- let depth = 0;
1274
- let quote = "";
1275
- let escaped = false;
1276
- let lineComment = false;
1277
- let blockComment = false;
1278
- for (let index = openParen; index < source.length; index += 1) {
1279
- const char = source[index];
1280
- const next = source[index + 1] ?? "";
1281
- if (lineComment) {
1282
- if (char === "\n")
1283
- lineComment = false;
1284
- continue;
1285
- }
1286
- if (blockComment) {
1287
- if (char === "*" && next === "/") {
1288
- blockComment = false;
1289
- index += 1;
1290
- }
1291
- continue;
1292
- }
1293
- if (quote) {
1294
- if (quote !== "`" && escaped) {
1295
- escaped = false;
1296
- continue;
1297
- }
1298
- if (quote !== "`" && char === "\\") {
1299
- escaped = true;
1300
- continue;
1301
- }
1302
- if (char === quote)
1303
- quote = "";
1304
- continue;
1305
- }
1306
- if (char === "/" && next === "/") {
1307
- lineComment = true;
1308
- index += 1;
1309
- continue;
1310
- }
1311
- if (char === "/" && next === "*") {
1312
- blockComment = true;
1313
- index += 1;
1314
- continue;
1315
- }
1316
- if (char === '"' || char === "'" || char === "`") {
1317
- quote = char;
1318
- continue;
1319
- }
1320
- if (char === "(")
1321
- depth += 1;
1322
- if (char === ")") {
1323
- depth -= 1;
1324
- if (depth === 0)
1325
- return index;
1326
- }
1327
- }
1328
- return -1;
1329
- }
1330
- async function parseSchema(file) {
1331
- const source = await readFile(file, "utf8");
1332
- const tablePattern = /s\.(Table|TenantTable|LandlordTable)\(\s*"([^"]+)"\s*,\s*func\([^)]*\)\s*\{([\s\S]*?)\n\s*\}\s*\)/g;
1333
- const columnPattern = /t\.(ID|String|Text|Int|Int64|Float64|Bool|Time|JSON)\(\s*"([^"]+)"([^)]*)\)/g;
1334
- const indexPattern = /t\.(Index|UniqueIndex|TrigramIndex)\(\s*"([^"]+)"([^)]*)\)/g;
1335
- const schema = emptySchemaDefinition();
1336
- for (const tableMatch of source.matchAll(tablePattern)) {
1337
- const table = { columns: {}, indexes: {} };
1338
- const scope = tableMatch[1];
1339
- const name = tableMatch[2];
1340
- const body = tableMatch[3];
1341
- for (const columnMatch of body.matchAll(columnPattern)) {
1342
- const kind = columnMatch[1];
1343
- table.columns[columnMatch[2]] = {
1344
- type: columnType(kind),
1345
- nullable: columnMatch[3].includes("gonvex.Nullable"),
1346
- primaryKey: kind === "ID",
1347
- };
1348
- }
1349
- for (const indexMatch of body.matchAll(indexPattern)) {
1350
- table.indexes[indexMatch[2]] = {
1351
- columns: stringArgs(indexMatch[3]),
1352
- unique: indexMatch[1] === "UniqueIndex",
1353
- ...(indexMatch[1] === "TrigramIndex" ? { kind: "trigram" } : {}),
1354
- };
1355
- }
1356
- if (scope === "LandlordTable") {
1357
- schema.landlordTables[name] = table;
1358
- }
1359
- else {
1360
- schema.tenantTables[name] = table;
1361
- schema.tables[name] = table;
1362
- }
1363
- }
1364
- return schema;
1365
- }
1366
1025
  async function writeBindings(root, manifest) {
1367
1026
  const dir = join(root, "gonvex", "_generated");
1368
1027
  await mkdir(dir, { recursive: true });
1028
+ await rm(join(dir, "landlord"), { recursive: true, force: true });
1369
1029
  let changedFiles = 0;
1370
1030
  if (await writeManifestIfChanged(join(dir, "manifest.json"), manifest))
1371
1031
  changedFiles += 1;
1372
1032
  const outputs = {
1373
1033
  "api.ts": renderAPI(manifest),
1374
- "client.ts": '// Generated by gonvex dev. Do not edit.\nexport { GonvexClient, ConvexReactClient } from "@gonvex/client";\n',
1375
- "react.ts": '// Generated by gonvex dev. Do not edit.\nexport { ConvexProvider, ConvexProviderWithAuth, ConvexReactClient, createGonvexAuth, GonvexAuthProvider, GonvexGoogleAuthButton, GonvexProvider, useAction, useConvex, useConvexAuth, useConvexConnectionState, useGonvexAuth, useMutation, usePaginatedQuery, useQuery, useSync, useSyncSelector } from "@gonvex/react";\nexport type { GonvexAuthConfig, GonvexAuthTenant, GonvexAuthUser, GonvexAuthValue } from "@gonvex/react";\n',
1034
+ "client.ts": '// Generated by gonvex dev. Do not edit.\nexport { GonvexClient } from "@gonvex/client";\n',
1035
+ "react.ts": '// Generated by gonvex dev. Do not edit.\nexport { createGonvexAuth, GonvexAuthProvider, GonvexGoogleAuthButton, GonvexProvider, GonvexProviderWithAuth, useAction, useControlQuery, useCurrentTenantProfile, useEntity, useGonvexAuth, useGonvexAuthState, useGonvexClient, useGonvexConnectionState, useInvitationList, useLiveQuery, useLiveQueryState, useQuery, useReducer, useReplicaCollection, useReplicaCollectionState, useReplicaEntities, useReplicaSelector, useRetainedLiveQuery } from "@gonvex/react";\nexport type { GonvexAuthAccount, GonvexAuthConfig, GonvexAuthProviderName, GonvexAuthTenant, GonvexAuthValue, GonvexDeveloperModeState } from "@gonvex/react";\n',
1376
1036
  "types.ts": "// Generated by gonvex dev. Do not edit.\nexport type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };\n",
1377
1037
  "schema.ts": renderSchemaIndex(manifest),
1378
- "landlord/schema.ts": renderScopedSchemaModule("landlord", manifest.schema.landlordTables),
1379
- "landlord/tables.ts": renderScopedTablesModule("landlord", manifest.schema.landlordTables),
1038
+ "control-plane/schema.ts": renderScopedSchemaModule("control-plane", manifest.schema.controlPlaneTables),
1039
+ "control-plane/tables.ts": renderScopedTablesModule("control-plane", manifest.schema.controlPlaneTables),
1380
1040
  "tenant/schema.ts": renderScopedSchemaModule("tenant", manifest.schema.tenantTables),
1381
1041
  "tenant/tables.ts": renderScopedTablesModule("tenant", manifest.schema.tenantTables),
1042
+ // Module-artifact projects emit the artifact next to the manifest so the
1043
+ // deploy payload is inspectable without re-running codegen.
1044
+ ...(manifest.module ? { "module.json": `${JSON.stringify(manifest.module, null, 2)}\n` } : {}),
1382
1045
  };
1383
1046
  for (const [name, contents] of Object.entries(outputs)) {
1384
1047
  if (await writeFileIfChanged(join(dir, name), contents))
@@ -1463,113 +1126,214 @@ function functionEntryChanged(previous, current, path) {
1463
1126
  const currentEntry = current.functions[path];
1464
1127
  if (JSON.stringify(previousEntry) !== JSON.stringify(currentEntry))
1465
1128
  return true;
1466
- const previousBundleFile = bundleFileForFunction(previousEntry);
1467
- const currentBundleFile = bundleFileForFunction(currentEntry);
1468
- return previous.bundle?.files[previousBundleFile] !== current.bundle?.files[currentBundleFile];
1129
+ return sourceForFunction(previous, previousEntry) !== sourceForFunction(current, currentEntry);
1130
+ }
1131
+ function sourceForFunction(manifest, entry) {
1132
+ return manifest.module.files[normalizedFunctionFile(entry)];
1469
1133
  }
1470
- function bundleFileForFunction(entry) {
1471
- const normalized = entry.file.replace(/\\/g, "/").replace(/^\.?\//, "");
1472
- const withoutGonvexPrefix = normalized.startsWith("gonvex/") ? normalized.slice("gonvex/".length) : normalized;
1473
- return `app/${withoutGonvexPrefix}`;
1134
+ function normalizedFunctionFile(entry) {
1135
+ return entry.file.replace(/\\/g, "/").replace(/^\.?\//, "");
1474
1136
  }
1475
1137
  function renderAPI(manifest) {
1476
- const root = {};
1477
- const optimisticWrites = {};
1478
- const optimisticMutations = {};
1138
+ const publicRoot = {};
1139
+ const internalRoot = {};
1140
+ const optimisticTransactions = {};
1141
+ const functionTypes = [];
1479
1142
  for (const [path, entry] of Object.entries(manifest.functions).sort(([a], [b]) => a.localeCompare(b))) {
1480
1143
  const parts = path.split(".").filter(Boolean);
1481
1144
  if (parts.length === 0)
1482
1145
  continue;
1483
- let target = root;
1146
+ let target = entry.internal ? internalRoot : publicRoot;
1484
1147
  for (const part of parts.slice(0, -1)) {
1485
1148
  target = target[part] ??= {};
1486
1149
  }
1487
- const reference = { kind: entry.kind, path };
1488
- const projection = entry.kind === "sync" && entry.sync
1489
- ? { entity: entry.sync.table, key: entry.sync.key, resultPath: [] }
1490
- : entry.dependencies?.optimisticProjection;
1491
- const mutation = entry.dependencies?.optimisticMutation;
1492
- if (projection || mutation) {
1493
- reference.optimistic = {
1494
- ...(projection ? { projection } : {}),
1495
- ...(mutation ? { mutation } : {}),
1496
- };
1150
+ const reference = {
1151
+ kind: entry.kind,
1152
+ path,
1153
+ __argsType: functionTypeName(path, "Args"),
1154
+ __resultType: functionTypeName(path, "Result"),
1155
+ ...(entry.delivery ? { delivery: entry.delivery } : {}),
1156
+ ...(entry.offline !== undefined ? { offline: entry.offline } : {}),
1157
+ ...(isModuleSchema(entry.args) ? { args: entry.args } : {}),
1158
+ ...(isModuleSchema(entry.result) ? { result: entry.result } : {}),
1159
+ };
1160
+ if (entry.delivery === "live" && entry.dependencies?.liveQueryPlan) {
1161
+ const plan = entry.dependencies.liveQueryPlan;
1162
+ reference.live = { entity: plan.table, key: plan.key, resultPath: plan.resultPath ?? [], plan };
1497
1163
  }
1498
- target[parts[parts.length - 1]] = reference;
1499
- if (entry.kind === "mutation" && entry.dependencies?.writes?.length) {
1500
- optimisticWrites[path] = entry.dependencies.writes;
1164
+ const transaction = entry.kind === "reducer" ? entry.optimistic : undefined;
1165
+ if (transaction !== undefined) {
1166
+ reference.optimistic = { transaction };
1501
1167
  }
1502
- if (entry.kind === "mutation" && mutation)
1503
- optimisticMutations[path] = mutation;
1168
+ target[parts[parts.length - 1]] = reference;
1169
+ if (entry.kind === "reducer" && transaction !== undefined)
1170
+ optimisticTransactions[path] = transaction;
1171
+ functionTypes.push({
1172
+ path,
1173
+ args: functionTypeName(path, "Args"),
1174
+ result: functionTypeName(path, "Result"),
1175
+ });
1504
1176
  }
1505
1177
  const lines = [
1506
1178
  "// Generated by gonvex dev. Do not edit.",
1507
1179
  "",
1508
- `export const api = ${renderObject(root, 0)} as const;`,
1180
+ "import { control as gonvexControl, type LiveQueryPlan } from \"@gonvex/client\";",
1509
1181
  "",
1510
- "export const internal = api;",
1182
+ "export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };",
1183
+ "export type FunctionKind = \"query\" | \"reducer\" | \"action\";",
1184
+ "export type OptimisticID = string | readonly string[];",
1185
+ "export type OptimisticValue = JsonValue | { readonly $arg: string | readonly string[] } | readonly OptimisticValue[] | { readonly [key: string]: OptimisticValue };",
1186
+ "export type OptimisticEffectDefinition =",
1187
+ " | { readonly operation: \"patch\"; readonly entity: string; readonly id: OptimisticID; readonly fields: Readonly<Record<string, OptimisticValue>> }",
1188
+ " | { readonly operation: \"upsert\"; readonly entity: string; readonly id: OptimisticID; readonly value: Readonly<Record<string, OptimisticValue>> }",
1189
+ " | { readonly operation: \"delete\"; readonly entity: string; readonly id: OptimisticID };",
1190
+ "export type OptimisticTransactionDefinition = { readonly effects: readonly OptimisticEffectDefinition[]; readonly expectedRevision?: number };",
1191
+ "export type FunctionReference<Kind extends FunctionKind = FunctionKind, Args = JsonValue, Result = JsonValue> = {",
1192
+ " readonly kind: Kind;",
1193
+ " readonly path: string;",
1194
+ " readonly delivery?: \"oneShot\" | \"live\" | \"replica\";",
1195
+ " readonly offline?: { readonly mode: \"forbidden\" | \"allowed\" | \"onlineOnly\"; readonly conflict?: \"reject\" | \"expectedVersion\" | \"merge\"; readonly reason?: string };",
1196
+ " readonly args?: Args;",
1197
+ " readonly result?: Result;",
1198
+ " readonly live?: { readonly entity: string; readonly key: string; readonly resultPath?: readonly string[]; readonly plan: LiveQueryPlan };",
1199
+ " readonly optimistic?: { readonly transaction?: OptimisticTransactionDefinition };",
1200
+ "};",
1201
+ "",
1202
+ ...functionTypes.flatMap(({ path, args, result }) => [
1203
+ `export type ${args} = ${renderSchemaType(manifest.functions[path]?.args)};`,
1204
+ `export type ${result} = ${renderSchemaType(manifest.functions[path]?.result)};`,
1205
+ ]),
1206
+ "",
1207
+ `export const api = ${renderObject(publicRoot, 0)} as const;`,
1208
+ "",
1209
+ "export const control = gonvexControl;",
1210
+ "",
1211
+ `export const internal = ${renderObject(internalRoot, 0)} as const;`,
1511
1212
  "export type Api = typeof api;",
1512
1213
  "",
1513
- `export const optimisticWrites: Record<string, Array<{ table: string; columns?: string[] }>> = ${renderObject(optimisticWrites, 0)};`,
1514
- `export const optimisticMutations: Record<string, { entity: string; rowIdPath: string[]; fieldsPath: string[] }> = ${renderObject(optimisticMutations, 0)};`,
1214
+ `export const optimisticTransactions: Record<string, OptimisticTransactionDefinition> = ${renderObject(optimisticTransactions, 0)};`,
1215
+ "",
1216
+ "export type ApiArgs = {",
1217
+ ...functionTypes.map(({ path, args }) => ` ${JSON.stringify(path)}: ${args};`),
1218
+ "};",
1219
+ "",
1220
+ "export type ApiResults = {",
1221
+ ...functionTypes.map(({ path, result }) => ` ${JSON.stringify(path)}: ${result};`),
1222
+ "};",
1515
1223
  "",
1516
1224
  "export function optimisticPatchesFor(",
1517
1225
  " path: string,",
1518
1226
  " args: Record<string, unknown>,",
1519
- "): Array<{ entity?: string; collection?: string; rowId: string; op: \"patch\"; fields: Record<string, unknown> }> {",
1520
- " const mutation = Object.prototype.hasOwnProperty.call(optimisticMutations, path)",
1521
- " ? optimisticMutations[path]",
1227
+ "): Array<{ entity?: string; collection?: string; rowId: string; op: \"patch\" | \"insert\" | \"upsert\" | \"delete\"; fields?: Record<string, unknown> }> {",
1228
+ " const transaction = Object.prototype.hasOwnProperty.call(optimisticTransactions, path)",
1229
+ " ? optimisticTransactions[path]",
1522
1230
  " : undefined;",
1523
- " if (mutation) {",
1524
- " const readPath = (value: unknown, segments: string[]): unknown =>",
1231
+ " if (transaction && Array.isArray(transaction.effects) && transaction.effects.length > 0) {",
1232
+ " const readPath = (value: unknown, segments: readonly string[]): unknown =>",
1525
1233
  " segments.reduce<unknown>((current, segment) =>",
1526
1234
  " current && typeof current === \"object\" ? (current as Record<string, unknown>)[segment] : undefined, value);",
1527
- " const rowId = String(readPath(args, mutation.rowIdPath) ?? args.id ?? args._id ?? \"\");",
1528
- " const nested = readPath(args, mutation.fieldsPath);",
1529
- " if (rowId && nested && typeof nested === \"object\" && !Array.isArray(nested)) {",
1530
- " return [{ entity: mutation.entity, rowId, op: \"patch\" as const, fields: { ...(nested as Record<string, unknown>) } }];",
1235
+ " const resolveValue = (value: unknown): unknown => {",
1236
+ " if (Array.isArray(value)) return value.map(resolveValue);",
1237
+ " if (!value || typeof value !== \"object\") return value;",
1238
+ " const record = value as Record<string, unknown>;",
1239
+ " if (Object.keys(record).length === 1 && Object.prototype.hasOwnProperty.call(record, \"$arg\")) {",
1240
+ " const path = record.$arg;",
1241
+ " const segments = Array.isArray(path)",
1242
+ " ? path.filter((part): part is string => typeof part === \"string\" && part.trim().length > 0)",
1243
+ " : typeof path === \"string\" ? path.split(\".\").map((part) => part.trim()).filter(Boolean) : [];",
1244
+ " return segments.length > 0 ? readPath(args, segments) : undefined;",
1245
+ " }",
1246
+ " return Object.fromEntries(Object.entries(record).map(([key, item]) => [key, resolveValue(item)]));",
1247
+ " };",
1248
+ " const containsUndefined = (value: unknown): boolean =>",
1249
+ " value === undefined || (Array.isArray(value)",
1250
+ " ? value.some(containsUndefined)",
1251
+ " : !!value && typeof value === \"object\" && Object.values(value as Record<string, unknown>).some(containsUndefined));",
1252
+ " const patches: Array<{ entity?: string; collection?: string; rowId: string; op: \"patch\" | \"insert\" | \"upsert\" | \"delete\"; fields?: Record<string, unknown> }> = [];",
1253
+ " for (const effect of transaction.effects) {",
1254
+ " const rawId = Array.isArray(effect.id) ? readPath(args, effect.id) : effect.id;",
1255
+ " const rowId = String(rawId ?? \"\").trim();",
1256
+ " if (!rowId || !effect.entity) return [];",
1257
+ " if (effect.operation === \"delete\") {",
1258
+ " patches.push({ entity: effect.entity, rowId, op: \"delete\" });",
1259
+ " continue;",
1260
+ " }",
1261
+ " const template = effect.operation === \"patch\" ? effect.fields : effect.value;",
1262
+ " if (!template || typeof template !== \"object\" || Array.isArray(template)) return [];",
1263
+ " const fields = resolveValue(template);",
1264
+ " if (!fields || typeof fields !== \"object\" || Array.isArray(fields) || containsUndefined(fields)) return [];",
1265
+ " patches.push({ entity: effect.entity, rowId, op: effect.operation === \"upsert\" ? \"upsert\" : \"patch\", fields: { ...(fields as Record<string, unknown>) } });",
1531
1266
  " }",
1267
+ " return patches;",
1532
1268
  " }",
1533
- " const writes = optimisticWrites[path];",
1534
- " const rowId = String(args.id ?? args._id ?? \"\");",
1535
- " if (!Array.isArray(writes) || rowId === \"\") return [];",
1536
- "",
1537
- " return writes.map(({ table, columns }) => ({",
1538
- " collection: table,",
1539
- " rowId,",
1540
- " op: \"patch\" as const,",
1541
- " fields: Object.fromEntries(",
1542
- " Object.entries(args).filter(([key]) =>",
1543
- " columns",
1544
- " ? columns.includes(key)",
1545
- " : key !== \"id\" && key !== \"_id\" && key !== \"tenantId\",",
1546
- " ),",
1547
- " ),",
1548
- " }));",
1269
+ " return [];",
1549
1270
  "}",
1550
1271
  "",
1551
1272
  ];
1552
1273
  return lines.join("\n");
1553
1274
  }
1275
+ function functionTypeName(path, suffix) {
1276
+ const parts = path.split(/[^A-Za-z0-9_$]+/).filter(Boolean);
1277
+ const stem = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Function";
1278
+ return `${/^[0-9]/.test(stem) ? "Fn" : ""}${stem}${suffix}`;
1279
+ }
1280
+ function renderSchemaType(schema) {
1281
+ if (!schema)
1282
+ return "JsonValue";
1283
+ switch (schema.kind) {
1284
+ case "string": return "string";
1285
+ case "number": return "number";
1286
+ case "boolean": return "boolean";
1287
+ case "null": return "null";
1288
+ case "any": return "JsonValue";
1289
+ case "id": return "string";
1290
+ case "literal": return renderLiteralType(schema.value);
1291
+ case "array": return `Array<${renderSchemaType(schema.items)}>`;
1292
+ case "record": return `Record<string, ${renderSchemaType(schema.values)}>`;
1293
+ case "optional": return `${renderSchemaType(schema.value)} | undefined`;
1294
+ case "object": {
1295
+ const fields = Object.entries(schema.fields).map(([key, value]) => {
1296
+ const property = /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
1297
+ const optional = value.kind === "optional" ? "?" : "";
1298
+ return ` ${property}${optional}: ${renderSchemaType(value)};`;
1299
+ });
1300
+ if (schema.allowUnknown)
1301
+ fields.push(" [key: string]: JsonValue;");
1302
+ return fields.length === 0 ? "{ }" : `{\n${fields.join("\n")}\n}`;
1303
+ }
1304
+ }
1305
+ }
1306
+ function renderLiteralType(value) {
1307
+ if (value === null)
1308
+ return "null";
1309
+ if (typeof value === "string")
1310
+ return JSON.stringify(value);
1311
+ if (typeof value === "boolean" || typeof value === "number")
1312
+ return JSON.stringify(value);
1313
+ if (Array.isArray(value))
1314
+ return `[${value.map(renderLiteralType).join(", ")}]`;
1315
+ const fields = Object.entries(value).map(([key, nested]) => `${JSON.stringify(key)}: ${renderLiteralType(nested)}`);
1316
+ return fields.length === 0 ? "{ }" : `{ ${fields.join(", ")} }`;
1317
+ }
1554
1318
  function renderSchemaIndex(manifest) {
1555
- const landlord = renderSchemaObject("landlord", manifest.schema.landlordTables, 0);
1319
+ const controlPlane = renderSchemaObject("control-plane", manifest.schema.controlPlaneTables, 0);
1556
1320
  const tenant = renderSchemaObject("tenant", manifest.schema.tenantTables, 0);
1557
1321
  return [
1558
1322
  "// Generated by gonvex dev. Do not edit.",
1559
1323
  "",
1560
- `export const landlord = ${landlord} as const;`,
1324
+ `export const controlPlane = ${controlPlane} as const;`,
1561
1325
  "",
1562
1326
  `export const tenant = ${tenant} as const;`,
1563
1327
  "",
1564
1328
  "export const tables = tenant.tables;",
1565
1329
  "",
1566
1330
  "export const schema = {",
1567
- " landlord,",
1331
+ " controlPlane,",
1568
1332
  " tenant,",
1569
1333
  " tables,",
1570
1334
  "} as const;",
1571
1335
  "",
1572
- "export type LandlordTableName = keyof typeof landlord.tables;",
1336
+ "export type ControlPlaneTableName = keyof typeof controlPlane.tables;",
1573
1337
  "export type TenantTableName = keyof typeof tenant.tables;",
1574
1338
  "export type TableName = TenantTableName;",
1575
1339
  "",
@@ -1581,6 +1345,7 @@ function renderScopedSchemaModule(scope, tables) {
1581
1345
  "",
1582
1346
  `export const schema = ${renderSchemaObject(scope, tables, 0)} as const;`,
1583
1347
  "",
1348
+ ...(scope === "control-plane" ? ["export const controlPlane = schema;"] : ["export const tenant = schema;"]),
1584
1349
  "export const tables = schema.tables;",
1585
1350
  "",
1586
1351
  "export type TableName = keyof typeof tables;",
@@ -1607,7 +1372,8 @@ function renderObject(value, depth) {
1607
1372
  return JSON.stringify(value);
1608
1373
  const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
1609
1374
  if (isFunctionRef(value)) {
1610
- return `{ kind: ${JSON.stringify(value.kind)}, path: ${JSON.stringify(value.path)} }`;
1375
+ const visible = Object.fromEntries(entries.filter(([key]) => !key.startsWith("__")));
1376
+ return `${renderObject(visible, depth)} as unknown as FunctionReference<${JSON.stringify(value.kind)}, ${value.__argsType}, ${value.__resultType}>`;
1611
1377
  }
1612
1378
  const indent = " ".repeat(depth);
1613
1379
  const childIndent = " ".repeat(depth + 1);
@@ -1619,7 +1385,10 @@ function renderObject(value, depth) {
1619
1385
  return lines.join("\n");
1620
1386
  }
1621
1387
  function isFunctionRef(value) {
1622
- return typeof value.kind === "string" && typeof value.path === "string" && Object.keys(value).length === 2;
1388
+ return typeof value.kind === "string"
1389
+ && typeof value.path === "string"
1390
+ && typeof value.__argsType === "string"
1391
+ && typeof value.__resultType === "string";
1623
1392
  }
1624
1393
  function propertyKey(key) {
1625
1394
  return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : JSON.stringify(key);
@@ -1627,12 +1396,12 @@ function propertyKey(key) {
1627
1396
  function emptySchemaDefinition() {
1628
1397
  return {
1629
1398
  tables: {},
1630
- landlordTables: {},
1399
+ controlPlaneTables: {},
1631
1400
  tenantTables: {},
1632
1401
  };
1633
1402
  }
1634
1403
  function mergeSchemaDefinition(target, source) {
1635
- Object.assign(target.landlordTables, source.landlordTables);
1404
+ Object.assign(target.controlPlaneTables, source.controlPlaneTables);
1636
1405
  Object.assign(target.tenantTables, source.tenantTables);
1637
1406
  target.tables = target.tenantTables;
1638
1407
  }
@@ -1648,6 +1417,8 @@ async function syncRuntime(settings, manifest) {
1648
1417
  });
1649
1418
  if (!response.ok)
1650
1419
  throw new Error(`runtime returned ${response.status} ${response.statusText}: ${await response.text()}`);
1420
+ const payload = await response.json();
1421
+ return payload.schemaDefinition;
1651
1422
  }
1652
1423
  async function fetchProjectEnv(settings) {
1653
1424
  const response = await fetch(projectEnvURL(settings), {
@@ -1769,9 +1540,12 @@ async function runtimeHasManifest(settings, manifest) {
1769
1540
  return false;
1770
1541
  const current = await response.json();
1771
1542
  return current.project === manifest.project
1772
- && current.bundle?.hash === manifest.bundle?.hash
1543
+ && deployedSourceHash(current) === deployedSourceHash(manifest)
1773
1544
  && Object.keys(current.functions ?? {}).length === Object.keys(manifest.functions ?? {}).length;
1774
1545
  }
1546
+ function deployedSourceHash(manifest) {
1547
+ return manifest.module.hash;
1548
+ }
1775
1549
  async function ensureProjectSettings(root, settings, options) {
1776
1550
  if (settings.key)
1777
1551
  return settings;
@@ -2229,24 +2003,6 @@ function loadDotEnv(path) {
2229
2003
  function readFileSyncText(path) {
2230
2004
  return existsSync(path) ? readFileSync(path, "utf8") : "";
2231
2005
  }
2232
- async function goFiles(root) {
2233
- if (!existsSync(root))
2234
- return [];
2235
- const entries = await readdir(root, { withFileTypes: true });
2236
- const files = [];
2237
- for (const entry of entries) {
2238
- const path = join(root, entry.name);
2239
- if (entry.isDirectory()) {
2240
- if (entry.name === "_generated")
2241
- continue;
2242
- files.push(...await goFiles(path));
2243
- }
2244
- else if (entry.isFile() && entry.name.endsWith(".go")) {
2245
- files.push(path);
2246
- }
2247
- }
2248
- return files.sort();
2249
- }
2250
2006
  async function migrationFiles(root) {
2251
2007
  if (!existsSync(root))
2252
2008
  return [];
@@ -2280,6 +2036,8 @@ async function copyTemplate(template, target, options = {}) {
2280
2036
  async function copyDir(source, target, overwrite) {
2281
2037
  await mkdir(target, { recursive: true });
2282
2038
  for (const entry of await readdir(source, { withFileTypes: true })) {
2039
+ if (entry.name === "node_modules" || entry.name === "dist" || entry.name === "_build")
2040
+ continue;
2283
2041
  const sourcePath = join(source, entry.name);
2284
2042
  const targetPath = join(target, entry.name === "_gitignore" ? ".gitignore" : entry.name);
2285
2043
  if (entry.isDirectory()) {
@@ -2329,45 +2087,6 @@ function templateDir(template) {
2329
2087
  return packageTemplate;
2330
2088
  return resolve(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "templates", template);
2331
2089
  }
2332
- function functionKind(raw) {
2333
- if (raw === "InternalMutation")
2334
- return "internalMutation";
2335
- if (raw === "LiveGrid")
2336
- return "liveGrid";
2337
- if (raw === "Sync")
2338
- return "sync";
2339
- if (raw === "PublicHTTP")
2340
- return "http";
2341
- return raw.toLowerCase();
2342
- }
2343
- function columnType(kind) {
2344
- if (kind === "ID")
2345
- return "id";
2346
- if (kind === "Int64")
2347
- return "int64";
2348
- if (kind === "Float64")
2349
- return "float64";
2350
- return kind.toLowerCase();
2351
- }
2352
- function stringArgs(input) {
2353
- const values = [];
2354
- for (const match of input.matchAll(/"((?:\\.|[^"\\])*)"|`([^`]*)`/g)) {
2355
- if (match[2] !== undefined) {
2356
- values.push(match[2]);
2357
- continue;
2358
- }
2359
- try {
2360
- values.push(JSON.parse(`"${match[1] ?? ""}"`));
2361
- }
2362
- catch {
2363
- values.push(match[1] ?? "");
2364
- }
2365
- }
2366
- return values;
2367
- }
2368
- function cleanParsedOptimisticPath(value) {
2369
- return value.split(".").map((segment) => segment.trim()).filter(Boolean);
2370
- }
2371
2090
  function valueFor(args, key) {
2372
2091
  const index = args.indexOf(key);
2373
2092
  if (index === -1)
@@ -2430,13 +2149,13 @@ function printAuthHelp() {
2430
2149
  console.log(" gonvex auth remove google [--origin URL]... [--callback-path /]");
2431
2150
  console.log(" gonvex auth status [--json]");
2432
2151
  console.log(" gonvex auth doctor [--json]");
2433
- console.log(" gonvex auth users [--json]");
2152
+ console.log(" gonvex auth accounts [--json]");
2434
2153
  console.log(" gonvex auth tenants list [--json]");
2435
2154
  console.log(" gonvex auth tenants create <name> [--owner <email>]");
2436
2155
  console.log(" gonvex auth memberships list --tenant <tenant-id> [--json]");
2437
2156
  console.log(" gonvex auth memberships add --tenant <tenant-id> --email <email> [--role member]");
2438
- console.log(" gonvex auth memberships remove --tenant <tenant-id> (--user <user-id> | --email <invited-email>)");
2439
- console.log(" gonvex auth user <disable|enable|delete> <user-id>");
2157
+ console.log(" gonvex auth memberships remove --tenant <tenant-id> (--member <member-id> | --email <invited-email>)");
2158
+ console.log(" gonvex auth account <disable|enable|delete> <account-id>");
2440
2159
  }
2441
2160
  function printEnvHelp() {
2442
2161
  console.log("Usage: gonvex env <command> [options]");