@ollie-shop/cli 1.2.1 → 1.2.2
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/.turbo/turbo-build.log +3 -3
- package/CHANGELOG.md +10 -0
- package/CONTEXT.md +8 -3
- package/README.md +14 -0
- package/dist/index.js +176 -2
- package/package.json +1 -1
- package/src/commands/function-cmd.ts +146 -0
- package/src/commands/help.tsx +9 -0
- package/src/core/function.ts +92 -0
- package/src/core/schema.ts +18 -2
- package/src/index.tsx +2 -0
- package/src/utils/validate.ts +44 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
> @ollie-shop/cli@1.2.
|
|
2
|
+
> @ollie-shop/cli@1.2.2 build /home/runner/work/ollie-shop/ollie-shop/packages/cli
|
|
3
3
|
> tsup
|
|
4
4
|
|
|
5
5
|
[34mCLI[39m Building entry: src/index.tsx
|
|
@@ -9,5 +9,5 @@
|
|
|
9
9
|
[34mCLI[39m Target: node22
|
|
10
10
|
[34mCLI[39m Cleaning output folder
|
|
11
11
|
[34mESM[39m Build start
|
|
12
|
-
[32mESM[39m [1mdist/index.js [22m[
|
|
13
|
-
[32mESM[39m ⚡️ Build success in
|
|
12
|
+
[32mESM[39m [1mdist/index.js [22m[32m85.34 KB[39m
|
|
13
|
+
[32mESM[39m ⚡️ Build success in 216ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# @ollie-shop/cli
|
|
2
2
|
|
|
3
|
+
## 1.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0268a4b: Bake prod Supabase + builder defaults into the CLI so `ollieshop` runs with zero env setup.
|
|
8
|
+
|
|
9
|
+
Agent commands (`whoami`, `store`, `version`, `component`, `function`, `deploy`, `status`) previously threw `Missing required environment variable: OLLIE_SUPABASE_URL` unless the user had exported `OLLIE_SUPABASE_URL`, `OLLIE_SUPABASE_ANON_KEY`, and `OLLIE_BUILDER_URL` in their shell first. All three values are public, so the friction was pure onboarding overhead.
|
|
10
|
+
|
|
11
|
+
`packages/cli/src/utils/supabase.ts` now holds a `PROD_DEFAULTS` const and an `envOrDefault()` helper. Env vars still win when set, so devs pointing the CLI at local Supabase (`http://127.0.0.1:54321`) or a non-prod project are unaffected. Fresh contributors can now run `ollieshop login && ollieshop whoami` without any shell setup.
|
|
12
|
+
|
|
3
13
|
## 1.2.1
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/CONTEXT.md
CHANGED
|
@@ -33,6 +33,7 @@ The CLI will throw a clear error if a required env var is missing. See `.env.exa
|
|
|
33
33
|
ollieshop schema store.create -o json
|
|
34
34
|
ollieshop schema version.create -o json
|
|
35
35
|
ollieshop schema component.create -o json
|
|
36
|
+
ollieshop schema function.create -o json
|
|
36
37
|
|
|
37
38
|
# 2. Verify identity
|
|
38
39
|
ollieshop whoami -o json
|
|
@@ -50,17 +51,21 @@ ollieshop init --store-id <STORE_ID> --version-id <VERSION_ID>
|
|
|
50
51
|
# 6. Register components
|
|
51
52
|
ollieshop component create --data '{"versionId":"<VERSION_ID>","name":"FreeShippingBar","slot":"cart_header_full_page","active":true}' -o json
|
|
52
53
|
|
|
53
|
-
# 7.
|
|
54
|
+
# 7. Register functions (optional)
|
|
55
|
+
ollieshop function create --data '{"versionId":"<VERSION_ID>","name":"myHook","active":true}' -o json
|
|
56
|
+
|
|
57
|
+
# 8. Deploy component (bundle + upload + build)
|
|
54
58
|
ollieshop deploy --component-id <COMPONENT_ID> --name FreeShippingBar --wait -o json
|
|
55
59
|
|
|
56
|
-
#
|
|
60
|
+
# 9. Check build status
|
|
57
61
|
ollieshop status --build-id <BUILD_ID> -o json
|
|
58
62
|
ollieshop status --build-id <BUILD_ID> --wait --timeout 300 -o json
|
|
59
63
|
|
|
60
|
-
#
|
|
64
|
+
# 10. List resources
|
|
61
65
|
ollieshop store list -o json --fields id,name,platform
|
|
62
66
|
ollieshop version list --store-id <STORE_ID> -o json --fields id,name,active
|
|
63
67
|
ollieshop component list --store-id <STORE_ID> -o json --fields id,name,slot,active
|
|
68
|
+
ollieshop function list --store-id <STORE_ID> -o json --fields id,name,active
|
|
64
69
|
```
|
|
65
70
|
|
|
66
71
|
## Deploy Workflow
|
package/README.md
CHANGED
|
@@ -106,6 +106,18 @@ ollieshop component list --store-id <STORE_UUID> -o json --fields id,name,slot,a
|
|
|
106
106
|
ollieshop component create --version-id <VERSION_UUID> --name FreeShippingBar --slot cart_header_full_page -o json
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
+
#### `function create|list`
|
|
110
|
+
|
|
111
|
+
Create or list functions for a version.
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# List functions
|
|
115
|
+
ollieshop function list --store-id <STORE_UUID> -o json --fields id,name,active
|
|
116
|
+
|
|
117
|
+
# Create a function
|
|
118
|
+
ollieshop function create --version-id <VERSION_UUID> --name myHook -o json
|
|
119
|
+
```
|
|
120
|
+
|
|
109
121
|
#### `deploy`
|
|
110
122
|
|
|
111
123
|
Bundle a component directory into a zip and upload to the Builder service.
|
|
@@ -207,6 +219,7 @@ src/
|
|
|
207
219
|
│ ├── store-cmd.ts # Agent: store CRUD
|
|
208
220
|
│ ├── version-cmd.ts # Agent: version CRUD
|
|
209
221
|
│ ├── component-cmd.ts # Agent: component CRUD
|
|
222
|
+
│ ├── function-cmd.ts # Agent: function CRUD
|
|
210
223
|
│ ├── deploy-cmd.ts # Agent: bundle + upload builds
|
|
211
224
|
│ ├── status-cmd.ts # Agent: check/poll build status
|
|
212
225
|
│ ├── schema-cmd.ts # Agent: schema introspection
|
|
@@ -218,6 +231,7 @@ src/
|
|
|
218
231
|
│ ├── store.ts # Store business logic + Supabase queries
|
|
219
232
|
│ ├── version.ts # Version business logic + Supabase queries
|
|
220
233
|
│ ├── component.ts # Component business logic + Supabase queries
|
|
234
|
+
│ ├── function.ts # Function business logic + Supabase queries
|
|
221
235
|
│ ├── deploy.ts # Builder API client (upload, status, poll)
|
|
222
236
|
│ └── schema.ts # Zod schemas + JSON Schema generation
|
|
223
237
|
└── utils/
|
package/dist/index.js
CHANGED
|
@@ -50,6 +50,10 @@ function HelpCommand() {
|
|
|
50
50
|
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "component create|list" }) }),
|
|
51
51
|
/* @__PURE__ */ jsx(Text, { children: "Create or list components" })
|
|
52
52
|
] }),
|
|
53
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
54
|
+
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "function create|list" }) }),
|
|
55
|
+
/* @__PURE__ */ jsx(Text, { children: "Create or list functions" })
|
|
56
|
+
] }),
|
|
53
57
|
/* @__PURE__ */ jsxs(Box, { children: [
|
|
54
58
|
/* @__PURE__ */ jsx(Box, { width: 24, children: /* @__PURE__ */ jsx(Text, { color: "green", children: "deploy" }) }),
|
|
55
59
|
/* @__PURE__ */ jsx(Text, { children: "Bundle and upload a component/function build" })
|
|
@@ -121,6 +125,7 @@ function HelpCommand() {
|
|
|
121
125
|
] }),
|
|
122
126
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop version create --store-id UUID --name v1 --active" }),
|
|
123
127
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop init --store-id UUID --version-id UUID" }),
|
|
128
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop function create --version-id UUID --name myHook" }),
|
|
124
129
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop deploy --component-id UUID --name FreeShippingBar --wait" }),
|
|
125
130
|
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "$ ollieshop status --build-id BUILD_ID --wait -o json" })
|
|
126
131
|
] })
|
|
@@ -1467,6 +1472,39 @@ function validateRequired(value, name) {
|
|
|
1467
1472
|
}
|
|
1468
1473
|
return rejectControlChars(value.trim(), name);
|
|
1469
1474
|
}
|
|
1475
|
+
function validateInteger(value, name, opts) {
|
|
1476
|
+
const n = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
1477
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
1478
|
+
throw new Error(`Invalid ${name}: "${String(value)}". Must be an integer.`);
|
|
1479
|
+
}
|
|
1480
|
+
if (opts?.min !== void 0 && n < opts.min) {
|
|
1481
|
+
throw new Error(`Invalid ${name}: ${n}. Must be >= ${opts.min}.`);
|
|
1482
|
+
}
|
|
1483
|
+
return n;
|
|
1484
|
+
}
|
|
1485
|
+
function validateTriggerUrl(value, name) {
|
|
1486
|
+
rejectControlChars(value, name);
|
|
1487
|
+
const trimmed = value.trim();
|
|
1488
|
+
if (trimmed === "") {
|
|
1489
|
+
throw new Error(
|
|
1490
|
+
`Invalid ${name}: must be a non-empty absolute http(s) URL or a relative path starting with "/".`
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
if (trimmed.startsWith("/")) {
|
|
1494
|
+
return trimmed;
|
|
1495
|
+
}
|
|
1496
|
+
try {
|
|
1497
|
+
const parsed2 = new URL(trimmed);
|
|
1498
|
+
if (parsed2.protocol !== "http:" && parsed2.protocol !== "https:") {
|
|
1499
|
+
throw new Error("unsupported protocol");
|
|
1500
|
+
}
|
|
1501
|
+
return trimmed;
|
|
1502
|
+
} catch {
|
|
1503
|
+
throw new Error(
|
|
1504
|
+
`Invalid ${name}: "${value}". Must be an absolute http(s) URL or a relative path starting with "/".`
|
|
1505
|
+
);
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1470
1508
|
|
|
1471
1509
|
// src/commands/business-rule-cmd.ts
|
|
1472
1510
|
async function businessRuleCommand(parsed2) {
|
|
@@ -1625,8 +1663,12 @@ var componentListSchema = z2.object({
|
|
|
1625
1663
|
var functionCreateSchema = z2.object({
|
|
1626
1664
|
versionId: z2.string().uuid().describe("Parent version UUID"),
|
|
1627
1665
|
name: z2.string().min(1).describe("Function name"),
|
|
1628
|
-
trigger: z2.string().min(1).describe(
|
|
1629
|
-
|
|
1666
|
+
trigger: z2.string().min(1).optional().describe(
|
|
1667
|
+
"Function trigger URL \u2014 absolute http(s) URL or relative path starting with /"
|
|
1668
|
+
),
|
|
1669
|
+
active: z2.boolean().default(false).describe("Whether function is active (default: false)"),
|
|
1670
|
+
onError: z2.enum(["throw", "skip"]).optional().describe("Error handling: throw (default) or skip"),
|
|
1671
|
+
priority: z2.number().int().min(0).optional().describe("Execution order priority (default: 0)")
|
|
1630
1672
|
});
|
|
1631
1673
|
var functionListSchema = z2.object({
|
|
1632
1674
|
versionId: z2.string().uuid().describe("Version UUID to list functions for")
|
|
@@ -2005,6 +2047,137 @@ async function deployCommand(parsed2) {
|
|
|
2005
2047
|
}
|
|
2006
2048
|
}
|
|
2007
2049
|
|
|
2050
|
+
// src/core/function.ts
|
|
2051
|
+
async function createFunction(client, input) {
|
|
2052
|
+
const parsed2 = functionCreateSchema.safeParse(input);
|
|
2053
|
+
if (!parsed2.success) {
|
|
2054
|
+
return {
|
|
2055
|
+
success: false,
|
|
2056
|
+
error: {
|
|
2057
|
+
message: parsed2.error.issues.map((i) => i.message).join("; ")
|
|
2058
|
+
}
|
|
2059
|
+
};
|
|
2060
|
+
}
|
|
2061
|
+
const { data, error } = await client.from("functions").insert({
|
|
2062
|
+
name: parsed2.data.name,
|
|
2063
|
+
active: parsed2.data.active,
|
|
2064
|
+
version_id: parsed2.data.versionId,
|
|
2065
|
+
trigger: parsed2.data.trigger ?? null,
|
|
2066
|
+
on_error: parsed2.data.onError ?? "throw",
|
|
2067
|
+
priority: parsed2.data.priority ?? 0
|
|
2068
|
+
}).select("id").single();
|
|
2069
|
+
if (error) {
|
|
2070
|
+
return { success: false, error: { message: error.message } };
|
|
2071
|
+
}
|
|
2072
|
+
return { success: true, data: { id: data.id } };
|
|
2073
|
+
}
|
|
2074
|
+
async function listFunctions(client, storeId, versionId) {
|
|
2075
|
+
let query = client.from("functions").select(
|
|
2076
|
+
"id, name, active, urn, on_error, priority, trigger, invocation, version_id, created_at, versions!inner(id, name)"
|
|
2077
|
+
).eq("versions.store_id", storeId);
|
|
2078
|
+
if (versionId) {
|
|
2079
|
+
query = query.eq("versions.id", versionId);
|
|
2080
|
+
}
|
|
2081
|
+
const { data, error } = await query;
|
|
2082
|
+
if (error) {
|
|
2083
|
+
return { success: false, error: { message: error.message } };
|
|
2084
|
+
}
|
|
2085
|
+
return { success: true, data: data ?? [] };
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// src/commands/function-cmd.ts
|
|
2089
|
+
var ON_ERROR_VALUES = ["throw", "skip"];
|
|
2090
|
+
async function functionCommand(parsed2) {
|
|
2091
|
+
const sub = parsed2.subcommand;
|
|
2092
|
+
if (sub === "create") return functionCreateCommand(parsed2);
|
|
2093
|
+
if (sub === "list" || sub === "ls") return functionListCommand(parsed2);
|
|
2094
|
+
console.error(
|
|
2095
|
+
`Unknown function subcommand: ${sub}. Use: function create | function list`
|
|
2096
|
+
);
|
|
2097
|
+
process.exit(1);
|
|
2098
|
+
}
|
|
2099
|
+
async function functionCreateCommand(parsed2) {
|
|
2100
|
+
const format = detectOutputFormat(parsed2.global.output);
|
|
2101
|
+
try {
|
|
2102
|
+
let input;
|
|
2103
|
+
if (parsed2.global.data) {
|
|
2104
|
+
const raw = JSON.parse(parsed2.global.data);
|
|
2105
|
+
input = {
|
|
2106
|
+
versionId: validateUuid(raw.versionId, "versionId"),
|
|
2107
|
+
name: validateRequired(raw.name, "name"),
|
|
2108
|
+
trigger: raw.trigger !== void 0 && raw.trigger !== null ? validateTriggerUrl(String(raw.trigger), "trigger") : void 0,
|
|
2109
|
+
active: raw.active === true,
|
|
2110
|
+
onError: raw.onError !== void 0 && raw.onError !== null ? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error") : "throw",
|
|
2111
|
+
priority: raw.priority !== void 0 && raw.priority !== null ? validateInteger(raw.priority, "priority", { min: 0 }) : 0
|
|
2112
|
+
};
|
|
2113
|
+
} else {
|
|
2114
|
+
const triggerFlag = getFlag(parsed2.flags, "trigger");
|
|
2115
|
+
const onErrorFlag = getFlag(parsed2.flags, "on-error");
|
|
2116
|
+
const priorityFlag = getFlag(parsed2.flags, "priority");
|
|
2117
|
+
input = {
|
|
2118
|
+
versionId: validateUuid(
|
|
2119
|
+
validateRequired(getFlag(parsed2.flags, "version-id"), "version-id"),
|
|
2120
|
+
"version-id"
|
|
2121
|
+
),
|
|
2122
|
+
name: validateRequired(getFlag(parsed2.flags, "name", "n"), "name"),
|
|
2123
|
+
trigger: triggerFlag !== void 0 ? validateTriggerUrl(triggerFlag, "trigger") : void 0,
|
|
2124
|
+
active: getBoolFlag(parsed2.flags, "active"),
|
|
2125
|
+
onError: onErrorFlag !== void 0 ? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error") : "throw",
|
|
2126
|
+
priority: priorityFlag !== void 0 ? validateInteger(priorityFlag, "priority", { min: 0 }) : 0
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
const validated = functionCreateSchema.safeParse(input);
|
|
2130
|
+
if (!validated.success) {
|
|
2131
|
+
throw new Error(validated.error.issues.map((i) => i.message).join("; "));
|
|
2132
|
+
}
|
|
2133
|
+
if (parsed2.global.dryRun) {
|
|
2134
|
+
outputDryRun(
|
|
2135
|
+
"function.create",
|
|
2136
|
+
validated.data,
|
|
2137
|
+
format
|
|
2138
|
+
);
|
|
2139
|
+
return;
|
|
2140
|
+
}
|
|
2141
|
+
const client = await getAuthenticatedClient();
|
|
2142
|
+
const result = await createFunction(client, validated.data);
|
|
2143
|
+
outputResult(result, format, parsed2.global.fields);
|
|
2144
|
+
if (!result.success) process.exit(1);
|
|
2145
|
+
} catch (err) {
|
|
2146
|
+
outputResult(
|
|
2147
|
+
{
|
|
2148
|
+
success: false,
|
|
2149
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
2150
|
+
},
|
|
2151
|
+
format
|
|
2152
|
+
);
|
|
2153
|
+
process.exit(1);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async function functionListCommand(parsed2) {
|
|
2157
|
+
const format = detectOutputFormat(parsed2.global.output);
|
|
2158
|
+
try {
|
|
2159
|
+
const storeId = validateUuid(
|
|
2160
|
+
validateRequired(getFlag(parsed2.flags, "store-id"), "store-id"),
|
|
2161
|
+
"store-id"
|
|
2162
|
+
);
|
|
2163
|
+
const versionId = getFlag(parsed2.flags, "version-id");
|
|
2164
|
+
if (versionId) validateUuid(versionId, "version-id");
|
|
2165
|
+
const client = await getAuthenticatedClient();
|
|
2166
|
+
const result = await listFunctions(client, storeId, versionId);
|
|
2167
|
+
outputResult(result, format, parsed2.global.fields);
|
|
2168
|
+
if (!result.success) process.exit(1);
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
outputResult(
|
|
2171
|
+
{
|
|
2172
|
+
success: false,
|
|
2173
|
+
error: { message: err instanceof Error ? err.message : String(err) }
|
|
2174
|
+
},
|
|
2175
|
+
format
|
|
2176
|
+
);
|
|
2177
|
+
process.exit(1);
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2008
2181
|
// src/commands/init-cmd.ts
|
|
2009
2182
|
async function initCommand(parsed2) {
|
|
2010
2183
|
const format = detectOutputFormat(parsed2.global.output);
|
|
@@ -2376,6 +2549,7 @@ var AGENT_COMMANDS = {
|
|
|
2376
2549
|
version: versionCommand,
|
|
2377
2550
|
component: componentCommand,
|
|
2378
2551
|
"business-rule": businessRuleCommand,
|
|
2552
|
+
function: functionCommand,
|
|
2379
2553
|
schema: schemaCommand,
|
|
2380
2554
|
init: initCommand,
|
|
2381
2555
|
deploy: deployCommand,
|
package/package.json
CHANGED
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
export async function functionCommand(parsed: ParsedArgs): Promise<void> {
|
|
21
|
+
const sub = parsed.subcommand;
|
|
22
|
+
if (sub === "create") return functionCreateCommand(parsed);
|
|
23
|
+
if (sub === "list" || sub === "ls") return functionListCommand(parsed);
|
|
24
|
+
|
|
25
|
+
console.error(
|
|
26
|
+
`Unknown function subcommand: ${sub}. Use: function create | function list`,
|
|
27
|
+
);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function functionCreateCommand(parsed: ParsedArgs): Promise<void> {
|
|
32
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
let input: {
|
|
36
|
+
versionId: string;
|
|
37
|
+
name: string;
|
|
38
|
+
trigger?: string;
|
|
39
|
+
active?: boolean;
|
|
40
|
+
onError?: "throw" | "skip";
|
|
41
|
+
priority?: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
if (parsed.global.data) {
|
|
45
|
+
const raw = JSON.parse(parsed.global.data);
|
|
46
|
+
input = {
|
|
47
|
+
versionId: validateUuid(raw.versionId, "versionId"),
|
|
48
|
+
name: validateRequired(raw.name, "name"),
|
|
49
|
+
trigger:
|
|
50
|
+
raw.trigger !== undefined && raw.trigger !== null
|
|
51
|
+
? validateTriggerUrl(String(raw.trigger), "trigger")
|
|
52
|
+
: undefined,
|
|
53
|
+
active: raw.active === true,
|
|
54
|
+
onError:
|
|
55
|
+
raw.onError !== undefined && raw.onError !== null
|
|
56
|
+
? validateEnum(String(raw.onError), ON_ERROR_VALUES, "on-error")
|
|
57
|
+
: "throw",
|
|
58
|
+
priority:
|
|
59
|
+
raw.priority !== undefined && raw.priority !== null
|
|
60
|
+
? validateInteger(raw.priority, "priority", { min: 0 })
|
|
61
|
+
: 0,
|
|
62
|
+
};
|
|
63
|
+
} else {
|
|
64
|
+
const triggerFlag = getFlag(parsed.flags, "trigger");
|
|
65
|
+
const onErrorFlag = getFlag(parsed.flags, "on-error");
|
|
66
|
+
const priorityFlag = getFlag(parsed.flags, "priority");
|
|
67
|
+
input = {
|
|
68
|
+
versionId: validateUuid(
|
|
69
|
+
validateRequired(getFlag(parsed.flags, "version-id"), "version-id"),
|
|
70
|
+
"version-id",
|
|
71
|
+
),
|
|
72
|
+
name: validateRequired(getFlag(parsed.flags, "name", "n"), "name"),
|
|
73
|
+
trigger:
|
|
74
|
+
triggerFlag !== undefined
|
|
75
|
+
? validateTriggerUrl(triggerFlag, "trigger")
|
|
76
|
+
: undefined,
|
|
77
|
+
active: getBoolFlag(parsed.flags, "active"),
|
|
78
|
+
onError:
|
|
79
|
+
onErrorFlag !== undefined
|
|
80
|
+
? validateEnum(onErrorFlag, ON_ERROR_VALUES, "on-error")
|
|
81
|
+
: "throw",
|
|
82
|
+
priority:
|
|
83
|
+
priorityFlag !== undefined
|
|
84
|
+
? validateInteger(priorityFlag, "priority", { min: 0 })
|
|
85
|
+
: 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const validated = functionCreateSchema.safeParse(input);
|
|
90
|
+
if (!validated.success) {
|
|
91
|
+
throw new Error(validated.error.issues.map((i) => i.message).join("; "));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (parsed.global.dryRun) {
|
|
95
|
+
outputDryRun(
|
|
96
|
+
"function.create",
|
|
97
|
+
validated.data as Record<string, unknown>,
|
|
98
|
+
format,
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const client = await getAuthenticatedClient();
|
|
104
|
+
const result = await createFunction(client, validated.data);
|
|
105
|
+
|
|
106
|
+
outputResult(result, format, parsed.global.fields);
|
|
107
|
+
if (!result.success) process.exit(1);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
outputResult(
|
|
110
|
+
{
|
|
111
|
+
success: false,
|
|
112
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
113
|
+
},
|
|
114
|
+
format,
|
|
115
|
+
);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function functionListCommand(parsed: ParsedArgs): Promise<void> {
|
|
121
|
+
const format = detectOutputFormat(parsed.global.output);
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const storeId = validateUuid(
|
|
125
|
+
validateRequired(getFlag(parsed.flags, "store-id"), "store-id"),
|
|
126
|
+
"store-id",
|
|
127
|
+
);
|
|
128
|
+
const versionId = getFlag(parsed.flags, "version-id");
|
|
129
|
+
if (versionId) validateUuid(versionId, "version-id");
|
|
130
|
+
|
|
131
|
+
const client = await getAuthenticatedClient();
|
|
132
|
+
const result = await listFunctions(client, storeId, versionId);
|
|
133
|
+
|
|
134
|
+
outputResult(result, format, parsed.global.fields);
|
|
135
|
+
if (!result.success) process.exit(1);
|
|
136
|
+
} catch (err) {
|
|
137
|
+
outputResult(
|
|
138
|
+
{
|
|
139
|
+
success: false,
|
|
140
|
+
error: { message: err instanceof Error ? err.message : String(err) },
|
|
141
|
+
},
|
|
142
|
+
format,
|
|
143
|
+
);
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
}
|
package/src/commands/help.tsx
CHANGED
|
@@ -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>
|
|
@@ -158,6 +164,9 @@ export function HelpCommand() {
|
|
|
158
164
|
$ ollieshop version create --store-id UUID --name v1 --active
|
|
159
165
|
</Text>
|
|
160
166
|
<Text dimColor>$ ollieshop init --store-id UUID --version-id UUID</Text>
|
|
167
|
+
<Text dimColor>
|
|
168
|
+
$ ollieshop function create --version-id UUID --name myHook
|
|
169
|
+
</Text>
|
|
161
170
|
<Text dimColor>
|
|
162
171
|
$ ollieshop deploy --component-id UUID --name FreeShippingBar --wait
|
|
163
172
|
</Text>
|
|
@@ -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?: 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
|
@@ -67,8 +67,24 @@ export const functionCreateSchema = z.object({
|
|
|
67
67
|
trigger: z
|
|
68
68
|
.string()
|
|
69
69
|
.min(1)
|
|
70
|
-
.
|
|
71
|
-
|
|
70
|
+
.optional()
|
|
71
|
+
.describe(
|
|
72
|
+
"Function trigger URL — absolute http(s) URL or relative path starting with /",
|
|
73
|
+
),
|
|
74
|
+
active: z
|
|
75
|
+
.boolean()
|
|
76
|
+
.default(false)
|
|
77
|
+
.describe("Whether function is active (default: false)"),
|
|
78
|
+
onError: z
|
|
79
|
+
.enum(["throw", "skip"])
|
|
80
|
+
.optional()
|
|
81
|
+
.describe("Error handling: throw (default) or skip"),
|
|
82
|
+
priority: z
|
|
83
|
+
.number()
|
|
84
|
+
.int()
|
|
85
|
+
.min(0)
|
|
86
|
+
.optional()
|
|
87
|
+
.describe("Execution order priority (default: 0)"),
|
|
72
88
|
});
|
|
73
89
|
|
|
74
90
|
export const functionListSchema = z.object({
|
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,
|
package/src/utils/validate.ts
CHANGED
|
@@ -56,3 +56,47 @@ export function validateRequired(
|
|
|
56
56
|
}
|
|
57
57
|
return rejectControlChars(value.trim(), name);
|
|
58
58
|
}
|
|
59
|
+
|
|
60
|
+
export function validateInteger(
|
|
61
|
+
value: unknown,
|
|
62
|
+
name: string,
|
|
63
|
+
opts?: { min?: number },
|
|
64
|
+
): number {
|
|
65
|
+
const n =
|
|
66
|
+
typeof value === "number"
|
|
67
|
+
? value
|
|
68
|
+
: typeof value === "string" && value.trim() !== ""
|
|
69
|
+
? Number(value)
|
|
70
|
+
: Number.NaN;
|
|
71
|
+
if (!Number.isFinite(n) || !Number.isInteger(n)) {
|
|
72
|
+
throw new Error(`Invalid ${name}: "${String(value)}". Must be an integer.`);
|
|
73
|
+
}
|
|
74
|
+
if (opts?.min !== undefined && n < opts.min) {
|
|
75
|
+
throw new Error(`Invalid ${name}: ${n}. Must be >= ${opts.min}.`);
|
|
76
|
+
}
|
|
77
|
+
return n;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function validateTriggerUrl(value: string, name: string): string {
|
|
81
|
+
rejectControlChars(value, name);
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
if (trimmed === "") {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Invalid ${name}: must be a non-empty absolute http(s) URL or a relative path starting with "/".`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
if (trimmed.startsWith("/")) {
|
|
89
|
+
return trimmed;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const parsed = new URL(trimmed);
|
|
93
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
94
|
+
throw new Error("unsupported protocol");
|
|
95
|
+
}
|
|
96
|
+
return trimmed;
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Invalid ${name}: "${value}". Must be an absolute http(s) URL or a relative path starting with "/".`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|