@ollie-shop/cli 1.2.1 → 1.3.3
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/.env.example +9 -5
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +61 -0
- package/CONTEXT.md +11 -6
- package/README.md +20 -10
- package/dist/index.js +451 -48
- package/package.json +1 -1
- package/src/cli.tsx +8 -1
- package/src/commands/function-cmd.ts +178 -0
- package/src/commands/help.tsx +14 -1
- package/src/commands/start.tsx +8 -18
- package/src/commands/store-cmd.ts +14 -4
- package/src/commands/whoami.ts +70 -9
- package/src/core/function.ts +92 -0
- package/src/core/schema.ts +25 -4
- package/src/core/store.ts +50 -3
- package/src/index.tsx +2 -0
- package/src/utils/auth.ts +4 -0
- package/src/utils/esbuild.ts +99 -15
- package/src/utils/parse-args.ts +2 -0
- package/src/utils/supabase.ts +66 -11
- package/src/utils/validate.ts +44 -0
- package/tsup.config.ts +7 -0
package/package.json
CHANGED
package/src/cli.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import { HelpCommand } from "./commands/help.js";
|
|
|
3
3
|
import { LoginCommand } from "./commands/login.js";
|
|
4
4
|
import { StartCommand } from "./commands/start.js";
|
|
5
5
|
|
|
6
|
+
// Inlined by tsup at build time from package.json (see tsup.config.ts).
|
|
7
|
+
declare const __OLLIE_CLI_VERSION__: string;
|
|
8
|
+
|
|
6
9
|
interface AppProps {
|
|
7
10
|
command: string;
|
|
8
11
|
args: string[];
|
|
@@ -28,9 +31,13 @@ export function App({ command, args }: AppProps) {
|
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function VersionCommand() {
|
|
34
|
+
const version =
|
|
35
|
+
typeof __OLLIE_CLI_VERSION__ === "string" && __OLLIE_CLI_VERSION__
|
|
36
|
+
? __OLLIE_CLI_VERSION__
|
|
37
|
+
: "unknown";
|
|
31
38
|
return (
|
|
32
39
|
<Box>
|
|
33
|
-
<Text>
|
|
40
|
+
<Text>ollieshop v{version}</Text>
|
|
34
41
|
</Box>
|
|
35
42
|
);
|
|
36
43
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { createFunction, listFunctions } from "../core/function.js";
|
|
2
|
+
import { functionCreateSchema } from "../core/schema.js";
|
|
3
|
+
import {
|
|
4
|
+
detectOutputFormat,
|
|
5
|
+
outputDryRun,
|
|
6
|
+
outputResult,
|
|
7
|
+
} from "../utils/output.js";
|
|
8
|
+
import { type ParsedArgs, getBoolFlag, getFlag } from "../utils/parse-args.js";
|
|
9
|
+
import { getAuthenticatedClient } from "../utils/supabase.js";
|
|
10
|
+
import {
|
|
11
|
+
validateEnum,
|
|
12
|
+
validateInteger,
|
|
13
|
+
validateRequired,
|
|
14
|
+
validateTriggerUrl,
|
|
15
|
+
validateUuid,
|
|
16
|
+
} from "../utils/validate.js";
|
|
17
|
+
|
|
18
|
+
const ON_ERROR_VALUES = ["throw", "skip"] as const;
|
|
19
|
+
|
|
20
|
+
function parseTriggerFromFlags(
|
|
21
|
+
urlFlag: string | undefined,
|
|
22
|
+
expressionFlag: string | undefined,
|
|
23
|
+
): { url: string; expression: string } | undefined {
|
|
24
|
+
if (urlFlag === undefined && expressionFlag === undefined) return undefined;
|
|
25
|
+
if (urlFlag === undefined || expressionFlag === undefined) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Both --trigger-url and --trigger-expression are required when configuring a trigger.",
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
url: validateTriggerUrl(urlFlag, "trigger-url"),
|
|
32
|
+
expression: validateRequired(expressionFlag, "trigger-expression"),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseTriggerFromData(
|
|
37
|
+
raw: unknown,
|
|
38
|
+
): { url: string; expression: string } | undefined {
|
|
39
|
+
if (raw === undefined || raw === null) return undefined;
|
|
40
|
+
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
41
|
+
throw new Error(
|
|
42
|
+
'Invalid trigger: must be an object with "url" and "expression" string properties.',
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const obj = raw as Record<string, unknown>;
|
|
46
|
+
if (typeof obj.url !== "string" || typeof obj.expression !== "string") {
|
|
47
|
+
throw new Error(
|
|
48
|
+
'Invalid trigger: both "url" and "expression" must be non-empty strings.',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
url: validateTriggerUrl(obj.url, "trigger.url"),
|
|
53
|
+
expression: validateRequired(obj.expression, "trigger.expression"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function functionCommand(parsed: ParsedArgs): Promise<void> {
|
|
58
|
+
const sub = parsed.subcommand;
|
|
59
|
+
if (sub === "create") return functionCreateCommand(parsed);
|
|
60
|
+
if (sub === "list" || sub === "ls") return functionListCommand(parsed);
|
|
61
|
+
|
|
62
|
+
console.error(
|
|
63
|
+
`Unknown function subcommand: ${sub}. Use: function create | function list`,
|
|
64
|
+
);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
69
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
let input: {
|
|
73
|
+
versionId: string;
|
|
74
|
+
name: string;
|
|
75
|
+
trigger?: { url: string; expression: string };
|
|
76
|
+
active?: boolean;
|
|
77
|
+
onError?: "throw" | "skip";
|
|
78
|
+
priority?: number;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (parsed.global.data) {
|
|
82
|
+
const raw = JSON.parse(parsed.global.data);
|
|
83
|
+
input = {
|
|
84
|
+
versionId: validateUuid(raw.versionId, "versionId"),
|
|
85
|
+
name: validateRequired(raw.name, "name"),
|
|
86
|
+
trigger: parseTriggerFromData(raw.trigger),
|
|
87
|
+
active: raw.active === true,
|
|
88
|
+
onError:
|
|
89
|
+
raw.onError !== undefined && raw.onError !== null
|
|
90
|
+
? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error")
|
|
91
|
+
: "throw",
|
|
92
|
+
priority:
|
|
93
|
+
raw.priority !== undefined && raw.priority !== null
|
|
94
|
+
? validateInteger(raw.priority, "priority", { min: 0 })
|
|
95
|
+
: 0,
|
|
96
|
+
};
|
|
97
|
+
} else {
|
|
98
|
+
const triggerUrlFlag = getFlag(parsed.flags, "trigger-url");
|
|
99
|
+
const triggerExpressionFlag = getFlag(parsed.flags, "trigger-expression");
|
|
100
|
+
const onErrorFlag = getFlag(parsed.flags, "on-error");
|
|
101
|
+
const priorityFlag = getFlag(parsed.flags, "priority");
|
|
102
|
+
input = {
|
|
103
|
+
versionId: validateUuid(
|
|
104
|
+
validateRequired(getFlag(parsed.flags, "version-id"), "version-id"),
|
|
105
|
+
"version-id",
|
|
106
|
+
),
|
|
107
|
+
name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
|
|
108
|
+
trigger: parseTriggerFromFlags(triggerUrlFlag, triggerExpressionFlag),
|
|
109
|
+
active: getBoolFlag(parsed.flags, "active"),
|
|
110
|
+
onError:
|
|
111
|
+
onErrorFlag !== undefined
|
|
112
|
+
? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error")
|
|
113
|
+
: "throw",
|
|
114
|
+
priority:
|
|
115
|
+
priorityFlag !== undefined
|
|
116
|
+
? validateInteger(priorityFlag, "priority", { min: 0 })
|
|
117
|
+
: 0,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const validated = functionCreateSchema.safeParse(input);
|
|
122
|
+
if (!validated.success) {
|
|
123
|
+
throw new Error(validated.error.issues.map((i) => i.message).join("; "));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (parsed.global.dryRun) {
|
|
127
|
+
outputDryRun(
|
|
128
|
+
"function.create",
|
|
129
|
+
validated.data as Record<string, unknown>,
|
|
130
|
+
format,
|
|
131
|
+
);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const client = await getAuthenticatedClient();
|
|
136
|
+
const result = await createFunction(client, validated.data);
|
|
137
|
+
|
|
138
|
+
outputResult(result, format, parsed.global.fields);
|
|
139
|
+
if (!result.success) process.exit(1);
|
|
140
|
+
} catch (err) {
|
|
141
|
+
outputResult(
|
|
142
|
+
{
|
|
143
|
+
success: false,
|
|
144
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
145
|
+
},
|
|
146
|
+
format,
|
|
147
|
+
);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function functionListCommand(parsed: ParsedArgs): Promise<void> {
|
|
153
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
const storeId = validateUuid(
|
|
157
|
+
validateRequired(getFlag(parsed.flags, "store-id"), "store-id"),
|
|
158
|
+
"store-id",
|
|
159
|
+
);
|
|
160
|
+
const versionId = getFlag(parsed.flags, "version-id");
|
|
161
|
+
if (versionId) validateUuid(versionId, "version-id");
|
|
162
|
+
|
|
163
|
+
const client = await getAuthenticatedClient();
|
|
164
|
+
const result = await listFunctions(client, storeId, versionId);
|
|
165
|
+
|
|
166
|
+
outputResult(result, format, parsed.global.fields);
|
|
167
|
+
if (!result.success) process.exit(1);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
outputResult(
|
|
170
|
+
{
|
|
171
|
+
success: false,
|
|
172
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
173
|
+
},
|
|
174
|
+
format,
|
|
175
|
+
);
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
package/src/commands/help.tsx
CHANGED
|
@@ -41,7 +41,7 @@ export function HelpCommand() {
|
|
|
41
41
|
<Box width={24}>
|
|
42
42
|
<Text color="green">whoami</Text>
|
|
43
43
|
</Box>
|
|
44
|
-
<Text>Show current user and
|
|
44
|
+
<Text>Show current user (and linked store/org from ollie.json)</Text>
|
|
45
45
|
</Box>
|
|
46
46
|
<Box>
|
|
47
47
|
<Box width={24}>
|
|
@@ -61,6 +61,12 @@ export function HelpCommand() {
|
|
|
61
61
|
</Box>
|
|
62
62
|
<Text>Create or list components</Text>
|
|
63
63
|
</Box>
|
|
64
|
+
<Box>
|
|
65
|
+
<Box width={24}>
|
|
66
|
+
<Text color="green">function create|list</Text>
|
|
67
|
+
</Box>
|
|
68
|
+
<Text>Create or list functions</Text>
|
|
69
|
+
</Box>
|
|
64
70
|
<Box>
|
|
65
71
|
<Box width={24}>
|
|
66
72
|
<Text color="green">deploy</Text>
|
|
@@ -147,6 +153,10 @@ export function HelpCommand() {
|
|
|
147
153
|
$ ollieshop store create --name "My Store" --platform vtex
|
|
148
154
|
--platform-store-id mystore
|
|
149
155
|
</Text>
|
|
156
|
+
<Text dimColor>
|
|
157
|
+
$ ollieshop store create --org UUID --name "My Store" --platform vtex
|
|
158
|
+
--platform-store-id mystore
|
|
159
|
+
</Text>
|
|
150
160
|
<Text dimColor>
|
|
151
161
|
$ ollieshop store create --data{" "}
|
|
152
162
|
{
|
|
@@ -158,6 +168,9 @@ export function HelpCommand() {
|
|
|
158
168
|
$ ollieshop version create --store-id UUID --name v1 --active
|
|
159
169
|
</Text>
|
|
160
170
|
<Text dimColor>$ ollieshop init --store-id UUID --version-id UUID</Text>
|
|
171
|
+
<Text dimColor>
|
|
172
|
+
$ ollieshop function create --version-id UUID --name myHook
|
|
173
|
+
</Text>
|
|
161
174
|
<Text dimColor>
|
|
162
175
|
$ ollieshop deploy --component-id UUID --name FreeShippingBar --wait
|
|
163
176
|
</Text>
|
package/src/commands/start.tsx
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ServeOnRequestArgs } from "esbuild";
|
|
2
2
|
import { Box, Text, useApp, useInput } from "ink";
|
|
3
3
|
import open from "open";
|
|
4
4
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
6
6
|
import {
|
|
7
7
|
type ComponentInfo,
|
|
8
|
-
createBuildContext,
|
|
9
8
|
discoverComponents,
|
|
10
9
|
startDevServer,
|
|
11
10
|
} from "../utils/esbuild.js";
|
|
@@ -55,7 +54,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
55
54
|
const [buildCount, setBuildCount] = useState(0);
|
|
56
55
|
const [lastBuildTime, setLastBuildTime] = useState<Date | null>(null);
|
|
57
56
|
const logIdRef = useRef(0);
|
|
58
|
-
const
|
|
57
|
+
const rebuildRef = useRef<(() => Promise<void>) | null>(null);
|
|
59
58
|
const stopRef = useRef<(() => Promise<void>) | null>(null);
|
|
60
59
|
|
|
61
60
|
// Parse args
|
|
@@ -121,28 +120,19 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
121
120
|
setComponents(found);
|
|
122
121
|
setState({ status: "building" });
|
|
123
122
|
|
|
124
|
-
//
|
|
125
|
-
const
|
|
123
|
+
// Start dev server
|
|
124
|
+
const server = await startDevServer({
|
|
125
|
+
port: PORT,
|
|
126
126
|
stage,
|
|
127
|
+
onRequest: handleRequest,
|
|
127
128
|
onBuildEnd: (updatedComponents) => {
|
|
128
129
|
setComponents(updatedComponents);
|
|
129
130
|
setBuildCount((c) => c + 1);
|
|
130
131
|
setLastBuildTime(new Date());
|
|
131
132
|
},
|
|
132
133
|
});
|
|
133
|
-
ctxRef.current = ctx;
|
|
134
|
-
|
|
135
|
-
// Do initial build (manifest is written by the plugin)
|
|
136
|
-
await ctx.rebuild();
|
|
137
|
-
|
|
138
|
-
if (!mounted) return;
|
|
139
|
-
|
|
140
|
-
// Start dev server
|
|
141
|
-
const server = await startDevServer(ctx, {
|
|
142
|
-
port: PORT,
|
|
143
|
-
onRequest: handleRequest,
|
|
144
|
-
});
|
|
145
134
|
|
|
135
|
+
rebuildRef.current = server.rebuild;
|
|
146
136
|
stopRef.current = server.stop;
|
|
147
137
|
|
|
148
138
|
if (!mounted) {
|
|
@@ -190,7 +180,7 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
190
180
|
|
|
191
181
|
// Manual rebuild with 'r' (manifest is updated by the plugin)
|
|
192
182
|
if (input === "r" && state.status === "running") {
|
|
193
|
-
|
|
183
|
+
rebuildRef.current?.();
|
|
194
184
|
}
|
|
195
185
|
|
|
196
186
|
// Open Studio in browser with 'o'
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
} from "../utils/output.js";
|
|
8
8
|
import { type ParsedArgs, getFlag } from "../utils/parse-args.js";
|
|
9
9
|
import { getAuthenticatedClient } from "../utils/supabase.js";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
validateEnum,
|
|
12
|
+
validateRequired,
|
|
13
|
+
validateUuid,
|
|
14
|
+
} from "../utils/validate.js";
|
|
11
15
|
|
|
12
16
|
export async function storeCommand(parsed: ParsedArgs): Promise<void> {
|
|
13
17
|
const sub = parsed.subcommand;
|
|
@@ -63,13 +67,16 @@ async function storeCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
63
67
|
};
|
|
64
68
|
}
|
|
65
69
|
|
|
70
|
+
const orgFlag = getFlag(parsed.flags, "org");
|
|
71
|
+
const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
|
|
72
|
+
|
|
66
73
|
if (parsed.global.dryRun) {
|
|
67
|
-
outputDryRun("store.create", input, format);
|
|
74
|
+
outputDryRun("store.create", { ...input, orgId }, format);
|
|
68
75
|
return;
|
|
69
76
|
}
|
|
70
77
|
|
|
71
78
|
const client = await getAuthenticatedClient();
|
|
72
|
-
const result = await createStore(client, input);
|
|
79
|
+
const result = await createStore(client, input, orgId);
|
|
73
80
|
|
|
74
81
|
outputResult(result, format, parsed.global.fields);
|
|
75
82
|
if (result.error) process.exit(1);
|
|
@@ -88,8 +95,11 @@ async function storeListCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
88
95
|
const format = detectOutputFormat(parsed.global.output);
|
|
89
96
|
|
|
90
97
|
try {
|
|
98
|
+
const orgFlag = getFlag(parsed.flags, "org");
|
|
99
|
+
const orgId = orgFlag ? validateUuid(orgFlag, "org") : undefined;
|
|
100
|
+
|
|
91
101
|
const client = await getAuthenticatedClient();
|
|
92
|
-
const result = await listStores(client);
|
|
102
|
+
const result = await listStores(client, orgId);
|
|
93
103
|
|
|
94
104
|
outputResult(result, format, parsed.global.fields);
|
|
95
105
|
if (result.error) process.exit(1);
|
package/src/commands/whoami.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { getStoreById } from "../core/store.js";
|
|
1
2
|
import { getCurrentUser } from "../utils/auth.js";
|
|
3
|
+
import { loadConfig, resolveStage } from "../utils/config.js";
|
|
2
4
|
import { detectOutputFormat, outputResult } from "../utils/output.js";
|
|
3
5
|
import type { ParsedArgs } from "../utils/parse-args.js";
|
|
6
|
+
import { getAuthenticatedClient } from "../utils/supabase.js";
|
|
4
7
|
|
|
5
8
|
export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
|
|
6
9
|
const format = detectOutputFormat(parsed.global.output);
|
|
@@ -9,20 +12,78 @@ export async function whoamiCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
9
12
|
if (!user) {
|
|
10
13
|
outputResult(
|
|
11
14
|
{
|
|
12
|
-
error: {
|
|
15
|
+
error: {
|
|
16
|
+
message: "Not logged in or session expired. Run `ollieshop login`.",
|
|
17
|
+
},
|
|
13
18
|
},
|
|
14
19
|
format,
|
|
15
20
|
);
|
|
16
21
|
process.exit(1);
|
|
17
22
|
}
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
try {
|
|
25
|
+
const stage = resolveStage(parsed.global.stage);
|
|
26
|
+
const config = await loadConfig({ stage });
|
|
27
|
+
|
|
28
|
+
// No project config: just report the authenticated user.
|
|
29
|
+
if (!config?.storeId) {
|
|
30
|
+
outputResult(
|
|
31
|
+
{
|
|
32
|
+
data: {
|
|
33
|
+
email: user.email,
|
|
34
|
+
hint: "No ollie.json found. Run `ollieshop init` to link a store.",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
format,
|
|
38
|
+
parsed.global.fields,
|
|
39
|
+
);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Config present: resolve the store/org it points at, gated by access.
|
|
44
|
+
const client = await getAuthenticatedClient();
|
|
45
|
+
const result = await getStoreById(client, config.storeId);
|
|
46
|
+
|
|
47
|
+
if (result.error || !result.data) {
|
|
48
|
+
outputResult(
|
|
49
|
+
{
|
|
50
|
+
data: {
|
|
51
|
+
email: user.email,
|
|
52
|
+
storeId: config.storeId,
|
|
53
|
+
store: null,
|
|
54
|
+
note:
|
|
55
|
+
result.error?.message ??
|
|
56
|
+
"Store not found. Run `ollieshop login` or check the storeId in ollie.json.",
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
format,
|
|
60
|
+
parsed.global.fields,
|
|
61
|
+
);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const store = result.data;
|
|
66
|
+
outputResult(
|
|
67
|
+
{
|
|
68
|
+
data: {
|
|
69
|
+
email: user.email,
|
|
70
|
+
orgId: store.organizationId,
|
|
71
|
+
org: store.organizationName,
|
|
72
|
+
storeId: store.id,
|
|
73
|
+
store: store.name,
|
|
74
|
+
...(config.versionId ? { versionId: config.versionId } : {}),
|
|
75
|
+
},
|
|
23
76
|
},
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)
|
|
77
|
+
format,
|
|
78
|
+
parsed.global.fields,
|
|
79
|
+
);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
outputResult(
|
|
82
|
+
{
|
|
83
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
84
|
+
},
|
|
85
|
+
format,
|
|
86
|
+
);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
28
89
|
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
+
import { functionCreateSchema } from "./schema.js";
|
|
3
|
+
|
|
4
|
+
export interface CreateFunctionInput {
|
|
5
|
+
versionId: string;
|
|
6
|
+
name: string;
|
|
7
|
+
trigger?: { url: string; expression: string };
|
|
8
|
+
active?: boolean;
|
|
9
|
+
onError?: "throw" | "skip";
|
|
10
|
+
priority?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface FunctionRecord {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
active: boolean;
|
|
17
|
+
urn: string | null;
|
|
18
|
+
on_error: string;
|
|
19
|
+
priority: number;
|
|
20
|
+
trigger: { url: string; expression: string } | null;
|
|
21
|
+
invocation: string | null;
|
|
22
|
+
version_id: string;
|
|
23
|
+
created_at: string;
|
|
24
|
+
versions?: { id: string; name: string };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createFunction(
|
|
28
|
+
client: SupabaseClient,
|
|
29
|
+
input: CreateFunctionInput,
|
|
30
|
+
): Promise<
|
|
31
|
+
| { success: true; data: { id: string } }
|
|
32
|
+
| { success: false; error: { message: string } }
|
|
33
|
+
> {
|
|
34
|
+
const parsed = functionCreateSchema.safeParse(input);
|
|
35
|
+
if (!parsed.success) {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: {
|
|
39
|
+
message: parsed.error.issues
|
|
40
|
+
.map((i: { message: string }) => i.message)
|
|
41
|
+
.join("; "),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { data, error } = await client
|
|
47
|
+
.from("functions")
|
|
48
|
+
.insert({
|
|
49
|
+
name: parsed.data.name,
|
|
50
|
+
active: parsed.data.active,
|
|
51
|
+
version_id: parsed.data.versionId,
|
|
52
|
+
trigger: parsed.data.trigger ?? null,
|
|
53
|
+
on_error: parsed.data.onError ?? "throw",
|
|
54
|
+
priority: parsed.data.priority ?? 0,
|
|
55
|
+
})
|
|
56
|
+
.select("id")
|
|
57
|
+
.single();
|
|
58
|
+
|
|
59
|
+
if (error) {
|
|
60
|
+
return { success: false, error: { message: error.message } };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { success: true, data: { id: data.id } };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function listFunctions(
|
|
67
|
+
client: SupabaseClient,
|
|
68
|
+
storeId: string,
|
|
69
|
+
versionId?: string,
|
|
70
|
+
): Promise<
|
|
71
|
+
| { success: true; data: FunctionRecord[] }
|
|
72
|
+
| { success: false; error: { message: string } }
|
|
73
|
+
> {
|
|
74
|
+
let query = client
|
|
75
|
+
.from("functions")
|
|
76
|
+
.select(
|
|
77
|
+
"id, name, active, urn, on_error, priority, trigger, invocation, version_id, created_at, versions!inner(id, name)",
|
|
78
|
+
)
|
|
79
|
+
.eq("versions.store_id", storeId);
|
|
80
|
+
|
|
81
|
+
if (versionId) {
|
|
82
|
+
query = query.eq("versions.id", versionId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const { data, error } = await query;
|
|
86
|
+
|
|
87
|
+
if (error) {
|
|
88
|
+
return { success: false, error: { message: error.message } };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { success: true, data: (data ?? []) as unknown as FunctionRecord[] };
|
|
92
|
+
}
|
package/src/core/schema.ts
CHANGED
|
@@ -65,10 +65,31 @@ export const functionCreateSchema = z.object({
|
|
|
65
65
|
versionId: z.string().uuid().describe("Parent version UUID"),
|
|
66
66
|
name: z.string().min(1).describe("Function name"),
|
|
67
67
|
trigger: z
|
|
68
|
-
.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
.object({
|
|
69
|
+
url: z.string().min(1).describe("Trigger URL — absolute http(s) URL"),
|
|
70
|
+
expression: z
|
|
71
|
+
.string()
|
|
72
|
+
.min(1)
|
|
73
|
+
.describe("JSONata-like match expression (e.g. 'method in [\"GET\"]')"),
|
|
74
|
+
})
|
|
75
|
+
.optional()
|
|
76
|
+
.describe(
|
|
77
|
+
"Function trigger — both url and expression are required together",
|
|
78
|
+
),
|
|
79
|
+
active: z
|
|
80
|
+
.boolean()
|
|
81
|
+
.default(false)
|
|
82
|
+
.describe("Whether function is active (default: false)"),
|
|
83
|
+
onError: z
|
|
84
|
+
.enum(["throw", "skip"])
|
|
85
|
+
.optional()
|
|
86
|
+
.describe("Error handling: throw (default) or skip"),
|
|
87
|
+
priority: z
|
|
88
|
+
.number()
|
|
89
|
+
.int()
|
|
90
|
+
.min(0)
|
|
91
|
+
.optional()
|
|
92
|
+
.describe("Execution order priority (default: 0)"),
|
|
72
93
|
});
|
|
73
94
|
|
|
74
95
|
export const functionListSchema = z.object({
|
package/src/core/store.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
|
2
|
-
import { getOrganizationId } from "../utils/supabase.js";
|
|
2
|
+
import { getOrganizationId, unwrapToOne } from "../utils/supabase.js";
|
|
3
3
|
import { storeCreateSchema } from "./schema.js";
|
|
4
4
|
|
|
5
5
|
export interface CreateStoreInput {
|
|
@@ -24,6 +24,7 @@ export interface StoreRecord {
|
|
|
24
24
|
export async function createStore(
|
|
25
25
|
client: SupabaseClient,
|
|
26
26
|
input: CreateStoreInput,
|
|
27
|
+
orgId?: string,
|
|
27
28
|
): Promise<{ data?: { id: string }; error?: { message: string } }> {
|
|
28
29
|
// Validate input against schema
|
|
29
30
|
const parsed = storeCreateSchema.safeParse(input);
|
|
@@ -33,7 +34,7 @@ export async function createStore(
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
const organizationId = await getOrganizationId(client);
|
|
37
|
+
const organizationId = await getOrganizationId(client, orgId);
|
|
37
38
|
|
|
38
39
|
const { data, error } = await client
|
|
39
40
|
.from("stores")
|
|
@@ -55,10 +56,56 @@ export async function createStore(
|
|
|
55
56
|
return { data: { id: data.id } };
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
export interface StoreWithOrg {
|
|
60
|
+
id: string;
|
|
61
|
+
name: string;
|
|
62
|
+
organizationId: string;
|
|
63
|
+
organizationName: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Resolves a store + org by id; RLS returns no row when the user lacks access.
|
|
67
|
+
export async function getStoreById(
|
|
68
|
+
client: SupabaseClient,
|
|
69
|
+
storeId: string,
|
|
70
|
+
): Promise<{ data?: StoreWithOrg; error?: { message: string } }> {
|
|
71
|
+
const { data, error } = await client
|
|
72
|
+
.from("stores")
|
|
73
|
+
.select("id, name, organization_id, organizations(id, name)")
|
|
74
|
+
.eq("id", storeId)
|
|
75
|
+
.maybeSingle();
|
|
76
|
+
|
|
77
|
+
if (error) {
|
|
78
|
+
return { error: { message: error.message } };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!data) {
|
|
82
|
+
return {
|
|
83
|
+
error: {
|
|
84
|
+
message: `Store ${storeId} not found or you don't have access to it.`,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const org = unwrapToOne<{ name?: string }>(
|
|
90
|
+
(data as { organizations?: unknown }).organizations,
|
|
91
|
+
);
|
|
92
|
+
const orgName = org?.name ?? null;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
data: {
|
|
96
|
+
id: data.id as string,
|
|
97
|
+
name: data.name as string,
|
|
98
|
+
organizationId: data.organization_id as string,
|
|
99
|
+
organizationName: orgName,
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
export async function listStores(
|
|
59
105
|
client: SupabaseClient,
|
|
106
|
+
orgId?: string,
|
|
60
107
|
): Promise<{ data?: StoreRecord[]; error?: { message: string } }> {
|
|
61
|
-
const organizationId = await getOrganizationId(client);
|
|
108
|
+
const organizationId = await getOrganizationId(client, orgId);
|
|
62
109
|
|
|
63
110
|
const { data, error } = await client
|
|
64
111
|
.from("stores")
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { App } from "./cli.js";
|
|
|
3
3
|
import { businessRuleCommand } from "./commands/business-rule-cmd.js";
|
|
4
4
|
import { componentCommand } from "./commands/component-cmd.js";
|
|
5
5
|
import { deployCommand } from "./commands/deploy-cmd.js";
|
|
6
|
+
import { functionCommand } from "./commands/function-cmd.js";
|
|
6
7
|
import { initCommand } from "./commands/init-cmd.js";
|
|
7
8
|
import { schemaCommand } from "./commands/schema-cmd.js";
|
|
8
9
|
import { statusCommand } from "./commands/status-cmd.js";
|
|
@@ -21,6 +22,7 @@ const AGENT_COMMANDS: Record<
|
|
|
21
22
|
version: versionCommand,
|
|
22
23
|
component: componentCommand,
|
|
23
24
|
"business-rule": businessRuleCommand,
|
|
25
|
+
function: functionCommand,
|
|
24
26
|
schema: schemaCommand,
|
|
25
27
|
init: initCommand,
|
|
26
28
|
deploy: deployCommand,
|