@ollie-shop/cli 1.2.2 → 1.4.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/.env.example +9 -5
- package/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +57 -0
- package/CONTEXT.md +11 -3
- package/README.md +7 -10
- package/dist/index.js +323 -76
- package/package.json +4 -5
- package/src/cli.tsx +8 -1
- package/src/commands/function-cmd.ts +42 -10
- package/src/commands/help.tsx +12 -1
- package/src/commands/start.tsx +60 -44
- package/src/commands/store-cmd.ts +14 -4
- package/src/commands/whoami.ts +70 -9
- package/src/core/function.ts +1 -1
- package/src/core/schema.ts +8 -3
- package/src/core/store.ts +50 -3
- 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/tsup.config.ts +7 -0
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
|
}
|
|
@@ -17,6 +17,43 @@ import {
|
|
|
17
17
|
|
|
18
18
|
const ON_ERROR_VALUES = ["throw", "skip"] as const;
|
|
19
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
|
+
|
|
20
57
|
export async function functionCommand(parsed: ParsedArgs): Promise<void> {
|
|
21
58
|
const sub = parsed.subcommand;
|
|
22
59
|
if (sub === "create") return functionCreateCommand(parsed);
|
|
@@ -35,7 +72,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
35
72
|
let input: {
|
|
36
73
|
versionId: string;
|
|
37
74
|
name: string;
|
|
38
|
-
trigger?: string;
|
|
75
|
+
trigger?: { url: string; expression: string };
|
|
39
76
|
active?: boolean;
|
|
40
77
|
onError?: "throw" | "skip";
|
|
41
78
|
priority?: number;
|
|
@@ -46,10 +83,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
46
83
|
input = {
|
|
47
84
|
versionId: validateUuid(raw.versionId, "versionId"),
|
|
48
85
|
name: validateRequired(raw.name, "name"),
|
|
49
|
-
trigger:
|
|
50
|
-
raw.trigger !== undefined && raw.trigger !== null
|
|
51
|
-
? validateTriggerUrl(String(raw.trigger), "trigger")
|
|
52
|
-
: undefined,
|
|
86
|
+
trigger: parseTriggerFromData(raw.trigger),
|
|
53
87
|
active: raw.active === true,
|
|
54
88
|
onError:
|
|
55
89
|
raw.onError !== undefined && raw.onError !== null
|
|
@@ -61,7 +95,8 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
61
95
|
: 0,
|
|
62
96
|
};
|
|
63
97
|
} else {
|
|
64
|
-
const
|
|
98
|
+
const triggerUrlFlag = getFlag(parsed.flags, "trigger-url");
|
|
99
|
+
const triggerExpressionFlag = getFlag(parsed.flags, "trigger-expression");
|
|
65
100
|
const onErrorFlag = getFlag(parsed.flags, "on-error");
|
|
66
101
|
const priorityFlag = getFlag(parsed.flags, "priority");
|
|
67
102
|
input = {
|
|
@@ -70,10 +105,7 @@ async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
|
70
105
|
"version-id",
|
|
71
106
|
),
|
|
72
107
|
name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
|
|
73
|
-
trigger:
|
|
74
|
-
triggerFlag !== undefined
|
|
75
|
-
? validateTriggerUrl(triggerFlag, "trigger")
|
|
76
|
-
: undefined,
|
|
108
|
+
trigger: parseTriggerFromFlags(triggerUrlFlag, triggerExpressionFlag),
|
|
77
109
|
active: getBoolFlag(parsed.flags, "active"),
|
|
78
110
|
onError:
|
|
79
111
|
onErrorFlag !== undefined
|
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}>
|
|
@@ -139,6 +139,12 @@ export function HelpCommand() {
|
|
|
139
139
|
</Box>
|
|
140
140
|
<Text>Config stage (loads ollie.{"{stage}"}.json)</Text>
|
|
141
141
|
</Box>
|
|
142
|
+
<Box>
|
|
143
|
+
<Box width={24}>
|
|
144
|
+
<Text color="yellow">--no-open</Text>
|
|
145
|
+
</Box>
|
|
146
|
+
<Text>start: don't auto-open Studio (also honored via CI env)</Text>
|
|
147
|
+
</Box>
|
|
142
148
|
</Box>
|
|
143
149
|
|
|
144
150
|
<Box marginTop={1}>
|
|
@@ -147,12 +153,17 @@ export function HelpCommand() {
|
|
|
147
153
|
<Box marginLeft={2} flexDirection="column">
|
|
148
154
|
<Text dimColor>$ ollieshop login</Text>
|
|
149
155
|
<Text dimColor>$ ollieshop start --stage dev</Text>
|
|
156
|
+
<Text dimColor>$ ollieshop start --no-open</Text>
|
|
150
157
|
<Text dimColor>$ ollieshop whoami -o json</Text>
|
|
151
158
|
<Text dimColor>$ ollieshop schema store.create</Text>
|
|
152
159
|
<Text dimColor>
|
|
153
160
|
$ ollieshop store create --name "My Store" --platform vtex
|
|
154
161
|
--platform-store-id mystore
|
|
155
162
|
</Text>
|
|
163
|
+
<Text dimColor>
|
|
164
|
+
$ ollieshop store create --org UUID --name "My Store" --platform vtex
|
|
165
|
+
--platform-store-id mystore
|
|
166
|
+
</Text>
|
|
156
167
|
<Text dimColor>
|
|
157
168
|
$ ollieshop store create --data{" "}
|
|
158
169
|
{
|
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,11 +54,20 @@ 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
|
|
62
61
|
const stage = resolveStage(parseArg(args, "--stage", "-s"));
|
|
62
|
+
// Suppress auto-opening Studio in the browser (Storybook-style). Useful when an
|
|
63
|
+
// agent spawns the dev server and drives its own harness instead. Also honored
|
|
64
|
+
// via the conventional CI env var.
|
|
65
|
+
const noOpen = args.includes("--no-open") || Boolean(process.env.CI);
|
|
66
|
+
// Keyboard shortcuts need raw mode, which Ink can only enable on a TTY stdin.
|
|
67
|
+
// When spawned headless (agent/CI/backgrounded), stdin isn't a TTY — guard the
|
|
68
|
+
// input handler so the server runs instead of crashing with "Raw mode is not
|
|
69
|
+
// supported on the current process.stdin".
|
|
70
|
+
const isInteractive = Boolean(process.stdin.isTTY);
|
|
63
71
|
|
|
64
72
|
const addLog = useCallback((log: Omit<RequestLog, "id" | "timestamp">) => {
|
|
65
73
|
setLogs((prev) => {
|
|
@@ -121,28 +129,19 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
121
129
|
setComponents(found);
|
|
122
130
|
setState({ status: "building" });
|
|
123
131
|
|
|
124
|
-
//
|
|
125
|
-
const
|
|
132
|
+
// Start dev server
|
|
133
|
+
const server = await startDevServer({
|
|
134
|
+
port: PORT,
|
|
126
135
|
stage,
|
|
136
|
+
onRequest: handleRequest,
|
|
127
137
|
onBuildEnd: (updatedComponents) => {
|
|
128
138
|
setComponents(updatedComponents);
|
|
129
139
|
setBuildCount((c) => c + 1);
|
|
130
140
|
setLastBuildTime(new Date());
|
|
131
141
|
},
|
|
132
142
|
});
|
|
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
143
|
|
|
144
|
+
rebuildRef.current = server.rebuild;
|
|
146
145
|
stopRef.current = server.stop;
|
|
147
146
|
|
|
148
147
|
if (!mounted) {
|
|
@@ -158,13 +157,15 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
158
157
|
versionId: config.versionId,
|
|
159
158
|
});
|
|
160
159
|
|
|
161
|
-
// Open Studio in browser
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
160
|
+
// Open Studio in browser (unless suppressed)
|
|
161
|
+
if (!noOpen) {
|
|
162
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
163
|
+
studioUrl.searchParams.set("storeId", config.storeId);
|
|
164
|
+
if (config.versionId) {
|
|
165
|
+
studioUrl.searchParams.set("versionId", config.versionId);
|
|
166
|
+
}
|
|
167
|
+
open(studioUrl.toString());
|
|
166
168
|
}
|
|
167
|
-
open(studioUrl.toString());
|
|
168
169
|
} catch (error) {
|
|
169
170
|
if (!mounted) return;
|
|
170
171
|
setState({
|
|
@@ -180,29 +181,32 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
180
181
|
mounted = false;
|
|
181
182
|
stopRef.current?.();
|
|
182
183
|
};
|
|
183
|
-
}, [stage, handleRequest]);
|
|
184
|
+
}, [stage, handleRequest, noOpen]);
|
|
184
185
|
|
|
185
|
-
// Handle keyboard input
|
|
186
|
-
useInput(
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
186
|
+
// Handle keyboard input (only when attached to a TTY — see isInteractive above)
|
|
187
|
+
useInput(
|
|
188
|
+
(input, key) => {
|
|
189
|
+
if (input === "q" || (input === "c" && key.ctrl)) {
|
|
190
|
+
stopRef.current?.().then(() => exit());
|
|
191
|
+
}
|
|
190
192
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Manual rebuild with 'r' (manifest is updated by the plugin)
|
|
194
|
+
if (input === "r" && state.status === "running") {
|
|
195
|
+
rebuildRef.current?.();
|
|
196
|
+
}
|
|
195
197
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
198
|
+
// Open Studio in browser with 'o'
|
|
199
|
+
if (input === "o" && state.status === "running") {
|
|
200
|
+
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
201
|
+
studioUrl.searchParams.set("storeId", state.storeId);
|
|
202
|
+
if (state.versionId) {
|
|
203
|
+
studioUrl.searchParams.set("versionId", state.versionId);
|
|
204
|
+
}
|
|
205
|
+
open(studioUrl.toString());
|
|
202
206
|
}
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
|
|
207
|
+
},
|
|
208
|
+
{ isActive: isInteractive },
|
|
209
|
+
);
|
|
206
210
|
|
|
207
211
|
return (
|
|
208
212
|
<Box flexDirection="column" gap={1}>
|
|
@@ -246,11 +250,12 @@ export function StartCommand({ args }: StartCommandProps) {
|
|
|
246
250
|
stage={stage}
|
|
247
251
|
storeId={state.storeId}
|
|
248
252
|
versionId={state.versionId}
|
|
253
|
+
noOpen={noOpen}
|
|
249
254
|
/>
|
|
250
255
|
<ComponentList components={components} />
|
|
251
256
|
<BuildInfo buildCount={buildCount} lastBuildTime={lastBuildTime} />
|
|
252
257
|
<RequestLogs logs={logs} />
|
|
253
|
-
<Footer />
|
|
258
|
+
<Footer interactive={isInteractive} />
|
|
254
259
|
</>
|
|
255
260
|
)}
|
|
256
261
|
</Box>
|
|
@@ -274,12 +279,14 @@ function ServerInfo({
|
|
|
274
279
|
stage,
|
|
275
280
|
storeId,
|
|
276
281
|
versionId,
|
|
282
|
+
noOpen,
|
|
277
283
|
}: {
|
|
278
284
|
host: string;
|
|
279
285
|
port: number;
|
|
280
286
|
stage?: string;
|
|
281
287
|
storeId: string;
|
|
282
288
|
versionId?: string;
|
|
289
|
+
noOpen?: boolean;
|
|
283
290
|
}) {
|
|
284
291
|
const studioUrl = new URL(STUDIO_BASE_URL);
|
|
285
292
|
|
|
@@ -311,6 +318,7 @@ function ServerInfo({
|
|
|
311
318
|
<Text bold color="magenta">
|
|
312
319
|
{studioUrl.toString()}
|
|
313
320
|
</Text>
|
|
321
|
+
{noOpen && <Text dimColor> (auto-open off — press o)</Text>}
|
|
314
322
|
</Box>
|
|
315
323
|
<Box marginLeft={2} marginTop={1}>
|
|
316
324
|
<Text dimColor>
|
|
@@ -396,7 +404,15 @@ function RequestLogs({ logs }: { logs: RequestLog[] }) {
|
|
|
396
404
|
);
|
|
397
405
|
}
|
|
398
406
|
|
|
399
|
-
function Footer() {
|
|
407
|
+
function Footer({ interactive = true }: { interactive?: boolean }) {
|
|
408
|
+
if (!interactive) {
|
|
409
|
+
return (
|
|
410
|
+
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
411
|
+
<Text dimColor>Headless (no TTY) — Ctrl+C to stop</Text>
|
|
412
|
+
</Box>
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
|
|
400
416
|
return (
|
|
401
417
|
<Box marginTop={1} borderStyle="single" borderColor="gray" paddingX={1}>
|
|
402
418
|
<Text dimColor>Press </Text>
|
|
@@ -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
|
}
|
package/src/core/function.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { functionCreateSchema } from "./schema.js";
|
|
|
4
4
|
export interface CreateFunctionInput {
|
|
5
5
|
versionId: string;
|
|
6
6
|
name: string;
|
|
7
|
-
trigger?: string;
|
|
7
|
+
trigger?: { url: string; expression: string };
|
|
8
8
|
active?: boolean;
|
|
9
9
|
onError?: "throw" | "skip";
|
|
10
10
|
priority?: number;
|
package/src/core/schema.ts
CHANGED
|
@@ -65,11 +65,16 @@ 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
|
-
|
|
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
|
+
})
|
|
70
75
|
.optional()
|
|
71
76
|
.describe(
|
|
72
|
-
"Function trigger
|
|
77
|
+
"Function trigger — both url and expression are required together",
|
|
73
78
|
),
|
|
74
79
|
active: z
|
|
75
80
|
.boolean()
|
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/utils/auth.ts
CHANGED
|
@@ -231,6 +231,10 @@ export async function getCurrentUser(): Promise<{ email: string } | null> {
|
|
|
231
231
|
|
|
232
232
|
try {
|
|
233
233
|
const decoded = jwtDecode<JwtPayload>(credentials.accessToken);
|
|
234
|
+
// Reject expired sessions (local `exp` check; misses server-side revocation).
|
|
235
|
+
if (decoded.exp && decoded.exp * 1000 <= Date.now()) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
234
238
|
return decoded.email ? { email: decoded.email } : null;
|
|
235
239
|
} catch {
|
|
236
240
|
return null;
|