@gonvex/cli 0.1.32 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -15
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +1 -1
- package/dist/browser.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +274 -555
- package/dist/index.js.map +1 -1
- package/dist/manifest-types.d.ts +261 -0
- package/dist/manifest-types.js +3 -0
- package/dist/manifest-types.js.map +1 -0
- package/dist/module-artifact.d.ts +26 -0
- package/dist/module-artifact.js +1585 -0
- package/dist/module-artifact.js.map +1 -0
- package/dist/react.d.ts +2 -2
- package/dist/react.js +1 -1
- package/dist/react.js.map +1 -1
- package/dist/templates/vite-react/README.md +5 -2
- package/dist/templates/vite-react/gonvex/_build/module.js +407 -0
- package/dist/templates/vite-react/gonvex/_generated/api.ts +185 -3
- package/dist/templates/vite-react/gonvex/_generated/client.ts +1 -1
- package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/schema.ts +2 -1
- package/dist/templates/vite-react/gonvex/_generated/{landlord → control-plane}/tables.ts +1 -1
- package/dist/templates/vite-react/gonvex/_generated/manifest.json +201 -66
- package/dist/templates/vite-react/gonvex/_generated/module.json +120 -0
- package/dist/templates/vite-react/gonvex/_generated/react.ts +2 -2
- package/dist/templates/vite-react/gonvex/_generated/schema.ts +4 -34
- package/dist/templates/vite-react/gonvex/_generated/tenant/schema.ts +1 -30
- package/dist/templates/vite-react/gonvex/_generated/tenant/tables.ts +0 -30
- package/dist/templates/vite-react/gonvex/index.ts +1 -0
- package/dist/templates/vite-react/gonvex/messages.ts +47 -0
- package/dist/templates/vite-react/migrations/0001_messages.sql +9 -0
- package/dist/templates/vite-react/package.json +1 -0
- package/dist/templates/vite-react/src/App.tsx +5 -5
- package/package.json +4 -3
- package/dist/templates/vite-react/gonvex/messages.go +0 -38
- 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
|
|
444
|
-
const manifest = await buildManifest(projectRoot,
|
|
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)
|
|
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", "--
|
|
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 === "
|
|
659
|
-
const
|
|
660
|
-
const payload = await runtimeJSON(await fetch(
|
|
661
|
-
const
|
|
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(
|
|
668
|
+
console.log(JSON.stringify(accounts, null, 2));
|
|
664
669
|
return;
|
|
665
670
|
}
|
|
666
|
-
if (
|
|
671
|
+
if (accounts.length === 0) {
|
|
667
672
|
console.log(`[gonvex] no app accounts for ${settings.projectID}`);
|
|
668
673
|
return;
|
|
669
674
|
}
|
|
670
|
-
for (const
|
|
671
|
-
console.log(`${
|
|
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.
|
|
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
|
|
737
|
+
const member = valueFor(argv, "--member");
|
|
733
738
|
const email = valueFor(argv, "--email");
|
|
734
|
-
if (!
|
|
735
|
-
throw new Error("--
|
|
736
|
-
const target =
|
|
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(
|
|
741
|
-
? `[gonvex] removed ${
|
|
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 === "
|
|
752
|
+
if (action === "account") {
|
|
748
753
|
const operation = positional[1];
|
|
749
|
-
const
|
|
750
|
-
if (!operation || !
|
|
751
|
-
throw new Error("usage: gonvex auth
|
|
752
|
-
const
|
|
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(
|
|
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 ${
|
|
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(
|
|
763
|
-
console.log(`[gonvex] deleted app account ${
|
|
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
|
|
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 {
|
|
871
|
-
const appStart = 'export default function App(props: { runtimeURL: string }) {\n const messages =
|
|
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 =
|
|
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.
|
|
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
|
|
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
|
|
911
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
functions,
|
|
989
|
-
schema,
|
|
990
|
-
bundle,
|
|
1003
|
+
moduleFiles: module,
|
|
1004
|
+
config,
|
|
991
1005
|
};
|
|
992
1006
|
}
|
|
993
|
-
async function
|
|
994
|
-
const
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
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
|
|
1375
|
-
"react.ts": '// Generated by gonvex dev. Do not edit.\nexport {
|
|
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 } 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
|
-
"
|
|
1379
|
-
"
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
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
|
|
1471
|
-
|
|
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
|
|
1477
|
-
const
|
|
1478
|
-
const
|
|
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 =
|
|
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 = {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
:
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
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
|
-
|
|
1499
|
-
if (
|
|
1500
|
-
|
|
1164
|
+
const transaction = entry.kind === "reducer" ? entry.optimistic : undefined;
|
|
1165
|
+
if (transaction !== undefined) {
|
|
1166
|
+
reference.optimistic = { transaction };
|
|
1501
1167
|
}
|
|
1502
|
-
|
|
1503
|
-
|
|
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
|
-
|
|
1180
|
+
"import { control as gonvexControl, type LiveQueryPlan } from \"@gonvex/client\";",
|
|
1509
1181
|
"",
|
|
1510
|
-
"export
|
|
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
|
|
1514
|
-
|
|
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
|
|
1520
|
-
" const
|
|
1521
|
-
" ?
|
|
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 (
|
|
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
|
|
1528
|
-
"
|
|
1529
|
-
"
|
|
1530
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
"
|
|
1331
|
+
" controlPlane,",
|
|
1568
1332
|
" tenant,",
|
|
1569
1333
|
" tables,",
|
|
1570
1334
|
"} as const;",
|
|
1571
1335
|
"",
|
|
1572
|
-
"export type
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
1399
|
+
controlPlaneTables: {},
|
|
1631
1400
|
tenantTables: {},
|
|
1632
1401
|
};
|
|
1633
1402
|
}
|
|
1634
1403
|
function mergeSchemaDefinition(target, source) {
|
|
1635
|
-
Object.assign(target.
|
|
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
|
|
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
|
|
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> (--
|
|
2439
|
-
console.log(" gonvex auth
|
|
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]");
|