@shipeasy/openapi 1.0.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/dist/index.d.ts +15 -0
- package/dist/index.js +4604 -0
- package/dist/resources/configs.d.ts +677 -0
- package/dist/resources/configs.js +2817 -0
- package/dist/resources/experiments.d.ts +775 -0
- package/dist/resources/experiments.js +649 -0
- package/dist/resources/gates.d.ts +670 -0
- package/dist/resources/gates.js +473 -0
- package/dist/resources/index.d.ts +2522 -0
- package/dist/resources/index.js +4528 -0
- package/dist/resources/killswitches.d.ts +432 -0
- package/dist/resources/killswitches.js +2813 -0
- package/dist/resources/universes.d.ts +182 -0
- package/dist/resources/universes.js +224 -0
- package/dist/transport.d.ts +30 -0
- package/dist/transport.js +44 -0
- package/openapi.json +6011 -0
- package/package.json +58 -0
|
@@ -0,0 +1,4528 @@
|
|
|
1
|
+
// src/resources/gates.ts
|
|
2
|
+
import { z as z2 } from "zod";
|
|
3
|
+
|
|
4
|
+
// ../core/src/schemas/gates.ts
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
var gateRuleSchema = z.object({
|
|
7
|
+
attr: z.string().min(1).describe(
|
|
8
|
+
"Attribute key on the evaluation context (e.g. `country`, `plan`, `email`). Matched case-sensitively against `Shipeasy.checkGate(user, name)` input."
|
|
9
|
+
),
|
|
10
|
+
op: z.enum(["eq", "neq", "in", "not_in", "gt", "gte", "lt", "lte", "contains", "regex"]).describe(
|
|
11
|
+
"Comparison operator. Equality: `eq`/`neq`. Set membership: `in`/`not_in` (value is an array). Numeric order: `gt`/`gte`/`lt`/`lte`. String: `contains` (substring), `regex` (JS-flavour pattern, value is the pattern string)."
|
|
12
|
+
),
|
|
13
|
+
value: z.unknown().describe(
|
|
14
|
+
"Operand. Shape depends on `op`: scalar for `eq/neq/gt/gte/lt/lte/contains`; array of scalars for `in/not_in`; string pattern for `regex`."
|
|
15
|
+
)
|
|
16
|
+
}).describe(
|
|
17
|
+
"Single targeting predicate. A gate's `rules` array is evaluated with AND semantics; use the gatekeeper `stack` for OR / fall-through logic."
|
|
18
|
+
);
|
|
19
|
+
var gateNameSchema = z.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/, "lowercase, digits, _/-; max 64 chars").describe(
|
|
20
|
+
"Stable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Lowercase letters, digits, `_` or `-`, must start with a letter/digit, max 64 chars. Immutable after create \u2014 rename = delete + recreate."
|
|
21
|
+
);
|
|
22
|
+
var stackedConditionSchema = z.object({
|
|
23
|
+
id: z.string().min(1).describe(
|
|
24
|
+
"Client-supplied stable id for the stack entry (used for React keys, audit diffs)."
|
|
25
|
+
),
|
|
26
|
+
type: z.literal("condition").describe("Discriminator. `condition` = rule-based predicate sub-gate."),
|
|
27
|
+
name: z.string().optional().describe("Human label shown in the dashboard."),
|
|
28
|
+
fromTemplate: z.string().nullable().optional().describe(
|
|
29
|
+
"Id of the project template this entry was seeded from, or `null` if hand-authored."
|
|
30
|
+
),
|
|
31
|
+
pass: z.enum(["all", "any"]).optional().describe("Combinator across `rules`. `all` = AND (default), `any` = OR."),
|
|
32
|
+
rules: z.array(gateRuleSchema).default([]).describe("Predicates evaluated under `pass`. Empty array = match nothing."),
|
|
33
|
+
locked: z.boolean().optional().describe("If `true`, dashboard hides edit controls (e.g. the trailing public-floor entry).")
|
|
34
|
+
}).describe(
|
|
35
|
+
"Rule-based sub-gate. When matched, evaluation short-circuits and the gate returns `true` for the current caller."
|
|
36
|
+
);
|
|
37
|
+
var stackedRolloutSchema = z.object({
|
|
38
|
+
id: z.string().min(1).describe("Client-supplied stable id for the stack entry."),
|
|
39
|
+
type: z.literal("rollout").describe("Discriminator. `rollout` = percentage-bucket sub-gate."),
|
|
40
|
+
name: z.string().optional().describe("Human label shown in the dashboard."),
|
|
41
|
+
fromTemplate: z.string().nullable().optional().describe("Source template id, or `null`."),
|
|
42
|
+
rolloutPct: z.number().int().min(0).max(1e4).describe(
|
|
43
|
+
"Bucket size in basis points (0\u201310000 = 0%\u2013100%). `5000` = 50%, `100` = 1%, allows sub-1% precision."
|
|
44
|
+
),
|
|
45
|
+
bucketBy: z.string().optional().describe(
|
|
46
|
+
"Attribute used to hash the caller into a bucket. Defaults to `user.userID`. Use e.g. `accountID` to bucket whole accounts together."
|
|
47
|
+
),
|
|
48
|
+
salt: z.string().optional().describe(
|
|
49
|
+
"Per-entry hash salt. Defaults to the gate's salt. Set explicitly to keep two rollouts independent or correlated across gates."
|
|
50
|
+
),
|
|
51
|
+
locked: z.boolean().optional().describe("If `true`, dashboard hides edit controls.")
|
|
52
|
+
}).describe(
|
|
53
|
+
"Percentage-rollout sub-gate. Hashes `bucketBy` (default `userID`) with `salt` and returns `true` for `rolloutPct/10000` fraction of callers."
|
|
54
|
+
);
|
|
55
|
+
var stackedGateEntrySchema = z.discriminatedUnion("type", [stackedConditionSchema, stackedRolloutSchema]).describe(
|
|
56
|
+
"One entry in the ordered gatekeeper stack. Evaluated top-to-bottom; first matching entry decides. Mix `condition` and `rollout` entries to express `internal-only` + `1% beta` + `50% public` in a single gate."
|
|
57
|
+
);
|
|
58
|
+
var gateMetadataFields = {
|
|
59
|
+
title: z.string().max(140).optional().describe("Human-readable title shown in the dashboard. Free-form, no key format constraint."),
|
|
60
|
+
description: z.string().max(2e3).optional().describe("Long-form description / runbook. Markdown is rendered in the dashboard."),
|
|
61
|
+
folder: z.string().max(64).optional().describe(
|
|
62
|
+
"Folder label for dashboard organisation. Free-form; folders are inferred from the set of values."
|
|
63
|
+
),
|
|
64
|
+
group: z.string().max(64).optional().describe("Group label for dashboard organisation (e.g. team or product area)."),
|
|
65
|
+
owner_email: z.string().max(190).optional().describe("Owner contact. Displayed verbatim; not used for auth.")
|
|
66
|
+
};
|
|
67
|
+
var gateMetadataPatchSchema = z.object(gateMetadataFields);
|
|
68
|
+
var gateCreateSchema = z.object({
|
|
69
|
+
name: gateNameSchema,
|
|
70
|
+
enabled: z.boolean().default(true).describe(
|
|
71
|
+
"Master switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH."
|
|
72
|
+
),
|
|
73
|
+
rollout_pct: z.number().int().min(0).max(1e4).default(0).describe(
|
|
74
|
+
"Initial rollout in basis points (0\u201310000 = 0%\u2013100%). Use `0` to create the gate dark and ramp via PATCH after deploy validation."
|
|
75
|
+
),
|
|
76
|
+
rules: z.array(gateRuleSchema).default([]).describe(
|
|
77
|
+
"Targeting predicates. AND-combined. If non-empty, the gate returns `true` only for callers that satisfy every rule **and** fall under `rollout_pct`."
|
|
78
|
+
),
|
|
79
|
+
salt: z.string().min(1).max(64).optional().describe(
|
|
80
|
+
"Hash salt for percentage bucketing. Auto-generated if omitted. Provide explicitly to keep a gate's buckets stable across delete/recreate. **Immutable after create** \u2014 there is no PATCH for `salt` because changing it would re-bucket every caller."
|
|
81
|
+
),
|
|
82
|
+
stack: z.array(stackedGateEntrySchema).nullable().optional().describe(
|
|
83
|
+
"Optional gatekeeper stack. When provided, takes precedence over `rules` + `rollout_pct` at evaluation time. Omit (or pass `null`) for a flat gate."
|
|
84
|
+
),
|
|
85
|
+
...gateMetadataFields
|
|
86
|
+
}).describe(
|
|
87
|
+
"Body for `POST /api/admin/gates`. At minimum supply `name`; everything else has sensible defaults."
|
|
88
|
+
);
|
|
89
|
+
var gateUpdateSchema = z.object({
|
|
90
|
+
rollout_pct: z.number().int().min(0).max(1e4).optional().describe("New rollout in basis points (0\u201310000 = 0%\u2013100%). Omit to leave unchanged."),
|
|
91
|
+
rules: z.array(gateRuleSchema).optional().describe(
|
|
92
|
+
"Replaces the rule list wholesale. To add a value to an `in` rule, send the full new `rules` array with the augmented `value` (e.g. previous `['US','CA']` \u2192 `['US','CA','GB']`)."
|
|
93
|
+
),
|
|
94
|
+
enabled: z.boolean().optional().describe(
|
|
95
|
+
"Master switch. `false` makes the gate evaluate to `false` for every caller regardless of `rollout_pct`, `rules`, or `stack` \u2014 use as kill switch."
|
|
96
|
+
),
|
|
97
|
+
stack: z.array(stackedGateEntrySchema).nullable().optional().describe(
|
|
98
|
+
"Replaces the gatekeeper stack wholesale. Send `null` to revert to flat `rules` + `rollout_pct` evaluation."
|
|
99
|
+
),
|
|
100
|
+
...gateMetadataFields
|
|
101
|
+
}).describe(
|
|
102
|
+
"Body for `PATCH /api/admin/gates/{id}`. Partial \u2014 only supplied fields change. Array fields (`rules`, `stack`) replace, not merge."
|
|
103
|
+
);
|
|
104
|
+
var gateResponseSchema = z.object({
|
|
105
|
+
id: z.string().describe("Stable opaque gate id (`gat_\u2026`)."),
|
|
106
|
+
name: gateNameSchema,
|
|
107
|
+
/** D1 stores enabled as 0/1; clients see either form. */
|
|
108
|
+
enabled: z.union([z.boolean(), z.number().int().min(0).max(1)]).describe("Master switch. Returned as `1`/`0` from D1; treat truthy as enabled."),
|
|
109
|
+
/** Rollout percentage in basis points (0–10000 = 0%–100%). */
|
|
110
|
+
rolloutPct: z.number().int().min(0).max(1e4).describe("Current rollout in basis points (0\u201310000)."),
|
|
111
|
+
rules: z.array(gateRuleSchema).optional(),
|
|
112
|
+
salt: z.string().optional().describe("Hash salt used for percentage bucketing."),
|
|
113
|
+
title: z.string().nullable().optional(),
|
|
114
|
+
description: z.string().nullable().optional(),
|
|
115
|
+
folder: z.string().nullable().optional(),
|
|
116
|
+
groupName: z.string().nullable().optional().describe("Group label (note: `groupName` in response, `group` in request)."),
|
|
117
|
+
ownerEmail: z.string().nullable().optional().describe("Owner email (note: `ownerEmail` in response, `owner_email` in request)."),
|
|
118
|
+
stack: z.array(stackedGateEntrySchema).nullable().optional(),
|
|
119
|
+
updatedAt: z.string().describe("ISO-8601 timestamp of last mutation.")
|
|
120
|
+
});
|
|
121
|
+
var gateCreateResponseSchema = z.object({
|
|
122
|
+
id: z.string().describe("Newly assigned gate id (`gat_\u2026`)."),
|
|
123
|
+
name: gateNameSchema
|
|
124
|
+
});
|
|
125
|
+
var gateUpdateResponseSchema = z.object({ id: z.string().describe("Gate id that was updated.") }).describe("Update returns only the id \u2014 re-fetch via `GET /api/admin/gates` for the full row.");
|
|
126
|
+
var gateDeleteResponseSchema = z.object({ ok: z.literal(true) });
|
|
127
|
+
var gateToggleResponseSchema = z.object({
|
|
128
|
+
id: z.string(),
|
|
129
|
+
enabled: z.boolean()
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// src/transport.ts
|
|
133
|
+
var ApiError = class extends Error {
|
|
134
|
+
constructor(message, status) {
|
|
135
|
+
super(message);
|
|
136
|
+
this.status = status;
|
|
137
|
+
this.name = "ApiError";
|
|
138
|
+
}
|
|
139
|
+
status;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// src/resources/gates.ts
|
|
143
|
+
var gateListResponseSchema = z2.object({
|
|
144
|
+
data: z2.array(gateResponseSchema),
|
|
145
|
+
next_cursor: z2.string().nullable()
|
|
146
|
+
});
|
|
147
|
+
var BASE = "/api/admin/gates";
|
|
148
|
+
function gatesClient(t) {
|
|
149
|
+
async function list(opts = {}) {
|
|
150
|
+
const query = {};
|
|
151
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
152
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
153
|
+
return t.request("GET", BASE, void 0, query);
|
|
154
|
+
}
|
|
155
|
+
async function listAll() {
|
|
156
|
+
const out = [];
|
|
157
|
+
let cursor;
|
|
158
|
+
do {
|
|
159
|
+
const page = await list({ limit: 500, cursor });
|
|
160
|
+
out.push(...page.data);
|
|
161
|
+
cursor = page.next_cursor ?? void 0;
|
|
162
|
+
} while (cursor);
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
async function resolve(idOrName) {
|
|
166
|
+
const all = await listAll();
|
|
167
|
+
const found = all.find((g) => g.id === idOrName) ?? all.find((g) => g.name === idOrName);
|
|
168
|
+
if (!found) throw new ApiError(`Gate '${idOrName}' not found`, 404);
|
|
169
|
+
return found;
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
list,
|
|
173
|
+
listAll,
|
|
174
|
+
resolve,
|
|
175
|
+
getByName: async (name) => {
|
|
176
|
+
const found = (await listAll()).find((g) => g.name === name);
|
|
177
|
+
if (!found) throw new ApiError(`Gate '${name}' not found`, 404);
|
|
178
|
+
return found;
|
|
179
|
+
},
|
|
180
|
+
create: (input) => t.request("POST", BASE, gateCreateSchema.parse(input)),
|
|
181
|
+
update: (id, input) => t.request("PATCH", `${BASE}/${id}`, gateUpdateSchema.parse(input)),
|
|
182
|
+
delete: (id) => t.request("DELETE", `${BASE}/${id}`),
|
|
183
|
+
enable: (id) => t.request("POST", `${BASE}/${id}/enable`),
|
|
184
|
+
disable: (id) => t.request("POST", `${BASE}/${id}/disable`),
|
|
185
|
+
setRollout: async (name, pct) => {
|
|
186
|
+
if (!Number.isFinite(pct) || pct < 0 || pct > 100) {
|
|
187
|
+
throw new ApiError("rollout must be a number between 0 and 100", 400);
|
|
188
|
+
}
|
|
189
|
+
const gate = await resolve(name);
|
|
190
|
+
return t.request("PATCH", `${BASE}/${gate.id}`, {
|
|
191
|
+
rollout_pct: Math.round(pct * 100)
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
var gatesResource = {
|
|
197
|
+
name: "gates",
|
|
198
|
+
basePath: BASE,
|
|
199
|
+
describeOne: "feature gate",
|
|
200
|
+
describeMany: "feature gates",
|
|
201
|
+
tag: {
|
|
202
|
+
name: "Gates",
|
|
203
|
+
description: [
|
|
204
|
+
"Feature gates: boolean flags evaluated at runtime against project rules + a percentage rollout.",
|
|
205
|
+
"",
|
|
206
|
+
"**Identity.** Each gate is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars) which is what SDKs pass to `Shipeasy.checkGate(user, '<name>')`. The `name` is immutable \u2014 rename means delete + recreate.",
|
|
207
|
+
"",
|
|
208
|
+
"**Evaluation model.** A gate returns `true` when (a) `enabled` is true, and (b) the caller satisfies the gate's rules. There are two evaluation shapes:",
|
|
209
|
+
"- **Flat** \u2014 `rules` (AND-combined predicates) gate the caller, then `rollout_pct` (basis points, 0\u201310000) hashes them into a bucket. Used for simple `is in X% rollout` gates.",
|
|
210
|
+
"- **Gatekeeper stack** \u2014 an ordered array of `condition` and `rollout` sub-gates, evaluated top-to-bottom; first match wins. Used to express `internal-only \u222A 1% beta \u222A 50% public` in one gate. When `stack` is present it takes precedence over the flat fields.",
|
|
211
|
+
"",
|
|
212
|
+
"**Rules.** Each rule is `{ attr, op, value }`. Supported ops: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `regex`. Attribute names match the keys on the SDK evaluation context (e.g. `country`, `plan`, `email`).",
|
|
213
|
+
"",
|
|
214
|
+
"**Rollout basis points.** `rollout_pct` is in **basis points**, not percent. `0` = 0%, `100` = 1%, `5000` = 50%, `10000` = 100%. This allows sub-1% precision (e.g. `7` = 0.07%).",
|
|
215
|
+
"",
|
|
216
|
+
"**Lifecycle.** Create dark (`rollout_pct: 0`) \u2192 attach rules \u2192 ramp via PATCH \u2192 flip kill-switch via `disable`/`enable` \u2192 delete once retired. Deletion is blocked while a running experiment references the gate as a targeting gate."
|
|
217
|
+
].join("\n")
|
|
218
|
+
},
|
|
219
|
+
schemas: {
|
|
220
|
+
create: gateCreateSchema,
|
|
221
|
+
update: gateUpdateSchema
|
|
222
|
+
},
|
|
223
|
+
/** Custom action endpoints beyond CRUD (POST <basePath>/:id/<action>). */
|
|
224
|
+
actions: [
|
|
225
|
+
{ name: "enable", description: "Enable a gate" },
|
|
226
|
+
{ name: "disable", description: "Disable a gate" }
|
|
227
|
+
],
|
|
228
|
+
/**
|
|
229
|
+
* Full operation list for the OpenAPI doc. Order here is the order shown in
|
|
230
|
+
* the docs sidebar. The typed client (`gatesClient`) is the runtime contract;
|
|
231
|
+
* this is the documented surface.
|
|
232
|
+
*/
|
|
233
|
+
endpoints: [
|
|
234
|
+
{
|
|
235
|
+
operationId: "listGates",
|
|
236
|
+
method: "GET",
|
|
237
|
+
path: "",
|
|
238
|
+
summary: "List feature gates",
|
|
239
|
+
description: "Returns a single page of gates ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate.",
|
|
240
|
+
response: gateListResponseSchema,
|
|
241
|
+
examples: {
|
|
242
|
+
response: {
|
|
243
|
+
data: [
|
|
244
|
+
{
|
|
245
|
+
id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
|
|
246
|
+
name: "checkout_v2",
|
|
247
|
+
enabled: 1,
|
|
248
|
+
rolloutPct: 5e3,
|
|
249
|
+
rules: [
|
|
250
|
+
{ attr: "country", op: "in", value: ["US", "CA", "GB"] },
|
|
251
|
+
{ attr: "plan", op: "neq", value: "free" }
|
|
252
|
+
],
|
|
253
|
+
salt: "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
|
|
254
|
+
title: "Checkout v2",
|
|
255
|
+
description: "New checkout flow. Pro users in US/CA/GB only.",
|
|
256
|
+
folder: "checkout",
|
|
257
|
+
groupName: "growth",
|
|
258
|
+
ownerEmail: "ana@example.com",
|
|
259
|
+
stack: null,
|
|
260
|
+
updatedAt: "2026-05-09T16:01:22.000Z"
|
|
261
|
+
}
|
|
262
|
+
],
|
|
263
|
+
next_cursor: null
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
useCase: "Snapshot every gate in the project \u2014 for example to render an admin overview or to drive a CI check that asserts no gate is left at 100% in staging."
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
operationId: "createGate",
|
|
270
|
+
method: "POST",
|
|
271
|
+
path: "",
|
|
272
|
+
summary: "Create a feature gate",
|
|
273
|
+
description: [
|
|
274
|
+
"Creates a new gate. Default `enabled: true` at the supplied `rollout_pct` (`0` = fully dark).",
|
|
275
|
+
"",
|
|
276
|
+
"Only `name` is required. Request fields use snake_case (`owner_email`); the GET response returns camelCase (`ownerEmail`, `groupName`).",
|
|
277
|
+
"",
|
|
278
|
+
"Returns `409` if `name` already exists in the project (case-sensitive)."
|
|
279
|
+
].join("\n"),
|
|
280
|
+
successStatus: 201,
|
|
281
|
+
request: gateCreateSchema,
|
|
282
|
+
response: gateCreateResponseSchema,
|
|
283
|
+
examples: {
|
|
284
|
+
requestExamples: {
|
|
285
|
+
minimal: {
|
|
286
|
+
summary: "Minimal \u2014 dark gate, name only",
|
|
287
|
+
description: "Smallest valid body. The gate is created `enabled: true` at `rollout_pct: 0`, with no rules. Ramp it later via PATCH.",
|
|
288
|
+
value: { name: "checkout_v2" }
|
|
289
|
+
},
|
|
290
|
+
targeted: {
|
|
291
|
+
summary: "Targeted rollout \u2014 rules + percentage",
|
|
292
|
+
description: "Gate Pro users in US/CA/GB, then hash 10% of that audience into the on-bucket (`rollout_pct: 1000` = 10.00%). Adds dashboard metadata so the gate is self-documenting.",
|
|
293
|
+
value: {
|
|
294
|
+
name: "checkout_v2",
|
|
295
|
+
rollout_pct: 1e3,
|
|
296
|
+
rules: [
|
|
297
|
+
{ attr: "country", op: "in", value: ["US", "CA", "GB"] },
|
|
298
|
+
{ attr: "plan", op: "neq", value: "free" }
|
|
299
|
+
],
|
|
300
|
+
title: "Checkout v2",
|
|
301
|
+
description: "New checkout flow. Pro users in US/CA/GB only.",
|
|
302
|
+
folder: "checkout",
|
|
303
|
+
group: "growth",
|
|
304
|
+
owner_email: "ana@example.com"
|
|
305
|
+
}
|
|
306
|
+
},
|
|
307
|
+
gatekeeperStack: {
|
|
308
|
+
summary: "Gatekeeper stack \u2014 internal \u222A beta \u222A public",
|
|
309
|
+
description: "Three-layer fall-through: employees always on, opted-in beta accounts always on, then 25% of everyone else. First match wins; evaluation stops as soon as one entry returns `true`.",
|
|
310
|
+
value: {
|
|
311
|
+
name: "checkout_v2",
|
|
312
|
+
title: "Checkout v2",
|
|
313
|
+
owner_email: "ana@example.com",
|
|
314
|
+
stack: [
|
|
315
|
+
{
|
|
316
|
+
id: "01HKJ0EMP",
|
|
317
|
+
type: "condition",
|
|
318
|
+
name: "Employees",
|
|
319
|
+
pass: "any",
|
|
320
|
+
rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: "01HKJ0BETA",
|
|
324
|
+
type: "condition",
|
|
325
|
+
name: "Beta opt-in",
|
|
326
|
+
pass: "all",
|
|
327
|
+
rules: [{ attr: "beta_opt_in", op: "eq", value: true }]
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "01HKJ0PUB",
|
|
331
|
+
type: "rollout",
|
|
332
|
+
name: "Public",
|
|
333
|
+
rolloutPct: 2500,
|
|
334
|
+
bucketBy: "userID",
|
|
335
|
+
locked: true
|
|
336
|
+
}
|
|
337
|
+
]
|
|
338
|
+
}
|
|
339
|
+
},
|
|
340
|
+
disabledOnCreate: {
|
|
341
|
+
summary: "Disabled on create \u2014 provision then enable later",
|
|
342
|
+
description: "Create with `enabled: false` so the gate evaluates to `false` for every caller until a follow-up `POST /{id}/enable`. Useful when provisioning ahead of a launch.",
|
|
343
|
+
value: { name: "checkout_v2", enabled: false, rollout_pct: 0 }
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", name: "checkout_v2" }
|
|
347
|
+
},
|
|
348
|
+
useCase: [
|
|
349
|
+
'- **Dark create + ramp later** \u2014 `{ "name": "checkout_v2" }` at 0% rollout. Ramp via PATCH after deploy validation.',
|
|
350
|
+
"- **Targeted rollout** \u2014 supply `rules` to gate the caller (e.g. only `plan = pro` users) plus a `rollout_pct` to bucket within that audience.",
|
|
351
|
+
"- **Gatekeeper stack** \u2014 supply `stack` instead of `rules`/`rollout_pct` for `internal \u222A beta \u222A public` fall-through. Stack entries evaluated top-to-bottom; first match wins.",
|
|
352
|
+
"- **Dashboard metadata** \u2014 populate `title`, `description`, `folder`, `group`, `owner_email` so the admin UI is self-documenting from day one.",
|
|
353
|
+
"- **Disabled on create** \u2014 pre-provision with `enabled: false` for a future launch; flip on with `POST /{id}/enable` at go-live."
|
|
354
|
+
].join("\n")
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
operationId: "updateGate",
|
|
358
|
+
method: "PATCH",
|
|
359
|
+
path: "/{id}",
|
|
360
|
+
summary: "Update a feature gate",
|
|
361
|
+
description: [
|
|
362
|
+
"Partial update \u2014 only supplied fields change. Array fields (`rules`, `stack`) **replace** wholesale; there is no merge or append.",
|
|
363
|
+
"",
|
|
364
|
+
"`name` and the gate id are immutable. The response carries only `{ id }` \u2014 re-fetch via `GET /api/admin/gates` for the new row."
|
|
365
|
+
].join("\n"),
|
|
366
|
+
pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
|
|
367
|
+
request: gateUpdateSchema,
|
|
368
|
+
response: gateUpdateResponseSchema,
|
|
369
|
+
examples: {
|
|
370
|
+
requestExamples: {
|
|
371
|
+
rampRollout: {
|
|
372
|
+
summary: "Ramp rollout \u2014 0 \u2192 50%",
|
|
373
|
+
description: "Single-field patch. `5000` basis points = 50%.",
|
|
374
|
+
value: { rollout_pct: 5e3 }
|
|
375
|
+
},
|
|
376
|
+
killSwitch: {
|
|
377
|
+
summary: "Kill switch \u2014 disable the gate",
|
|
378
|
+
description: "Forces evaluation to `false` for every caller, regardless of rules or rollout. Same effect as `POST /{id}/disable` but composable with other patches.",
|
|
379
|
+
value: { enabled: false }
|
|
380
|
+
},
|
|
381
|
+
extendInRule: {
|
|
382
|
+
summary: "Add a value to an `in` rule",
|
|
383
|
+
description: "Array fields replace wholesale \u2014 there is no per-rule patch. To add `'AU'` to a country whitelist that previously held `['US','CA','GB']`, send the full new `rules` array. Same pattern applies to `not_in`.",
|
|
384
|
+
value: {
|
|
385
|
+
rules: [
|
|
386
|
+
{ attr: "country", op: "in", value: ["US", "CA", "GB", "AU"] },
|
|
387
|
+
{ attr: "plan", op: "neq", value: "free" }
|
|
388
|
+
]
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
replaceStack: {
|
|
392
|
+
summary: "Replace gatekeeper stack",
|
|
393
|
+
description: 'Stack is replace-wholesale. Send the full new ordered list. Pass `"stack": null` to revert the gate to flat `rules` + `rollout_pct` evaluation.',
|
|
394
|
+
value: {
|
|
395
|
+
stack: [
|
|
396
|
+
{
|
|
397
|
+
id: "01HKJ0EMP",
|
|
398
|
+
type: "condition",
|
|
399
|
+
name: "Employees",
|
|
400
|
+
pass: "any",
|
|
401
|
+
rules: [{ attr: "email", op: "regex", value: "@acme\\.com$" }]
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
id: "01HKJ0PUB",
|
|
405
|
+
type: "rollout",
|
|
406
|
+
name: "Public",
|
|
407
|
+
rolloutPct: 5e3,
|
|
408
|
+
locked: true
|
|
409
|
+
}
|
|
410
|
+
]
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
updateMetadata: {
|
|
414
|
+
summary: "Update dashboard metadata",
|
|
415
|
+
description: "Any subset of `title`, `description`, `folder`, `group`, `owner_email`. Snake-case in the request; the GET response returns camelCase (`groupName`, `ownerEmail`).",
|
|
416
|
+
value: {
|
|
417
|
+
title: "Checkout v2 (50%)",
|
|
418
|
+
description: "Bumped to 50% after passing canary. Added AU.",
|
|
419
|
+
folder: "checkout",
|
|
420
|
+
owner_email: "ana@example.com"
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc" }
|
|
425
|
+
},
|
|
426
|
+
useCase: [
|
|
427
|
+
'- **Ramp rollout** \u2014 `{ "rollout_pct": 5000 }` for 50%. Basis points (0\u201310000); `100` = 1%.',
|
|
428
|
+
'- **Kill switch** \u2014 `{ "enabled": false }`. Forces evaluation to `false` for every caller regardless of rules/rollout. Re-enable with `POST /{id}/enable` or `{ "enabled": true }`.',
|
|
429
|
+
'- **Modify a rule\'s `in` set** \u2014 send the full new `rules` array. To add `\'GB\'` to `[\'US\',\'CA\']`: `{ "rules": [{ "attr": "country", "op": "in", "value": ["US","CA","GB"] }] }`. No per-rule patch endpoint.',
|
|
430
|
+
'- **Add targeting from scratch** \u2014 `{ "rules": [{ "attr": "email", "op": "regex", "value": "@acme\\\\.com$" }] }`.',
|
|
431
|
+
'- **Switch to gatekeeper stack** \u2014 send a non-null `stack`. To revert to flat eval, send `{ "stack": null }`.',
|
|
432
|
+
"- **Update metadata** \u2014 any subset of `title`, `description`, `folder`, `group`, `owner_email`."
|
|
433
|
+
].join("\n")
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
operationId: "deleteGate",
|
|
437
|
+
method: "DELETE",
|
|
438
|
+
path: "/{id}",
|
|
439
|
+
summary: "Delete a feature gate",
|
|
440
|
+
description: "Soft-deletes the gate. Returns 409 if the gate is still referenced by a running experiment as a targeting gate \u2014 stop the experiment first.",
|
|
441
|
+
pathParams: { id: "Stable opaque gate id (`gat_\u2026`)." },
|
|
442
|
+
response: gateDeleteResponseSchema,
|
|
443
|
+
examples: { response: { ok: true } },
|
|
444
|
+
useCase: "Tear down a gate after a feature has fully shipped and the rollout flag is no longer needed."
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
operationId: "enableGate",
|
|
448
|
+
method: "POST",
|
|
449
|
+
path: "/{id}/enable",
|
|
450
|
+
summary: "Enable a gate",
|
|
451
|
+
description: "Sets `enabled: true`. The current `rollout_pct` is preserved.",
|
|
452
|
+
pathParams: { id: "Stable opaque gate id." },
|
|
453
|
+
response: gateToggleResponseSchema,
|
|
454
|
+
examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: true } },
|
|
455
|
+
useCase: "Re-enable a previously disabled gate without re-issuing a full update."
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
operationId: "disableGate",
|
|
459
|
+
method: "POST",
|
|
460
|
+
path: "/{id}/disable",
|
|
461
|
+
summary: "Disable a gate",
|
|
462
|
+
description: "Sets `enabled: false` so the gate evaluates to `false` for every caller, regardless of `rollout_pct` or `rules`. Use as a quick kill switch.",
|
|
463
|
+
pathParams: { id: "Stable opaque gate id." },
|
|
464
|
+
response: gateToggleResponseSchema,
|
|
465
|
+
examples: { response: { id: "gat_01j7w7m9q4hxbf6npe6s9zr3vc", enabled: false } },
|
|
466
|
+
useCase: "Flip a gate off in production without redeploying \u2014 the canonical kill-switch flow."
|
|
467
|
+
}
|
|
468
|
+
]
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/resources/experiments.ts
|
|
472
|
+
import { z as z4 } from "zod";
|
|
473
|
+
|
|
474
|
+
// ../core/src/schemas/experiments.ts
|
|
475
|
+
import { z as z3 } from "zod";
|
|
476
|
+
var experimentNameSchema = z3.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).describe(
|
|
477
|
+
"Stable experiment key (a-z, 0-9, `_`/`-`, must start with letter/digit, max 64 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create."
|
|
478
|
+
);
|
|
479
|
+
var experimentGroupSchema = z3.object({
|
|
480
|
+
name: z3.string().min(1).max(64).describe("Group label (e.g. `control`, `treatment_a`). Max 64 chars."),
|
|
481
|
+
weight: z3.number().int().min(0).max(1e4).describe(
|
|
482
|
+
"Allocation weight in basis points (0\u201310000). The sum across all groups must equal exactly **10000** (100%)."
|
|
483
|
+
),
|
|
484
|
+
params: z3.record(z3.string(), z3.unknown()).default({}).describe(
|
|
485
|
+
"Per-group parameter values delivered to the SDK when a caller is hashed into this group. Keys must match the experiment's `params` schema."
|
|
486
|
+
)
|
|
487
|
+
}).describe("One variant in the experiment. Weights across all groups must sum to 10000.");
|
|
488
|
+
var paramSchemaSchema = z3.record(z3.string(), z3.enum(["string", "bool", "number"])).describe(
|
|
489
|
+
"Map of param-name \u2192 scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`."
|
|
490
|
+
);
|
|
491
|
+
var experimentCreateSchema = z3.object({
|
|
492
|
+
name: experimentNameSchema,
|
|
493
|
+
description: z3.string().max(2e3).nullable().default(null).describe("Free-form description. Max 2000 chars, markdown rendered in the dashboard."),
|
|
494
|
+
tag: z3.string().max(64).nullable().default(null).describe("Optional tag used to group experiments in the dashboard."),
|
|
495
|
+
universe: z3.string().min(1).describe(
|
|
496
|
+
"Name of an existing universe in the project. Returns `422` if the universe doesn't exist."
|
|
497
|
+
),
|
|
498
|
+
targeting_gate: z3.string().nullable().default(null).describe(
|
|
499
|
+
"Optional gate name. Only callers that pass the gate are enrolled in the experiment."
|
|
500
|
+
),
|
|
501
|
+
allocation_pct: z3.number().int().min(0).max(1e4).default(0).describe(
|
|
502
|
+
"Share of the (gated) audience allocated to the experiment, in basis points (0\u201310000 = 0%\u2013100%). `0` = unallocated. Immutable while the experiment is running."
|
|
503
|
+
),
|
|
504
|
+
salt: z3.string().min(1).max(64).optional().describe("Hash salt for bucketing. Auto-generated if omitted. Immutable while running."),
|
|
505
|
+
params: paramSchemaSchema.default({}),
|
|
506
|
+
groups: z3.array(experimentGroupSchema).min(2).describe(
|
|
507
|
+
"Two or more variants. Weights must sum to exactly 10000 (100%). Immutable while running."
|
|
508
|
+
),
|
|
509
|
+
significance_threshold: z3.number().min(1e-4).max(0.5).default(0.05).describe(
|
|
510
|
+
"p-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher."
|
|
511
|
+
),
|
|
512
|
+
min_runtime_days: z3.number().int().min(0).max(365).default(0).describe("Minimum days the experiment must run before results are considered conclusive."),
|
|
513
|
+
min_sample_size: z3.number().int().min(1).default(100).describe("Minimum exposures per group before results are considered conclusive."),
|
|
514
|
+
sequential_testing: z3.boolean().default(false).describe(
|
|
515
|
+
"Enable sequential testing (always-valid p-values). Requires Premium plan or higher."
|
|
516
|
+
)
|
|
517
|
+
}).refine(
|
|
518
|
+
(e) => e.groups.reduce((s, g) => s + g.weight, 0) === 1e4,
|
|
519
|
+
"groups weights must sum to exactly 10000"
|
|
520
|
+
).describe(
|
|
521
|
+
"Body for `POST /api/admin/experiments`. `name`, `universe`, and `groups` (\u22652, weights sum to 10000) required."
|
|
522
|
+
);
|
|
523
|
+
var experimentUpdateSchema = z3.object({
|
|
524
|
+
name: experimentNameSchema.optional(),
|
|
525
|
+
description: z3.string().max(2e3).nullable().optional(),
|
|
526
|
+
tag: z3.string().max(64).nullable().optional(),
|
|
527
|
+
targeting_gate: z3.string().nullable().optional(),
|
|
528
|
+
allocation_pct: z3.number().int().min(0).max(1e4).optional().describe("Basis-points allocation (0\u201310000). Immutable while the experiment is running."),
|
|
529
|
+
salt: z3.string().min(1).max(64).optional().describe("Hash salt. Immutable while running."),
|
|
530
|
+
universe: z3.string().optional().describe(
|
|
531
|
+
"New universe name. Immutable while running. Returns `422` if the universe doesn't exist."
|
|
532
|
+
),
|
|
533
|
+
params: paramSchemaSchema.optional(),
|
|
534
|
+
groups: z3.array(experimentGroupSchema).min(2).optional().describe("Replacement groups. Weights must sum to 10000. Immutable while running."),
|
|
535
|
+
significance_threshold: z3.number().min(1e-4).max(0.5).optional(),
|
|
536
|
+
min_runtime_days: z3.number().int().min(0).max(365).optional(),
|
|
537
|
+
min_sample_size: z3.number().int().min(1).optional(),
|
|
538
|
+
sequential_testing: z3.boolean().optional()
|
|
539
|
+
}).describe(
|
|
540
|
+
"Body for `PATCH /api/admin/experiments/{id}`. Partial \u2014 only supplied fields change. `allocation_pct`, `groups`, `salt`, `universe`, `params` are immutable while the experiment is running (stop first)."
|
|
541
|
+
);
|
|
542
|
+
var experimentStatusUpdateSchema = z3.object({
|
|
543
|
+
status: z3.enum(["draft", "running", "stopped", "archived"]).describe(
|
|
544
|
+
"Target status. Allowed transitions: `draft \u2192 running`, `running \u2192 stopped`, `stopped \u2192 archived`, `draft \u2192 archived`. Restarting an archived experiment is not allowed."
|
|
545
|
+
)
|
|
546
|
+
}).describe("Body for `POST /api/admin/experiments/{id}/status`.");
|
|
547
|
+
var experimentMetricsUpdateSchema = z3.object({
|
|
548
|
+
metrics: z3.array(
|
|
549
|
+
z3.object({
|
|
550
|
+
metric_id: z3.string().describe("Existing metric id in the project."),
|
|
551
|
+
role: z3.enum(["goal", "guardrail", "secondary"]).describe(
|
|
552
|
+
"Metric role. `goal` drives the decision, `guardrail` blocks ship if degraded, `secondary` is informational."
|
|
553
|
+
)
|
|
554
|
+
})
|
|
555
|
+
).describe("Replacement metrics list \u2014 replaces the current attachments wholesale.")
|
|
556
|
+
}).describe(
|
|
557
|
+
"Body for `POST /api/admin/experiments/{id}/metrics`. Replaces the experiment's metric attachments wholesale. Returns `422` if any `metric_id` is unknown."
|
|
558
|
+
);
|
|
559
|
+
var experimentResponseSchema = z3.object({
|
|
560
|
+
id: z3.string().describe("Stable opaque experiment id (`exp_\u2026`)."),
|
|
561
|
+
name: experimentNameSchema,
|
|
562
|
+
description: z3.string().nullable(),
|
|
563
|
+
tag: z3.string().nullable(),
|
|
564
|
+
status: z3.enum(["draft", "running", "stopped", "archived"]),
|
|
565
|
+
universe: z3.string().describe("Universe name this experiment draws from."),
|
|
566
|
+
targetingGate: z3.string().nullable(),
|
|
567
|
+
allocationPct: z3.number().int().describe("Allocation in basis points (0\u201310000)."),
|
|
568
|
+
salt: z3.string(),
|
|
569
|
+
params: z3.record(z3.string(), z3.enum(["string", "bool", "number"])),
|
|
570
|
+
groups: z3.array(experimentGroupSchema),
|
|
571
|
+
significanceThreshold: z3.number(),
|
|
572
|
+
minRuntimeDays: z3.number().int(),
|
|
573
|
+
minSampleSize: z3.number().int(),
|
|
574
|
+
sequentialTesting: z3.boolean(),
|
|
575
|
+
startedAt: z3.string().nullable().describe("ISO-8601 timestamp the experiment last transitioned to `running`, or `null`."),
|
|
576
|
+
stoppedAt: z3.string().nullable().optional(),
|
|
577
|
+
updatedAt: z3.string()
|
|
578
|
+
});
|
|
579
|
+
var experimentCreateResponseSchema = z3.object({
|
|
580
|
+
id: z3.string().describe("Newly assigned experiment id."),
|
|
581
|
+
name: experimentNameSchema
|
|
582
|
+
});
|
|
583
|
+
var experimentUpdateResponseSchema = z3.object({
|
|
584
|
+
id: z3.string().describe("Experiment id that was updated.")
|
|
585
|
+
});
|
|
586
|
+
var experimentDeleteResponseSchema = z3.object({ ok: z3.literal(true) });
|
|
587
|
+
var experimentStatusResponseSchema = z3.object({
|
|
588
|
+
id: z3.string(),
|
|
589
|
+
status: z3.enum(["draft", "running", "stopped", "archived"])
|
|
590
|
+
});
|
|
591
|
+
var experimentMetricsResponseSchema = z3.object({
|
|
592
|
+
id: z3.string(),
|
|
593
|
+
metrics: z3.array(
|
|
594
|
+
z3.object({
|
|
595
|
+
metric_id: z3.string(),
|
|
596
|
+
role: z3.enum(["goal", "guardrail", "secondary"])
|
|
597
|
+
})
|
|
598
|
+
)
|
|
599
|
+
});
|
|
600
|
+
var experimentResultRowSchema = z3.object({
|
|
601
|
+
metric: z3.string(),
|
|
602
|
+
group_name: z3.string(),
|
|
603
|
+
ds: z3.string().describe("Date slice (`YYYY-MM-DD`)."),
|
|
604
|
+
n: z3.number().nullable(),
|
|
605
|
+
mean: z3.number().nullable(),
|
|
606
|
+
delta_pct: z3.number().nullable(),
|
|
607
|
+
p_value: z3.number().nullable(),
|
|
608
|
+
srm_detected: z3.number().nullable().describe("`1` if sample-ratio mismatch detected, else `0`.")
|
|
609
|
+
});
|
|
610
|
+
var experimentResultsResponseSchema = z3.object({
|
|
611
|
+
experiment: z3.object({
|
|
612
|
+
id: z3.string(),
|
|
613
|
+
name: experimentNameSchema,
|
|
614
|
+
status: z3.enum(["draft", "running", "stopped", "archived"])
|
|
615
|
+
}),
|
|
616
|
+
results: z3.array(experimentResultRowSchema)
|
|
617
|
+
});
|
|
618
|
+
var experimentTimeseriesResponseSchema = z3.object({
|
|
619
|
+
experiment: z3.object({
|
|
620
|
+
id: z3.string(),
|
|
621
|
+
name: experimentNameSchema,
|
|
622
|
+
status: z3.enum(["draft", "running", "stopped", "archived"])
|
|
623
|
+
}),
|
|
624
|
+
series: z3.array(experimentResultRowSchema)
|
|
625
|
+
});
|
|
626
|
+
var experimentReanalyzeResponseSchema = z3.object({
|
|
627
|
+
id: z3.string(),
|
|
628
|
+
queued: z3.literal(true)
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
// src/resources/experiments.ts
|
|
632
|
+
var experimentListResponseSchema = z4.object({
|
|
633
|
+
data: z4.array(experimentResponseSchema),
|
|
634
|
+
next_cursor: z4.string().nullable()
|
|
635
|
+
});
|
|
636
|
+
var BASE2 = "/api/admin/experiments";
|
|
637
|
+
function experimentsClient(t) {
|
|
638
|
+
async function list(opts = {}) {
|
|
639
|
+
const query = {};
|
|
640
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
641
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
642
|
+
return t.request("GET", BASE2, void 0, query);
|
|
643
|
+
}
|
|
644
|
+
async function listAll() {
|
|
645
|
+
const out = [];
|
|
646
|
+
let cursor;
|
|
647
|
+
do {
|
|
648
|
+
const page = await list({ limit: 500, cursor });
|
|
649
|
+
out.push(...page.data);
|
|
650
|
+
cursor = page.next_cursor ?? void 0;
|
|
651
|
+
} while (cursor);
|
|
652
|
+
return out;
|
|
653
|
+
}
|
|
654
|
+
async function resolve(idOrName) {
|
|
655
|
+
try {
|
|
656
|
+
return await t.request("GET", `${BASE2}/${idOrName}`);
|
|
657
|
+
} catch (err) {
|
|
658
|
+
if (!(err instanceof ApiError) || err.status !== 404) throw err;
|
|
659
|
+
}
|
|
660
|
+
const all = await listAll();
|
|
661
|
+
const found = all.find((e) => e.name === idOrName);
|
|
662
|
+
if (!found) throw new ApiError(`Experiment '${idOrName}' not found`, 404);
|
|
663
|
+
return found;
|
|
664
|
+
}
|
|
665
|
+
async function setStatus(id, status) {
|
|
666
|
+
return t.request(
|
|
667
|
+
"POST",
|
|
668
|
+
`${BASE2}/${id}/status`,
|
|
669
|
+
experimentStatusUpdateSchema.parse({ status })
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return {
|
|
673
|
+
list,
|
|
674
|
+
listAll,
|
|
675
|
+
resolve,
|
|
676
|
+
get: (id) => t.request("GET", `${BASE2}/${id}`),
|
|
677
|
+
create: (input) => t.request("POST", BASE2, experimentCreateSchema.parse(input)),
|
|
678
|
+
update: (id, input) => t.request("PATCH", `${BASE2}/${id}`, experimentUpdateSchema.parse(input)),
|
|
679
|
+
delete: (id) => t.request("DELETE", `${BASE2}/${id}`),
|
|
680
|
+
setStatus,
|
|
681
|
+
start: (id) => setStatus(id, "running"),
|
|
682
|
+
stop: (id) => setStatus(id, "stopped"),
|
|
683
|
+
archive: (id) => setStatus(id, "archived"),
|
|
684
|
+
setMetrics: (id, input) => t.request("POST", `${BASE2}/${id}/metrics`, experimentMetricsUpdateSchema.parse(input)),
|
|
685
|
+
results: (id) => t.request("GET", `${BASE2}/${id}/results`),
|
|
686
|
+
timeseries: (id, metric) => t.request(
|
|
687
|
+
"GET",
|
|
688
|
+
`${BASE2}/${id}/timeseries`,
|
|
689
|
+
void 0,
|
|
690
|
+
metric ? { metric } : void 0
|
|
691
|
+
),
|
|
692
|
+
reanalyze: (id) => t.request("POST", `${BASE2}/${id}/reanalyze`)
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
var SAMPLE_EXP = {
|
|
696
|
+
id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
|
|
697
|
+
name: "checkout_button_color",
|
|
698
|
+
description: "Test green vs. blue CTA on the checkout page.",
|
|
699
|
+
tag: "checkout",
|
|
700
|
+
status: "running",
|
|
701
|
+
universe: "primary_users",
|
|
702
|
+
targetingGate: null,
|
|
703
|
+
allocationPct: 5e3,
|
|
704
|
+
salt: "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
|
|
705
|
+
params: { cta_color: "string" },
|
|
706
|
+
groups: [
|
|
707
|
+
{ name: "control", weight: 5e3, params: { cta_color: "blue" } },
|
|
708
|
+
{ name: "treatment", weight: 5e3, params: { cta_color: "green" } }
|
|
709
|
+
],
|
|
710
|
+
significanceThreshold: 0.05,
|
|
711
|
+
minRuntimeDays: 7,
|
|
712
|
+
minSampleSize: 1e3,
|
|
713
|
+
sequentialTesting: false,
|
|
714
|
+
startedAt: "2026-05-01T12:00:00.000Z",
|
|
715
|
+
stoppedAt: null,
|
|
716
|
+
updatedAt: "2026-05-09T18:22:11.000Z"
|
|
717
|
+
};
|
|
718
|
+
var experimentsResource = {
|
|
719
|
+
name: "experiments",
|
|
720
|
+
basePath: BASE2,
|
|
721
|
+
describeOne: "experiment",
|
|
722
|
+
describeMany: "experiments",
|
|
723
|
+
tag: {
|
|
724
|
+
name: "Experiments",
|
|
725
|
+
description: [
|
|
726
|
+
"A/B/n experiments: randomised group assignment plus the analysis pipeline (t-test, sequential testing, SRM detection) on top of a universe.",
|
|
727
|
+
"",
|
|
728
|
+
"**Identity.** Stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Immutable after create.",
|
|
729
|
+
"",
|
|
730
|
+
"**Lifecycle.** `draft \u2192 running \u2192 stopped \u2192 archived`. Transition via `POST /{id}/status`. Restarting an archived experiment is not allowed.",
|
|
731
|
+
"",
|
|
732
|
+
"**Allocation.** `allocation_pct` (basis points, 0\u201310000) is the share of the targeted audience enrolled; `groups[].weight` (must sum to 10000) splits the enrolled audience. `targeting_gate` narrows the eligible audience before allocation.",
|
|
733
|
+
"",
|
|
734
|
+
"**Immutable while running.** `allocation_pct`, `groups`, `salt`, `universe`, `params` cannot be edited on a running experiment \u2014 stop it first.",
|
|
735
|
+
"",
|
|
736
|
+
"**Metrics.** Attach via `POST /{id}/metrics`. Each metric has a role: `goal` drives the decision, `guardrail` blocks ship on regression, `secondary` is informational.",
|
|
737
|
+
"",
|
|
738
|
+
"**Analysis.** Daily cron writes results to D1. Read via `GET /{id}/results` (latest per metric/group/day) or `GET /{id}/timeseries` (full history). `POST /{id}/reanalyze` requeues the analysis pass."
|
|
739
|
+
].join("\n")
|
|
740
|
+
},
|
|
741
|
+
schemas: {
|
|
742
|
+
create: experimentCreateSchema,
|
|
743
|
+
update: experimentUpdateSchema
|
|
744
|
+
},
|
|
745
|
+
actions: [
|
|
746
|
+
{ name: "start", description: "Transition draft experiment to running" },
|
|
747
|
+
{ name: "stop", description: "Stop a running experiment" },
|
|
748
|
+
{ name: "archive", description: "Archive a stopped experiment" },
|
|
749
|
+
{ name: "reanalyze", description: "Re-run analysis pass for the experiment" }
|
|
750
|
+
],
|
|
751
|
+
endpoints: [
|
|
752
|
+
{
|
|
753
|
+
operationId: "listExperiments",
|
|
754
|
+
method: "GET",
|
|
755
|
+
path: "",
|
|
756
|
+
summary: "List experiments",
|
|
757
|
+
description: "Returns a single page of non-archived experiments ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate.",
|
|
758
|
+
response: experimentListResponseSchema,
|
|
759
|
+
examples: { response: { data: [SAMPLE_EXP], next_cursor: null } },
|
|
760
|
+
useCase: "Snapshot every active experiment in the project \u2014 e.g. render an overview dashboard or drive a CI check that no experiment has been running past its `min_runtime_days`."
|
|
761
|
+
},
|
|
762
|
+
{
|
|
763
|
+
operationId: "getExperiment",
|
|
764
|
+
method: "GET",
|
|
765
|
+
path: "/{id}",
|
|
766
|
+
summary: "Get one experiment",
|
|
767
|
+
description: "Returns the full experiment row including groups, params, allocation, and lifecycle timestamps.",
|
|
768
|
+
pathParams: { id: "Stable opaque experiment id (`exp_\u2026`) or the experiment's `name`." },
|
|
769
|
+
response: experimentResponseSchema,
|
|
770
|
+
examples: { response: SAMPLE_EXP },
|
|
771
|
+
useCase: "Fetch one experiment to render the detail page or to inspect its current allocation and group weights."
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
operationId: "createExperiment",
|
|
775
|
+
method: "POST",
|
|
776
|
+
path: "",
|
|
777
|
+
summary: "Create an experiment",
|
|
778
|
+
description: [
|
|
779
|
+
"Creates a new experiment in `draft` status. `name`, `universe`, and `groups` are required; everything else has sensible defaults.",
|
|
780
|
+
"",
|
|
781
|
+
"Returns `409` if `name` already exists, `422` if the named universe doesn't exist, `403` if a plan-gated option is set (`sequential_testing`, custom `significance_threshold`) on a plan that doesn't include it."
|
|
782
|
+
].join("\n"),
|
|
783
|
+
successStatus: 201,
|
|
784
|
+
request: experimentCreateSchema,
|
|
785
|
+
response: experimentCreateResponseSchema,
|
|
786
|
+
examples: {
|
|
787
|
+
requestExamples: {
|
|
788
|
+
minimal: {
|
|
789
|
+
summary: "Minimal \u2014 two-group 50/50",
|
|
790
|
+
description: "Smallest valid body. Two groups at 50/50, no allocation, no targeting.",
|
|
791
|
+
value: {
|
|
792
|
+
name: "checkout_button_color",
|
|
793
|
+
universe: "primary_users",
|
|
794
|
+
groups: [
|
|
795
|
+
{ name: "control", weight: 5e3 },
|
|
796
|
+
{ name: "treatment", weight: 5e3 }
|
|
797
|
+
]
|
|
798
|
+
}
|
|
799
|
+
},
|
|
800
|
+
targeted: {
|
|
801
|
+
summary: "Targeted \u2014 gate + allocation + params",
|
|
802
|
+
description: "Gate the audience with a `targeting_gate`, allocate 50% of it, and parameterise the variants.",
|
|
803
|
+
value: {
|
|
804
|
+
name: "checkout_button_color",
|
|
805
|
+
description: "Green CTA on checkout for Pro users.",
|
|
806
|
+
universe: "primary_users",
|
|
807
|
+
targeting_gate: "pro_plan_only",
|
|
808
|
+
allocation_pct: 5e3,
|
|
809
|
+
params: { cta_color: "string" },
|
|
810
|
+
groups: [
|
|
811
|
+
{ name: "control", weight: 5e3, params: { cta_color: "blue" } },
|
|
812
|
+
{ name: "treatment", weight: 5e3, params: { cta_color: "green" } }
|
|
813
|
+
]
|
|
814
|
+
}
|
|
815
|
+
},
|
|
816
|
+
multivariant: {
|
|
817
|
+
summary: "Multivariant \u2014 three groups",
|
|
818
|
+
description: "Three variants splitting 40/30/30. Requires the sum to equal 10000.",
|
|
819
|
+
value: {
|
|
820
|
+
name: "headline_test",
|
|
821
|
+
universe: "primary_users",
|
|
822
|
+
groups: [
|
|
823
|
+
{ name: "control", weight: 4e3 },
|
|
824
|
+
{ name: "alt_a", weight: 3e3 },
|
|
825
|
+
{ name: "alt_b", weight: 3e3 }
|
|
826
|
+
]
|
|
827
|
+
}
|
|
828
|
+
},
|
|
829
|
+
sequential: {
|
|
830
|
+
summary: "Sequential testing \u2014 Premium plan",
|
|
831
|
+
description: "Enable always-valid sequential tests. Requires Premium plan or higher.",
|
|
832
|
+
value: {
|
|
833
|
+
name: "checkout_button_color",
|
|
834
|
+
universe: "primary_users",
|
|
835
|
+
groups: [
|
|
836
|
+
{ name: "control", weight: 5e3 },
|
|
837
|
+
{ name: "treatment", weight: 5e3 }
|
|
838
|
+
],
|
|
839
|
+
sequential_testing: true,
|
|
840
|
+
min_runtime_days: 14
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", name: "checkout_button_color" }
|
|
845
|
+
},
|
|
846
|
+
useCase: [
|
|
847
|
+
"- **Minimal 50/50** \u2014 `name` + `universe` + two equal-weight groups.",
|
|
848
|
+
"- **Targeted rollout** \u2014 supply `targeting_gate` to restrict the eligible audience and `allocation_pct` to enrol a slice of it.",
|
|
849
|
+
"- **Multivariant** \u2014 three or more groups with weights summing to 10000.",
|
|
850
|
+
"- **Sequential testing** \u2014 `sequential_testing: true` for Premium plans."
|
|
851
|
+
].join("\n")
|
|
852
|
+
},
|
|
853
|
+
{
|
|
854
|
+
operationId: "updateExperiment",
|
|
855
|
+
method: "PATCH",
|
|
856
|
+
path: "/{id}",
|
|
857
|
+
summary: "Update an experiment",
|
|
858
|
+
description: [
|
|
859
|
+
"Partial update. `allocation_pct`, `groups`, `salt`, `universe`, `params` are **immutable while running** \u2014 returns `409` if you try. Stop the experiment first.",
|
|
860
|
+
"",
|
|
861
|
+
"Editing `groups` while in `draft` is fine; weights must still sum to 10000."
|
|
862
|
+
].join("\n"),
|
|
863
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
864
|
+
request: experimentUpdateSchema,
|
|
865
|
+
response: experimentUpdateResponseSchema,
|
|
866
|
+
examples: {
|
|
867
|
+
requestExamples: {
|
|
868
|
+
rampAllocation: {
|
|
869
|
+
summary: "Ramp allocation \u2014 0 \u2192 50%",
|
|
870
|
+
description: "Bumps the share of the targeted audience enrolled. Allowed only when the experiment is in `draft`.",
|
|
871
|
+
value: { allocation_pct: 5e3 }
|
|
872
|
+
},
|
|
873
|
+
changeWeights: {
|
|
874
|
+
summary: "Change group weights",
|
|
875
|
+
description: "Replace the groups array \u2014 weights must still sum to 10000.",
|
|
876
|
+
value: {
|
|
877
|
+
groups: [
|
|
878
|
+
{ name: "control", weight: 2500 },
|
|
879
|
+
{ name: "treatment", weight: 7500 }
|
|
880
|
+
]
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
tighterSignificance: {
|
|
884
|
+
summary: "Tighter significance threshold",
|
|
885
|
+
description: "Drop the p-value cutoff from 0.05 \u2192 0.01. Requires Pro plan or higher.",
|
|
886
|
+
value: { significance_threshold: 0.01 }
|
|
887
|
+
},
|
|
888
|
+
updateMetadata: {
|
|
889
|
+
summary: "Update metadata",
|
|
890
|
+
description: "Change description/tag/targeting_gate freely on running experiments.",
|
|
891
|
+
value: {
|
|
892
|
+
description: "Bumped to 50% after passing canary on 25%.",
|
|
893
|
+
tag: "checkout"
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
},
|
|
897
|
+
response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1" }
|
|
898
|
+
},
|
|
899
|
+
useCase: [
|
|
900
|
+
"- **Update metadata** \u2014 `description`, `tag`, `targeting_gate` editable any time.",
|
|
901
|
+
"- **Ramp before launch** \u2014 set `allocation_pct` while still in `draft`.",
|
|
902
|
+
"- **Tighten significance** \u2014 `significance_threshold` (Pro+).",
|
|
903
|
+
"- **Rewire groups** \u2014 replace `groups` wholesale while in `draft`; immutable once running."
|
|
904
|
+
].join("\n")
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
operationId: "deleteExperiment",
|
|
908
|
+
method: "DELETE",
|
|
909
|
+
path: "/{id}",
|
|
910
|
+
summary: "Delete an experiment",
|
|
911
|
+
description: "Archives the experiment (soft-delete via status transition). Returns `409` if the experiment is still `running` \u2014 stop it first.",
|
|
912
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
913
|
+
response: experimentDeleteResponseSchema,
|
|
914
|
+
examples: { response: { ok: true } },
|
|
915
|
+
useCase: "Tear down an experiment after the analysis is signed off."
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
operationId: "setExperimentStatus",
|
|
919
|
+
method: "POST",
|
|
920
|
+
path: "/{id}/status",
|
|
921
|
+
summary: "Transition experiment status",
|
|
922
|
+
description: [
|
|
923
|
+
"Drives the experiment lifecycle. Allowed transitions:",
|
|
924
|
+
"",
|
|
925
|
+
"- `draft \u2192 running` \u2014 starts allocation. Bumps the `startedAt` timestamp.",
|
|
926
|
+
"- `running \u2192 stopped` \u2014 halts allocation. Existing exposures stay in the dataset.",
|
|
927
|
+
"- `stopped \u2192 archived` \u2014 soft-delete.",
|
|
928
|
+
"- `draft \u2192 archived` \u2014 discard an unstarted experiment.",
|
|
929
|
+
"",
|
|
930
|
+
"Restarting an `archived` experiment is not allowed; clone instead. Returns `409` on illegal transitions and `429` if the plan's `experiments_running` limit is exceeded on `\u2192 running`."
|
|
931
|
+
].join("\n"),
|
|
932
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
933
|
+
request: experimentStatusUpdateSchema,
|
|
934
|
+
response: experimentStatusResponseSchema,
|
|
935
|
+
examples: {
|
|
936
|
+
requestExamples: {
|
|
937
|
+
start: {
|
|
938
|
+
summary: "Start \u2014 draft \u2192 running",
|
|
939
|
+
description: "Begins allocation. The experiment counts against the plan's `experiments_running` limit.",
|
|
940
|
+
value: { status: "running" }
|
|
941
|
+
},
|
|
942
|
+
stop: {
|
|
943
|
+
summary: "Stop \u2014 running \u2192 stopped",
|
|
944
|
+
description: "Halts allocation. Existing exposures remain analysable.",
|
|
945
|
+
value: { status: "stopped" }
|
|
946
|
+
},
|
|
947
|
+
archive: {
|
|
948
|
+
summary: "Archive \u2014 stopped \u2192 archived",
|
|
949
|
+
description: "Soft-deletes the experiment. List endpoints exclude archived rows by default.",
|
|
950
|
+
value: { status: "archived" }
|
|
951
|
+
}
|
|
952
|
+
},
|
|
953
|
+
response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", status: "running" }
|
|
954
|
+
},
|
|
955
|
+
useCase: [
|
|
956
|
+
'- **Start** \u2014 `{ "status": "running" }` after wiring up the SDK and verifying targeting on staging.',
|
|
957
|
+
'- **Stop** \u2014 `{ "status": "stopped" }` once the experiment hits its `min_runtime_days` and conclusive results land.',
|
|
958
|
+
'- **Archive** \u2014 `{ "status": "archived" }` to soft-delete after sign-off.'
|
|
959
|
+
].join("\n")
|
|
960
|
+
},
|
|
961
|
+
{
|
|
962
|
+
operationId: "setExperimentMetrics",
|
|
963
|
+
method: "POST",
|
|
964
|
+
path: "/{id}/metrics",
|
|
965
|
+
summary: "Attach metrics",
|
|
966
|
+
description: [
|
|
967
|
+
"Replaces the experiment's metric attachments wholesale. Each entry pairs an existing `metric_id` with a `role` (`goal` / `guardrail` / `secondary`).",
|
|
968
|
+
"",
|
|
969
|
+
"Returns `422` if any `metric_id` doesn't exist in the project. Pass `{ metrics: [] }` to detach everything."
|
|
970
|
+
].join("\n"),
|
|
971
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
972
|
+
request: experimentMetricsUpdateSchema,
|
|
973
|
+
response: experimentMetricsResponseSchema,
|
|
974
|
+
examples: {
|
|
975
|
+
requestExamples: {
|
|
976
|
+
standard: {
|
|
977
|
+
summary: "Standard \u2014 one goal + one guardrail",
|
|
978
|
+
description: "Attaches one decision metric and one safety metric.",
|
|
979
|
+
value: {
|
|
980
|
+
metrics: [
|
|
981
|
+
{ metric_id: "met_checkout_completed", role: "goal" },
|
|
982
|
+
{ metric_id: "met_page_errors", role: "guardrail" }
|
|
983
|
+
]
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
detach: {
|
|
987
|
+
summary: "Detach all",
|
|
988
|
+
description: "Send an empty array to clear every metric.",
|
|
989
|
+
value: { metrics: [] }
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
response: {
|
|
993
|
+
id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
|
|
994
|
+
metrics: [
|
|
995
|
+
{ metric_id: "met_checkout_completed", role: "goal" },
|
|
996
|
+
{ metric_id: "met_page_errors", role: "guardrail" }
|
|
997
|
+
]
|
|
998
|
+
}
|
|
999
|
+
},
|
|
1000
|
+
useCase: [
|
|
1001
|
+
"- **Standard setup** \u2014 one `goal`, one or two `guardrail`, optional `secondary` metrics for diagnostics.",
|
|
1002
|
+
'- **Detach all** \u2014 send `{ "metrics": [] }` before archiving.'
|
|
1003
|
+
].join("\n")
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
operationId: "getExperimentResults",
|
|
1007
|
+
method: "GET",
|
|
1008
|
+
path: "/{id}/results",
|
|
1009
|
+
summary: "Get analysis results",
|
|
1010
|
+
description: "Returns the latest analysis output for the experiment \u2014 one row per metric/group/day, including sample size, mean, % delta vs. control, p-value, and a sample-ratio mismatch flag.",
|
|
1011
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
1012
|
+
response: experimentResultsResponseSchema,
|
|
1013
|
+
examples: {
|
|
1014
|
+
response: {
|
|
1015
|
+
experiment: {
|
|
1016
|
+
id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
|
|
1017
|
+
name: "checkout_button_color",
|
|
1018
|
+
status: "running"
|
|
1019
|
+
},
|
|
1020
|
+
results: [
|
|
1021
|
+
{
|
|
1022
|
+
metric: "checkout_completed",
|
|
1023
|
+
group_name: "control",
|
|
1024
|
+
ds: "2026-05-09",
|
|
1025
|
+
n: 12421,
|
|
1026
|
+
mean: 0.1834,
|
|
1027
|
+
delta_pct: null,
|
|
1028
|
+
p_value: null,
|
|
1029
|
+
srm_detected: 0
|
|
1030
|
+
},
|
|
1031
|
+
{
|
|
1032
|
+
metric: "checkout_completed",
|
|
1033
|
+
group_name: "treatment",
|
|
1034
|
+
ds: "2026-05-09",
|
|
1035
|
+
n: 12519,
|
|
1036
|
+
mean: 0.1922,
|
|
1037
|
+
delta_pct: 4.8,
|
|
1038
|
+
p_value: 0.018,
|
|
1039
|
+
srm_detected: 0
|
|
1040
|
+
}
|
|
1041
|
+
]
|
|
1042
|
+
}
|
|
1043
|
+
},
|
|
1044
|
+
useCase: "Render the results table on the experiment detail page or drive an automated decision once a `goal` metric reaches significance."
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
operationId: "getExperimentTimeseries",
|
|
1048
|
+
method: "GET",
|
|
1049
|
+
path: "/{id}/timeseries",
|
|
1050
|
+
summary: "Get analysis timeseries",
|
|
1051
|
+
description: "Same row shape as `/results`, but returns every daily slice rather than the latest. Filter to a single metric with the `metric` query parameter.",
|
|
1052
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
1053
|
+
queryParams: {
|
|
1054
|
+
metric: {
|
|
1055
|
+
schema: z4.string().optional(),
|
|
1056
|
+
description: "Optional metric name to filter the series."
|
|
1057
|
+
}
|
|
1058
|
+
},
|
|
1059
|
+
response: experimentTimeseriesResponseSchema,
|
|
1060
|
+
examples: {
|
|
1061
|
+
response: {
|
|
1062
|
+
experiment: {
|
|
1063
|
+
id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
|
|
1064
|
+
name: "checkout_button_color",
|
|
1065
|
+
status: "running"
|
|
1066
|
+
},
|
|
1067
|
+
series: [
|
|
1068
|
+
{
|
|
1069
|
+
metric: "checkout_completed",
|
|
1070
|
+
group_name: "treatment",
|
|
1071
|
+
ds: "2026-05-08",
|
|
1072
|
+
n: 11200,
|
|
1073
|
+
mean: 0.1903,
|
|
1074
|
+
delta_pct: 3.9,
|
|
1075
|
+
p_value: 0.034,
|
|
1076
|
+
srm_detected: 0
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
metric: "checkout_completed",
|
|
1080
|
+
group_name: "treatment",
|
|
1081
|
+
ds: "2026-05-09",
|
|
1082
|
+
n: 12519,
|
|
1083
|
+
mean: 0.1922,
|
|
1084
|
+
delta_pct: 4.8,
|
|
1085
|
+
p_value: 0.018,
|
|
1086
|
+
srm_detected: 0
|
|
1087
|
+
}
|
|
1088
|
+
]
|
|
1089
|
+
}
|
|
1090
|
+
},
|
|
1091
|
+
useCase: "Drive a chart of metric movement over the experiment runtime, or sanity-check the lift is monotonic before deciding."
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
operationId: "reanalyzeExperiment",
|
|
1095
|
+
method: "POST",
|
|
1096
|
+
path: "/{id}/reanalyze",
|
|
1097
|
+
summary: "Re-queue analysis",
|
|
1098
|
+
description: "Requeues the daily analysis pass for this experiment outside the normal cron cadence. Useful after attaching a new metric or correcting an event taxonomy. The job runs asynchronously.",
|
|
1099
|
+
pathParams: { id: "Stable opaque experiment id." },
|
|
1100
|
+
response: experimentReanalyzeResponseSchema,
|
|
1101
|
+
examples: { response: { id: "exp_01j7wb12c3d4e5f6g7h8j9k0l1", queued: true } },
|
|
1102
|
+
useCase: "Force-refresh results after wiring up a new metric without waiting for the next nightly cron tick."
|
|
1103
|
+
}
|
|
1104
|
+
]
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
// src/resources/configs.ts
|
|
1108
|
+
import { z as z6 } from "zod";
|
|
1109
|
+
|
|
1110
|
+
// ../core/src/schemas/configs.ts
|
|
1111
|
+
import { z as z5 } from "zod";
|
|
1112
|
+
|
|
1113
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/entity.js
|
|
1114
|
+
var entityKind = /* @__PURE__ */ Symbol.for("drizzle:entityKind");
|
|
1115
|
+
function is(value, type) {
|
|
1116
|
+
if (!value || typeof value !== "object") {
|
|
1117
|
+
return false;
|
|
1118
|
+
}
|
|
1119
|
+
if (value instanceof type) {
|
|
1120
|
+
return true;
|
|
1121
|
+
}
|
|
1122
|
+
if (!Object.prototype.hasOwnProperty.call(type, entityKind)) {
|
|
1123
|
+
throw new Error(
|
|
1124
|
+
`Class "${type.name ?? "<unknown>"}" doesn't look like a Drizzle entity. If this is incorrect and the class is provided by Drizzle, please report this as a bug.`
|
|
1125
|
+
);
|
|
1126
|
+
}
|
|
1127
|
+
let cls = Object.getPrototypeOf(value).constructor;
|
|
1128
|
+
if (cls) {
|
|
1129
|
+
while (cls) {
|
|
1130
|
+
if (entityKind in cls && cls[entityKind] === type[entityKind]) {
|
|
1131
|
+
return true;
|
|
1132
|
+
}
|
|
1133
|
+
cls = Object.getPrototypeOf(cls);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
return false;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/column.js
|
|
1140
|
+
var Column = class {
|
|
1141
|
+
constructor(table, config) {
|
|
1142
|
+
this.table = table;
|
|
1143
|
+
this.config = config;
|
|
1144
|
+
this.name = config.name;
|
|
1145
|
+
this.keyAsName = config.keyAsName;
|
|
1146
|
+
this.notNull = config.notNull;
|
|
1147
|
+
this.default = config.default;
|
|
1148
|
+
this.defaultFn = config.defaultFn;
|
|
1149
|
+
this.onUpdateFn = config.onUpdateFn;
|
|
1150
|
+
this.hasDefault = config.hasDefault;
|
|
1151
|
+
this.primary = config.primaryKey;
|
|
1152
|
+
this.isUnique = config.isUnique;
|
|
1153
|
+
this.uniqueName = config.uniqueName;
|
|
1154
|
+
this.uniqueType = config.uniqueType;
|
|
1155
|
+
this.dataType = config.dataType;
|
|
1156
|
+
this.columnType = config.columnType;
|
|
1157
|
+
this.generated = config.generated;
|
|
1158
|
+
this.generatedIdentity = config.generatedIdentity;
|
|
1159
|
+
}
|
|
1160
|
+
static [entityKind] = "Column";
|
|
1161
|
+
name;
|
|
1162
|
+
keyAsName;
|
|
1163
|
+
primary;
|
|
1164
|
+
notNull;
|
|
1165
|
+
default;
|
|
1166
|
+
defaultFn;
|
|
1167
|
+
onUpdateFn;
|
|
1168
|
+
hasDefault;
|
|
1169
|
+
isUnique;
|
|
1170
|
+
uniqueName;
|
|
1171
|
+
uniqueType;
|
|
1172
|
+
dataType;
|
|
1173
|
+
columnType;
|
|
1174
|
+
enumValues = void 0;
|
|
1175
|
+
generated = void 0;
|
|
1176
|
+
generatedIdentity = void 0;
|
|
1177
|
+
config;
|
|
1178
|
+
mapFromDriverValue(value) {
|
|
1179
|
+
return value;
|
|
1180
|
+
}
|
|
1181
|
+
mapToDriverValue(value) {
|
|
1182
|
+
return value;
|
|
1183
|
+
}
|
|
1184
|
+
// ** @internal */
|
|
1185
|
+
shouldDisableInsert() {
|
|
1186
|
+
return this.config.generated !== void 0 && this.config.generated.type !== "byDefault";
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
|
|
1190
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/column-builder.js
|
|
1191
|
+
var ColumnBuilder = class {
|
|
1192
|
+
static [entityKind] = "ColumnBuilder";
|
|
1193
|
+
config;
|
|
1194
|
+
constructor(name, dataType, columnType) {
|
|
1195
|
+
this.config = {
|
|
1196
|
+
name,
|
|
1197
|
+
keyAsName: name === "",
|
|
1198
|
+
notNull: false,
|
|
1199
|
+
default: void 0,
|
|
1200
|
+
hasDefault: false,
|
|
1201
|
+
primaryKey: false,
|
|
1202
|
+
isUnique: false,
|
|
1203
|
+
uniqueName: void 0,
|
|
1204
|
+
uniqueType: void 0,
|
|
1205
|
+
dataType,
|
|
1206
|
+
columnType,
|
|
1207
|
+
generated: void 0
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Changes the data type of the column. Commonly used with `json` columns. Also, useful for branded types.
|
|
1212
|
+
*
|
|
1213
|
+
* @example
|
|
1214
|
+
* ```ts
|
|
1215
|
+
* const users = pgTable('users', {
|
|
1216
|
+
* id: integer('id').$type<UserId>().primaryKey(),
|
|
1217
|
+
* details: json('details').$type<UserDetails>().notNull(),
|
|
1218
|
+
* });
|
|
1219
|
+
* ```
|
|
1220
|
+
*/
|
|
1221
|
+
$type() {
|
|
1222
|
+
return this;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* Adds a `not null` clause to the column definition.
|
|
1226
|
+
*
|
|
1227
|
+
* Affects the `select` model of the table - columns *without* `not null` will be nullable on select.
|
|
1228
|
+
*/
|
|
1229
|
+
notNull() {
|
|
1230
|
+
this.config.notNull = true;
|
|
1231
|
+
return this;
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* Adds a `default <value>` clause to the column definition.
|
|
1235
|
+
*
|
|
1236
|
+
* Affects the `insert` model of the table - columns *with* `default` are optional on insert.
|
|
1237
|
+
*
|
|
1238
|
+
* If you need to set a dynamic default value, use {@link $defaultFn} instead.
|
|
1239
|
+
*/
|
|
1240
|
+
default(value) {
|
|
1241
|
+
this.config.default = value;
|
|
1242
|
+
this.config.hasDefault = true;
|
|
1243
|
+
return this;
|
|
1244
|
+
}
|
|
1245
|
+
/**
|
|
1246
|
+
* Adds a dynamic default value to the column.
|
|
1247
|
+
* The function will be called when the row is inserted, and the returned value will be used as the column value.
|
|
1248
|
+
*
|
|
1249
|
+
* **Note:** This value does not affect the `drizzle-kit` behavior, it is only used at runtime in `drizzle-orm`.
|
|
1250
|
+
*/
|
|
1251
|
+
$defaultFn(fn) {
|
|
1252
|
+
this.config.defaultFn = fn;
|
|
1253
|
+
this.config.hasDefault = true;
|
|
1254
|
+
return this;
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1257
|
+
* Alias for {@link $defaultFn}.
|
|
1258
|
+
*/
|
|
1259
|
+
$default = this.$defaultFn;
|
|
1260
|
+
/**
|
|
1261
|
+
* Adds a dynamic update value to the column.
|
|
1262
|
+
* The function will be called when the row is updated, and the returned value will be used as the column value if none is provided.
|
|
1263
|
+
* If no `default` (or `$defaultFn`) value is provided, the function will be called when the row is inserted as well, and the returned value will be used as the column value.
|
|
1264
|
+
*
|
|
1265
|
+
* **Note:** This value does not affect the `drizzle-kit` behavior, it is only used at runtime in `drizzle-orm`.
|
|
1266
|
+
*/
|
|
1267
|
+
$onUpdateFn(fn) {
|
|
1268
|
+
this.config.onUpdateFn = fn;
|
|
1269
|
+
this.config.hasDefault = true;
|
|
1270
|
+
return this;
|
|
1271
|
+
}
|
|
1272
|
+
/**
|
|
1273
|
+
* Alias for {@link $onUpdateFn}.
|
|
1274
|
+
*/
|
|
1275
|
+
$onUpdate = this.$onUpdateFn;
|
|
1276
|
+
/**
|
|
1277
|
+
* Adds a `primary key` clause to the column definition. This implicitly makes the column `not null`.
|
|
1278
|
+
*
|
|
1279
|
+
* In SQLite, `integer primary key` implicitly makes the column auto-incrementing.
|
|
1280
|
+
*/
|
|
1281
|
+
primaryKey() {
|
|
1282
|
+
this.config.primaryKey = true;
|
|
1283
|
+
this.config.notNull = true;
|
|
1284
|
+
return this;
|
|
1285
|
+
}
|
|
1286
|
+
/** @internal Sets the name of the column to the key within the table definition if a name was not given. */
|
|
1287
|
+
setName(name) {
|
|
1288
|
+
if (this.config.name !== "")
|
|
1289
|
+
return;
|
|
1290
|
+
this.config.name = name;
|
|
1291
|
+
}
|
|
1292
|
+
};
|
|
1293
|
+
|
|
1294
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/table.utils.js
|
|
1295
|
+
var TableName = /* @__PURE__ */ Symbol.for("drizzle:Name");
|
|
1296
|
+
|
|
1297
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/pg-core/foreign-keys.js
|
|
1298
|
+
var ForeignKeyBuilder = class {
|
|
1299
|
+
static [entityKind] = "PgForeignKeyBuilder";
|
|
1300
|
+
/** @internal */
|
|
1301
|
+
reference;
|
|
1302
|
+
/** @internal */
|
|
1303
|
+
_onUpdate = "no action";
|
|
1304
|
+
/** @internal */
|
|
1305
|
+
_onDelete = "no action";
|
|
1306
|
+
constructor(config, actions) {
|
|
1307
|
+
this.reference = () => {
|
|
1308
|
+
const { name, columns, foreignColumns } = config();
|
|
1309
|
+
return { name, columns, foreignTable: foreignColumns[0].table, foreignColumns };
|
|
1310
|
+
};
|
|
1311
|
+
if (actions) {
|
|
1312
|
+
this._onUpdate = actions.onUpdate;
|
|
1313
|
+
this._onDelete = actions.onDelete;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
onUpdate(action) {
|
|
1317
|
+
this._onUpdate = action === void 0 ? "no action" : action;
|
|
1318
|
+
return this;
|
|
1319
|
+
}
|
|
1320
|
+
onDelete(action) {
|
|
1321
|
+
this._onDelete = action === void 0 ? "no action" : action;
|
|
1322
|
+
return this;
|
|
1323
|
+
}
|
|
1324
|
+
/** @internal */
|
|
1325
|
+
build(table) {
|
|
1326
|
+
return new ForeignKey(table, this);
|
|
1327
|
+
}
|
|
1328
|
+
};
|
|
1329
|
+
var ForeignKey = class {
|
|
1330
|
+
constructor(table, builder) {
|
|
1331
|
+
this.table = table;
|
|
1332
|
+
this.reference = builder.reference;
|
|
1333
|
+
this.onUpdate = builder._onUpdate;
|
|
1334
|
+
this.onDelete = builder._onDelete;
|
|
1335
|
+
}
|
|
1336
|
+
static [entityKind] = "PgForeignKey";
|
|
1337
|
+
reference;
|
|
1338
|
+
onUpdate;
|
|
1339
|
+
onDelete;
|
|
1340
|
+
getName() {
|
|
1341
|
+
const { name, columns, foreignColumns } = this.reference();
|
|
1342
|
+
const columnNames = columns.map((column) => column.name);
|
|
1343
|
+
const foreignColumnNames = foreignColumns.map((column) => column.name);
|
|
1344
|
+
const chunks = [
|
|
1345
|
+
this.table[TableName],
|
|
1346
|
+
...columnNames,
|
|
1347
|
+
foreignColumns[0].table[TableName],
|
|
1348
|
+
...foreignColumnNames
|
|
1349
|
+
];
|
|
1350
|
+
return name ?? `${chunks.join("_")}_fk`;
|
|
1351
|
+
}
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/tracing-utils.js
|
|
1355
|
+
function iife(fn, ...args) {
|
|
1356
|
+
return fn(...args);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/pg-core/unique-constraint.js
|
|
1360
|
+
function uniqueKeyName(table, columns) {
|
|
1361
|
+
return `${table[TableName]}_${columns.join("_")}_unique`;
|
|
1362
|
+
}
|
|
1363
|
+
var UniqueConstraintBuilder = class {
|
|
1364
|
+
constructor(columns, name) {
|
|
1365
|
+
this.name = name;
|
|
1366
|
+
this.columns = columns;
|
|
1367
|
+
}
|
|
1368
|
+
static [entityKind] = "PgUniqueConstraintBuilder";
|
|
1369
|
+
/** @internal */
|
|
1370
|
+
columns;
|
|
1371
|
+
/** @internal */
|
|
1372
|
+
nullsNotDistinctConfig = false;
|
|
1373
|
+
nullsNotDistinct() {
|
|
1374
|
+
this.nullsNotDistinctConfig = true;
|
|
1375
|
+
return this;
|
|
1376
|
+
}
|
|
1377
|
+
/** @internal */
|
|
1378
|
+
build(table) {
|
|
1379
|
+
return new UniqueConstraint(table, this.columns, this.nullsNotDistinctConfig, this.name);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
var UniqueOnConstraintBuilder = class {
|
|
1383
|
+
static [entityKind] = "PgUniqueOnConstraintBuilder";
|
|
1384
|
+
/** @internal */
|
|
1385
|
+
name;
|
|
1386
|
+
constructor(name) {
|
|
1387
|
+
this.name = name;
|
|
1388
|
+
}
|
|
1389
|
+
on(...columns) {
|
|
1390
|
+
return new UniqueConstraintBuilder(columns, this.name);
|
|
1391
|
+
}
|
|
1392
|
+
};
|
|
1393
|
+
var UniqueConstraint = class {
|
|
1394
|
+
constructor(table, columns, nullsNotDistinct, name) {
|
|
1395
|
+
this.table = table;
|
|
1396
|
+
this.columns = columns;
|
|
1397
|
+
this.name = name ?? uniqueKeyName(this.table, this.columns.map((column) => column.name));
|
|
1398
|
+
this.nullsNotDistinct = nullsNotDistinct;
|
|
1399
|
+
}
|
|
1400
|
+
static [entityKind] = "PgUniqueConstraint";
|
|
1401
|
+
columns;
|
|
1402
|
+
name;
|
|
1403
|
+
nullsNotDistinct = false;
|
|
1404
|
+
getName() {
|
|
1405
|
+
return this.name;
|
|
1406
|
+
}
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/pg-core/utils/array.js
|
|
1410
|
+
function parsePgArrayValue(arrayString, startFrom, inQuotes) {
|
|
1411
|
+
for (let i = startFrom; i < arrayString.length; i++) {
|
|
1412
|
+
const char = arrayString[i];
|
|
1413
|
+
if (char === "\\") {
|
|
1414
|
+
i++;
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
if (char === '"') {
|
|
1418
|
+
return [arrayString.slice(startFrom, i).replace(/\\/g, ""), i + 1];
|
|
1419
|
+
}
|
|
1420
|
+
if (inQuotes) {
|
|
1421
|
+
continue;
|
|
1422
|
+
}
|
|
1423
|
+
if (char === "," || char === "}") {
|
|
1424
|
+
return [arrayString.slice(startFrom, i).replace(/\\/g, ""), i];
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return [arrayString.slice(startFrom).replace(/\\/g, ""), arrayString.length];
|
|
1428
|
+
}
|
|
1429
|
+
function parsePgNestedArray(arrayString, startFrom = 0) {
|
|
1430
|
+
const result = [];
|
|
1431
|
+
let i = startFrom;
|
|
1432
|
+
let lastCharIsComma = false;
|
|
1433
|
+
while (i < arrayString.length) {
|
|
1434
|
+
const char = arrayString[i];
|
|
1435
|
+
if (char === ",") {
|
|
1436
|
+
if (lastCharIsComma || i === startFrom) {
|
|
1437
|
+
result.push("");
|
|
1438
|
+
}
|
|
1439
|
+
lastCharIsComma = true;
|
|
1440
|
+
i++;
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
lastCharIsComma = false;
|
|
1444
|
+
if (char === "\\") {
|
|
1445
|
+
i += 2;
|
|
1446
|
+
continue;
|
|
1447
|
+
}
|
|
1448
|
+
if (char === '"') {
|
|
1449
|
+
const [value2, startFrom2] = parsePgArrayValue(arrayString, i + 1, true);
|
|
1450
|
+
result.push(value2);
|
|
1451
|
+
i = startFrom2;
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
if (char === "}") {
|
|
1455
|
+
return [result, i + 1];
|
|
1456
|
+
}
|
|
1457
|
+
if (char === "{") {
|
|
1458
|
+
const [value2, startFrom2] = parsePgNestedArray(arrayString, i + 1);
|
|
1459
|
+
result.push(value2);
|
|
1460
|
+
i = startFrom2;
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
const [value, newStartFrom] = parsePgArrayValue(arrayString, i, false);
|
|
1464
|
+
result.push(value);
|
|
1465
|
+
i = newStartFrom;
|
|
1466
|
+
}
|
|
1467
|
+
return [result, i];
|
|
1468
|
+
}
|
|
1469
|
+
function parsePgArray(arrayString) {
|
|
1470
|
+
const [result] = parsePgNestedArray(arrayString, 1);
|
|
1471
|
+
return result;
|
|
1472
|
+
}
|
|
1473
|
+
function makePgArray(array) {
|
|
1474
|
+
return `{${array.map((item) => {
|
|
1475
|
+
if (Array.isArray(item)) {
|
|
1476
|
+
return makePgArray(item);
|
|
1477
|
+
}
|
|
1478
|
+
if (typeof item === "string") {
|
|
1479
|
+
return `"${item.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
1480
|
+
}
|
|
1481
|
+
return `${item}`;
|
|
1482
|
+
}).join(",")}}`;
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/pg-core/columns/common.js
|
|
1486
|
+
var PgColumnBuilder = class extends ColumnBuilder {
|
|
1487
|
+
foreignKeyConfigs = [];
|
|
1488
|
+
static [entityKind] = "PgColumnBuilder";
|
|
1489
|
+
array(size) {
|
|
1490
|
+
return new PgArrayBuilder(this.config.name, this, size);
|
|
1491
|
+
}
|
|
1492
|
+
references(ref, actions = {}) {
|
|
1493
|
+
this.foreignKeyConfigs.push({ ref, actions });
|
|
1494
|
+
return this;
|
|
1495
|
+
}
|
|
1496
|
+
unique(name, config) {
|
|
1497
|
+
this.config.isUnique = true;
|
|
1498
|
+
this.config.uniqueName = name;
|
|
1499
|
+
this.config.uniqueType = config?.nulls;
|
|
1500
|
+
return this;
|
|
1501
|
+
}
|
|
1502
|
+
generatedAlwaysAs(as) {
|
|
1503
|
+
this.config.generated = {
|
|
1504
|
+
as,
|
|
1505
|
+
type: "always",
|
|
1506
|
+
mode: "stored"
|
|
1507
|
+
};
|
|
1508
|
+
return this;
|
|
1509
|
+
}
|
|
1510
|
+
/** @internal */
|
|
1511
|
+
buildForeignKeys(column, table) {
|
|
1512
|
+
return this.foreignKeyConfigs.map(({ ref, actions }) => {
|
|
1513
|
+
return iife(
|
|
1514
|
+
(ref2, actions2) => {
|
|
1515
|
+
const builder = new ForeignKeyBuilder(() => {
|
|
1516
|
+
const foreignColumn = ref2();
|
|
1517
|
+
return { columns: [column], foreignColumns: [foreignColumn] };
|
|
1518
|
+
});
|
|
1519
|
+
if (actions2.onUpdate) {
|
|
1520
|
+
builder.onUpdate(actions2.onUpdate);
|
|
1521
|
+
}
|
|
1522
|
+
if (actions2.onDelete) {
|
|
1523
|
+
builder.onDelete(actions2.onDelete);
|
|
1524
|
+
}
|
|
1525
|
+
return builder.build(table);
|
|
1526
|
+
},
|
|
1527
|
+
ref,
|
|
1528
|
+
actions
|
|
1529
|
+
);
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
/** @internal */
|
|
1533
|
+
buildExtraConfigColumn(table) {
|
|
1534
|
+
return new ExtraConfigColumn(table, this.config);
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
var PgColumn = class extends Column {
|
|
1538
|
+
constructor(table, config) {
|
|
1539
|
+
if (!config.uniqueName) {
|
|
1540
|
+
config.uniqueName = uniqueKeyName(table, [config.name]);
|
|
1541
|
+
}
|
|
1542
|
+
super(table, config);
|
|
1543
|
+
this.table = table;
|
|
1544
|
+
}
|
|
1545
|
+
static [entityKind] = "PgColumn";
|
|
1546
|
+
};
|
|
1547
|
+
var ExtraConfigColumn = class extends PgColumn {
|
|
1548
|
+
static [entityKind] = "ExtraConfigColumn";
|
|
1549
|
+
getSQLType() {
|
|
1550
|
+
return this.getSQLType();
|
|
1551
|
+
}
|
|
1552
|
+
indexConfig = {
|
|
1553
|
+
order: this.config.order ?? "asc",
|
|
1554
|
+
nulls: this.config.nulls ?? "last",
|
|
1555
|
+
opClass: this.config.opClass
|
|
1556
|
+
};
|
|
1557
|
+
defaultConfig = {
|
|
1558
|
+
order: "asc",
|
|
1559
|
+
nulls: "last",
|
|
1560
|
+
opClass: void 0
|
|
1561
|
+
};
|
|
1562
|
+
asc() {
|
|
1563
|
+
this.indexConfig.order = "asc";
|
|
1564
|
+
return this;
|
|
1565
|
+
}
|
|
1566
|
+
desc() {
|
|
1567
|
+
this.indexConfig.order = "desc";
|
|
1568
|
+
return this;
|
|
1569
|
+
}
|
|
1570
|
+
nullsFirst() {
|
|
1571
|
+
this.indexConfig.nulls = "first";
|
|
1572
|
+
return this;
|
|
1573
|
+
}
|
|
1574
|
+
nullsLast() {
|
|
1575
|
+
this.indexConfig.nulls = "last";
|
|
1576
|
+
return this;
|
|
1577
|
+
}
|
|
1578
|
+
/**
|
|
1579
|
+
* ### PostgreSQL documentation quote
|
|
1580
|
+
*
|
|
1581
|
+
* > An operator class with optional parameters can be specified for each column of an index.
|
|
1582
|
+
* The operator class identifies the operators to be used by the index for that column.
|
|
1583
|
+
* For example, a B-tree index on four-byte integers would use the int4_ops class;
|
|
1584
|
+
* this operator class includes comparison functions for four-byte integers.
|
|
1585
|
+
* In practice the default operator class for the column's data type is usually sufficient.
|
|
1586
|
+
* The main point of having operator classes is that for some data types, there could be more than one meaningful ordering.
|
|
1587
|
+
* For example, we might want to sort a complex-number data type either by absolute value or by real part.
|
|
1588
|
+
* We could do this by defining two operator classes for the data type and then selecting the proper class when creating an index.
|
|
1589
|
+
* More information about operator classes check:
|
|
1590
|
+
*
|
|
1591
|
+
* ### Useful links
|
|
1592
|
+
* https://www.postgresql.org/docs/current/sql-createindex.html
|
|
1593
|
+
*
|
|
1594
|
+
* https://www.postgresql.org/docs/current/indexes-opclass.html
|
|
1595
|
+
*
|
|
1596
|
+
* https://www.postgresql.org/docs/current/xindex.html
|
|
1597
|
+
*
|
|
1598
|
+
* ### Additional types
|
|
1599
|
+
* If you have the `pg_vector` extension installed in your database, you can use the
|
|
1600
|
+
* `vector_l2_ops`, `vector_ip_ops`, `vector_cosine_ops`, `vector_l1_ops`, `bit_hamming_ops`, `bit_jaccard_ops`, `halfvec_l2_ops`, `sparsevec_l2_ops` options, which are predefined types.
|
|
1601
|
+
*
|
|
1602
|
+
* **You can always specify any string you want in the operator class, in case Drizzle doesn't have it natively in its types**
|
|
1603
|
+
*
|
|
1604
|
+
* @param opClass
|
|
1605
|
+
* @returns
|
|
1606
|
+
*/
|
|
1607
|
+
op(opClass) {
|
|
1608
|
+
this.indexConfig.opClass = opClass;
|
|
1609
|
+
return this;
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
var IndexedColumn = class {
|
|
1613
|
+
static [entityKind] = "IndexedColumn";
|
|
1614
|
+
constructor(name, keyAsName, type, indexConfig) {
|
|
1615
|
+
this.name = name;
|
|
1616
|
+
this.keyAsName = keyAsName;
|
|
1617
|
+
this.type = type;
|
|
1618
|
+
this.indexConfig = indexConfig;
|
|
1619
|
+
}
|
|
1620
|
+
name;
|
|
1621
|
+
keyAsName;
|
|
1622
|
+
type;
|
|
1623
|
+
indexConfig;
|
|
1624
|
+
};
|
|
1625
|
+
var PgArrayBuilder = class extends PgColumnBuilder {
|
|
1626
|
+
static [entityKind] = "PgArrayBuilder";
|
|
1627
|
+
constructor(name, baseBuilder, size) {
|
|
1628
|
+
super(name, "array", "PgArray");
|
|
1629
|
+
this.config.baseBuilder = baseBuilder;
|
|
1630
|
+
this.config.size = size;
|
|
1631
|
+
}
|
|
1632
|
+
/** @internal */
|
|
1633
|
+
build(table) {
|
|
1634
|
+
const baseColumn = this.config.baseBuilder.build(table);
|
|
1635
|
+
return new PgArray(
|
|
1636
|
+
table,
|
|
1637
|
+
this.config,
|
|
1638
|
+
baseColumn
|
|
1639
|
+
);
|
|
1640
|
+
}
|
|
1641
|
+
};
|
|
1642
|
+
var PgArray = class _PgArray extends PgColumn {
|
|
1643
|
+
constructor(table, config, baseColumn, range) {
|
|
1644
|
+
super(table, config);
|
|
1645
|
+
this.baseColumn = baseColumn;
|
|
1646
|
+
this.range = range;
|
|
1647
|
+
this.size = config.size;
|
|
1648
|
+
}
|
|
1649
|
+
size;
|
|
1650
|
+
static [entityKind] = "PgArray";
|
|
1651
|
+
getSQLType() {
|
|
1652
|
+
return `${this.baseColumn.getSQLType()}[${typeof this.size === "number" ? this.size : ""}]`;
|
|
1653
|
+
}
|
|
1654
|
+
mapFromDriverValue(value) {
|
|
1655
|
+
if (typeof value === "string") {
|
|
1656
|
+
value = parsePgArray(value);
|
|
1657
|
+
}
|
|
1658
|
+
return value.map((v) => this.baseColumn.mapFromDriverValue(v));
|
|
1659
|
+
}
|
|
1660
|
+
mapToDriverValue(value, isNestedArray = false) {
|
|
1661
|
+
const a = value.map(
|
|
1662
|
+
(v) => v === null ? null : is(this.baseColumn, _PgArray) ? this.baseColumn.mapToDriverValue(v, true) : this.baseColumn.mapToDriverValue(v)
|
|
1663
|
+
);
|
|
1664
|
+
if (isNestedArray)
|
|
1665
|
+
return a;
|
|
1666
|
+
return makePgArray(a);
|
|
1667
|
+
}
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/pg-core/columns/enum.js
|
|
1671
|
+
var isPgEnumSym = /* @__PURE__ */ Symbol.for("drizzle:isPgEnum");
|
|
1672
|
+
function isPgEnum(obj) {
|
|
1673
|
+
return !!obj && typeof obj === "function" && isPgEnumSym in obj && obj[isPgEnumSym] === true;
|
|
1674
|
+
}
|
|
1675
|
+
var PgEnumColumnBuilder = class extends PgColumnBuilder {
|
|
1676
|
+
static [entityKind] = "PgEnumColumnBuilder";
|
|
1677
|
+
constructor(name, enumInstance) {
|
|
1678
|
+
super(name, "string", "PgEnumColumn");
|
|
1679
|
+
this.config.enum = enumInstance;
|
|
1680
|
+
}
|
|
1681
|
+
/** @internal */
|
|
1682
|
+
build(table) {
|
|
1683
|
+
return new PgEnumColumn(
|
|
1684
|
+
table,
|
|
1685
|
+
this.config
|
|
1686
|
+
);
|
|
1687
|
+
}
|
|
1688
|
+
};
|
|
1689
|
+
var PgEnumColumn = class extends PgColumn {
|
|
1690
|
+
static [entityKind] = "PgEnumColumn";
|
|
1691
|
+
enum = this.config.enum;
|
|
1692
|
+
enumValues = this.config.enum.enumValues;
|
|
1693
|
+
constructor(table, config) {
|
|
1694
|
+
super(table, config);
|
|
1695
|
+
this.enum = config.enum;
|
|
1696
|
+
}
|
|
1697
|
+
getSQLType() {
|
|
1698
|
+
return this.enum.enumName;
|
|
1699
|
+
}
|
|
1700
|
+
};
|
|
1701
|
+
|
|
1702
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/subquery.js
|
|
1703
|
+
var Subquery = class {
|
|
1704
|
+
static [entityKind] = "Subquery";
|
|
1705
|
+
constructor(sql2, selection, alias, isWith = false) {
|
|
1706
|
+
this._ = {
|
|
1707
|
+
brand: "Subquery",
|
|
1708
|
+
sql: sql2,
|
|
1709
|
+
selectedFields: selection,
|
|
1710
|
+
alias,
|
|
1711
|
+
isWith
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
// getSQL(): SQL<unknown> {
|
|
1715
|
+
// return new SQL([this]);
|
|
1716
|
+
// }
|
|
1717
|
+
};
|
|
1718
|
+
var WithSubquery = class extends Subquery {
|
|
1719
|
+
static [entityKind] = "WithSubquery";
|
|
1720
|
+
};
|
|
1721
|
+
|
|
1722
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/version.js
|
|
1723
|
+
var version = "0.36.4";
|
|
1724
|
+
|
|
1725
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/tracing.js
|
|
1726
|
+
var otel;
|
|
1727
|
+
var rawTracer;
|
|
1728
|
+
var tracer = {
|
|
1729
|
+
startActiveSpan(name, fn) {
|
|
1730
|
+
if (!otel) {
|
|
1731
|
+
return fn();
|
|
1732
|
+
}
|
|
1733
|
+
if (!rawTracer) {
|
|
1734
|
+
rawTracer = otel.trace.getTracer("drizzle-orm", version);
|
|
1735
|
+
}
|
|
1736
|
+
return iife(
|
|
1737
|
+
(otel2, rawTracer2) => rawTracer2.startActiveSpan(
|
|
1738
|
+
name,
|
|
1739
|
+
(span) => {
|
|
1740
|
+
try {
|
|
1741
|
+
return fn(span);
|
|
1742
|
+
} catch (e) {
|
|
1743
|
+
span.setStatus({
|
|
1744
|
+
code: otel2.SpanStatusCode.ERROR,
|
|
1745
|
+
message: e instanceof Error ? e.message : "Unknown error"
|
|
1746
|
+
// eslint-disable-line no-instanceof/no-instanceof
|
|
1747
|
+
});
|
|
1748
|
+
throw e;
|
|
1749
|
+
} finally {
|
|
1750
|
+
span.end();
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
),
|
|
1754
|
+
otel,
|
|
1755
|
+
rawTracer
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
|
|
1760
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/view-common.js
|
|
1761
|
+
var ViewBaseConfig = /* @__PURE__ */ Symbol.for("drizzle:ViewBaseConfig");
|
|
1762
|
+
|
|
1763
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/table.js
|
|
1764
|
+
var Schema = /* @__PURE__ */ Symbol.for("drizzle:Schema");
|
|
1765
|
+
var Columns = /* @__PURE__ */ Symbol.for("drizzle:Columns");
|
|
1766
|
+
var ExtraConfigColumns = /* @__PURE__ */ Symbol.for("drizzle:ExtraConfigColumns");
|
|
1767
|
+
var OriginalName = /* @__PURE__ */ Symbol.for("drizzle:OriginalName");
|
|
1768
|
+
var BaseName = /* @__PURE__ */ Symbol.for("drizzle:BaseName");
|
|
1769
|
+
var IsAlias = /* @__PURE__ */ Symbol.for("drizzle:IsAlias");
|
|
1770
|
+
var ExtraConfigBuilder = /* @__PURE__ */ Symbol.for("drizzle:ExtraConfigBuilder");
|
|
1771
|
+
var IsDrizzleTable = /* @__PURE__ */ Symbol.for("drizzle:IsDrizzleTable");
|
|
1772
|
+
var Table = class {
|
|
1773
|
+
static [entityKind] = "Table";
|
|
1774
|
+
/** @internal */
|
|
1775
|
+
static Symbol = {
|
|
1776
|
+
Name: TableName,
|
|
1777
|
+
Schema,
|
|
1778
|
+
OriginalName,
|
|
1779
|
+
Columns,
|
|
1780
|
+
ExtraConfigColumns,
|
|
1781
|
+
BaseName,
|
|
1782
|
+
IsAlias,
|
|
1783
|
+
ExtraConfigBuilder
|
|
1784
|
+
};
|
|
1785
|
+
/**
|
|
1786
|
+
* @internal
|
|
1787
|
+
* Can be changed if the table is aliased.
|
|
1788
|
+
*/
|
|
1789
|
+
[TableName];
|
|
1790
|
+
/**
|
|
1791
|
+
* @internal
|
|
1792
|
+
* Used to store the original name of the table, before any aliasing.
|
|
1793
|
+
*/
|
|
1794
|
+
[OriginalName];
|
|
1795
|
+
/** @internal */
|
|
1796
|
+
[Schema];
|
|
1797
|
+
/** @internal */
|
|
1798
|
+
[Columns];
|
|
1799
|
+
/** @internal */
|
|
1800
|
+
[ExtraConfigColumns];
|
|
1801
|
+
/**
|
|
1802
|
+
* @internal
|
|
1803
|
+
* Used to store the table name before the transformation via the `tableCreator` functions.
|
|
1804
|
+
*/
|
|
1805
|
+
[BaseName];
|
|
1806
|
+
/** @internal */
|
|
1807
|
+
[IsAlias] = false;
|
|
1808
|
+
/** @internal */
|
|
1809
|
+
[IsDrizzleTable] = true;
|
|
1810
|
+
/** @internal */
|
|
1811
|
+
[ExtraConfigBuilder] = void 0;
|
|
1812
|
+
constructor(name, schema, baseName) {
|
|
1813
|
+
this[TableName] = this[OriginalName] = name;
|
|
1814
|
+
this[Schema] = schema;
|
|
1815
|
+
this[BaseName] = baseName;
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
|
|
1819
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sql/sql.js
|
|
1820
|
+
var FakePrimitiveParam = class {
|
|
1821
|
+
static [entityKind] = "FakePrimitiveParam";
|
|
1822
|
+
};
|
|
1823
|
+
function isSQLWrapper(value) {
|
|
1824
|
+
return value !== null && value !== void 0 && typeof value.getSQL === "function";
|
|
1825
|
+
}
|
|
1826
|
+
function mergeQueries(queries) {
|
|
1827
|
+
const result = { sql: "", params: [] };
|
|
1828
|
+
for (const query of queries) {
|
|
1829
|
+
result.sql += query.sql;
|
|
1830
|
+
result.params.push(...query.params);
|
|
1831
|
+
if (query.typings?.length) {
|
|
1832
|
+
if (!result.typings) {
|
|
1833
|
+
result.typings = [];
|
|
1834
|
+
}
|
|
1835
|
+
result.typings.push(...query.typings);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return result;
|
|
1839
|
+
}
|
|
1840
|
+
var StringChunk = class {
|
|
1841
|
+
static [entityKind] = "StringChunk";
|
|
1842
|
+
value;
|
|
1843
|
+
constructor(value) {
|
|
1844
|
+
this.value = Array.isArray(value) ? value : [value];
|
|
1845
|
+
}
|
|
1846
|
+
getSQL() {
|
|
1847
|
+
return new SQL([this]);
|
|
1848
|
+
}
|
|
1849
|
+
};
|
|
1850
|
+
var SQL = class _SQL {
|
|
1851
|
+
constructor(queryChunks) {
|
|
1852
|
+
this.queryChunks = queryChunks;
|
|
1853
|
+
}
|
|
1854
|
+
static [entityKind] = "SQL";
|
|
1855
|
+
/** @internal */
|
|
1856
|
+
decoder = noopDecoder;
|
|
1857
|
+
shouldInlineParams = false;
|
|
1858
|
+
append(query) {
|
|
1859
|
+
this.queryChunks.push(...query.queryChunks);
|
|
1860
|
+
return this;
|
|
1861
|
+
}
|
|
1862
|
+
toQuery(config) {
|
|
1863
|
+
return tracer.startActiveSpan("drizzle.buildSQL", (span) => {
|
|
1864
|
+
const query = this.buildQueryFromSourceParams(this.queryChunks, config);
|
|
1865
|
+
span?.setAttributes({
|
|
1866
|
+
"drizzle.query.text": query.sql,
|
|
1867
|
+
"drizzle.query.params": JSON.stringify(query.params)
|
|
1868
|
+
});
|
|
1869
|
+
return query;
|
|
1870
|
+
});
|
|
1871
|
+
}
|
|
1872
|
+
buildQueryFromSourceParams(chunks, _config) {
|
|
1873
|
+
const config = Object.assign({}, _config, {
|
|
1874
|
+
inlineParams: _config.inlineParams || this.shouldInlineParams,
|
|
1875
|
+
paramStartIndex: _config.paramStartIndex || { value: 0 }
|
|
1876
|
+
});
|
|
1877
|
+
const {
|
|
1878
|
+
casing,
|
|
1879
|
+
escapeName,
|
|
1880
|
+
escapeParam,
|
|
1881
|
+
prepareTyping,
|
|
1882
|
+
inlineParams,
|
|
1883
|
+
paramStartIndex
|
|
1884
|
+
} = config;
|
|
1885
|
+
return mergeQueries(chunks.map((chunk) => {
|
|
1886
|
+
if (is(chunk, StringChunk)) {
|
|
1887
|
+
return { sql: chunk.value.join(""), params: [] };
|
|
1888
|
+
}
|
|
1889
|
+
if (is(chunk, Name)) {
|
|
1890
|
+
return { sql: escapeName(chunk.value), params: [] };
|
|
1891
|
+
}
|
|
1892
|
+
if (chunk === void 0) {
|
|
1893
|
+
return { sql: "", params: [] };
|
|
1894
|
+
}
|
|
1895
|
+
if (Array.isArray(chunk)) {
|
|
1896
|
+
const result = [new StringChunk("(")];
|
|
1897
|
+
for (const [i, p] of chunk.entries()) {
|
|
1898
|
+
result.push(p);
|
|
1899
|
+
if (i < chunk.length - 1) {
|
|
1900
|
+
result.push(new StringChunk(", "));
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
result.push(new StringChunk(")"));
|
|
1904
|
+
return this.buildQueryFromSourceParams(result, config);
|
|
1905
|
+
}
|
|
1906
|
+
if (is(chunk, _SQL)) {
|
|
1907
|
+
return this.buildQueryFromSourceParams(chunk.queryChunks, {
|
|
1908
|
+
...config,
|
|
1909
|
+
inlineParams: inlineParams || chunk.shouldInlineParams
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
if (is(chunk, Table)) {
|
|
1913
|
+
const schemaName = chunk[Table.Symbol.Schema];
|
|
1914
|
+
const tableName = chunk[Table.Symbol.Name];
|
|
1915
|
+
return {
|
|
1916
|
+
sql: schemaName === void 0 ? escapeName(tableName) : escapeName(schemaName) + "." + escapeName(tableName),
|
|
1917
|
+
params: []
|
|
1918
|
+
};
|
|
1919
|
+
}
|
|
1920
|
+
if (is(chunk, Column)) {
|
|
1921
|
+
const columnName = casing.getColumnCasing(chunk);
|
|
1922
|
+
if (_config.invokeSource === "indexes") {
|
|
1923
|
+
return { sql: escapeName(columnName), params: [] };
|
|
1924
|
+
}
|
|
1925
|
+
const schemaName = chunk.table[Table.Symbol.Schema];
|
|
1926
|
+
return {
|
|
1927
|
+
sql: chunk.table[IsAlias] || schemaName === void 0 ? escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName) : escapeName(schemaName) + "." + escapeName(chunk.table[Table.Symbol.Name]) + "." + escapeName(columnName),
|
|
1928
|
+
params: []
|
|
1929
|
+
};
|
|
1930
|
+
}
|
|
1931
|
+
if (is(chunk, View)) {
|
|
1932
|
+
const schemaName = chunk[ViewBaseConfig].schema;
|
|
1933
|
+
const viewName = chunk[ViewBaseConfig].name;
|
|
1934
|
+
return {
|
|
1935
|
+
sql: schemaName === void 0 ? escapeName(viewName) : escapeName(schemaName) + "." + escapeName(viewName),
|
|
1936
|
+
params: []
|
|
1937
|
+
};
|
|
1938
|
+
}
|
|
1939
|
+
if (is(chunk, Param)) {
|
|
1940
|
+
if (is(chunk.value, Placeholder)) {
|
|
1941
|
+
return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk], typings: ["none"] };
|
|
1942
|
+
}
|
|
1943
|
+
const mappedValue = chunk.value === null ? null : chunk.encoder.mapToDriverValue(chunk.value);
|
|
1944
|
+
if (is(mappedValue, _SQL)) {
|
|
1945
|
+
return this.buildQueryFromSourceParams([mappedValue], config);
|
|
1946
|
+
}
|
|
1947
|
+
if (inlineParams) {
|
|
1948
|
+
return { sql: this.mapInlineParam(mappedValue, config), params: [] };
|
|
1949
|
+
}
|
|
1950
|
+
let typings = ["none"];
|
|
1951
|
+
if (prepareTyping) {
|
|
1952
|
+
typings = [prepareTyping(chunk.encoder)];
|
|
1953
|
+
}
|
|
1954
|
+
return { sql: escapeParam(paramStartIndex.value++, mappedValue), params: [mappedValue], typings };
|
|
1955
|
+
}
|
|
1956
|
+
if (is(chunk, Placeholder)) {
|
|
1957
|
+
return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk], typings: ["none"] };
|
|
1958
|
+
}
|
|
1959
|
+
if (is(chunk, _SQL.Aliased) && chunk.fieldAlias !== void 0) {
|
|
1960
|
+
return { sql: escapeName(chunk.fieldAlias), params: [] };
|
|
1961
|
+
}
|
|
1962
|
+
if (is(chunk, Subquery)) {
|
|
1963
|
+
if (chunk._.isWith) {
|
|
1964
|
+
return { sql: escapeName(chunk._.alias), params: [] };
|
|
1965
|
+
}
|
|
1966
|
+
return this.buildQueryFromSourceParams([
|
|
1967
|
+
new StringChunk("("),
|
|
1968
|
+
chunk._.sql,
|
|
1969
|
+
new StringChunk(") "),
|
|
1970
|
+
new Name(chunk._.alias)
|
|
1971
|
+
], config);
|
|
1972
|
+
}
|
|
1973
|
+
if (isPgEnum(chunk)) {
|
|
1974
|
+
if (chunk.schema) {
|
|
1975
|
+
return { sql: escapeName(chunk.schema) + "." + escapeName(chunk.enumName), params: [] };
|
|
1976
|
+
}
|
|
1977
|
+
return { sql: escapeName(chunk.enumName), params: [] };
|
|
1978
|
+
}
|
|
1979
|
+
if (isSQLWrapper(chunk)) {
|
|
1980
|
+
if (chunk.shouldOmitSQLParens?.()) {
|
|
1981
|
+
return this.buildQueryFromSourceParams([chunk.getSQL()], config);
|
|
1982
|
+
}
|
|
1983
|
+
return this.buildQueryFromSourceParams([
|
|
1984
|
+
new StringChunk("("),
|
|
1985
|
+
chunk.getSQL(),
|
|
1986
|
+
new StringChunk(")")
|
|
1987
|
+
], config);
|
|
1988
|
+
}
|
|
1989
|
+
if (inlineParams) {
|
|
1990
|
+
return { sql: this.mapInlineParam(chunk, config), params: [] };
|
|
1991
|
+
}
|
|
1992
|
+
return { sql: escapeParam(paramStartIndex.value++, chunk), params: [chunk], typings: ["none"] };
|
|
1993
|
+
}));
|
|
1994
|
+
}
|
|
1995
|
+
mapInlineParam(chunk, { escapeString }) {
|
|
1996
|
+
if (chunk === null) {
|
|
1997
|
+
return "null";
|
|
1998
|
+
}
|
|
1999
|
+
if (typeof chunk === "number" || typeof chunk === "boolean") {
|
|
2000
|
+
return chunk.toString();
|
|
2001
|
+
}
|
|
2002
|
+
if (typeof chunk === "string") {
|
|
2003
|
+
return escapeString(chunk);
|
|
2004
|
+
}
|
|
2005
|
+
if (typeof chunk === "object") {
|
|
2006
|
+
const mappedValueAsString = chunk.toString();
|
|
2007
|
+
if (mappedValueAsString === "[object Object]") {
|
|
2008
|
+
return escapeString(JSON.stringify(chunk));
|
|
2009
|
+
}
|
|
2010
|
+
return escapeString(mappedValueAsString);
|
|
2011
|
+
}
|
|
2012
|
+
throw new Error("Unexpected param value: " + chunk);
|
|
2013
|
+
}
|
|
2014
|
+
getSQL() {
|
|
2015
|
+
return this;
|
|
2016
|
+
}
|
|
2017
|
+
as(alias) {
|
|
2018
|
+
if (alias === void 0) {
|
|
2019
|
+
return this;
|
|
2020
|
+
}
|
|
2021
|
+
return new _SQL.Aliased(this, alias);
|
|
2022
|
+
}
|
|
2023
|
+
mapWith(decoder) {
|
|
2024
|
+
this.decoder = typeof decoder === "function" ? { mapFromDriverValue: decoder } : decoder;
|
|
2025
|
+
return this;
|
|
2026
|
+
}
|
|
2027
|
+
inlineParams() {
|
|
2028
|
+
this.shouldInlineParams = true;
|
|
2029
|
+
return this;
|
|
2030
|
+
}
|
|
2031
|
+
/**
|
|
2032
|
+
* This method is used to conditionally include a part of the query.
|
|
2033
|
+
*
|
|
2034
|
+
* @param condition - Condition to check
|
|
2035
|
+
* @returns itself if the condition is `true`, otherwise `undefined`
|
|
2036
|
+
*/
|
|
2037
|
+
if(condition) {
|
|
2038
|
+
return condition ? this : void 0;
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
var Name = class {
|
|
2042
|
+
constructor(value) {
|
|
2043
|
+
this.value = value;
|
|
2044
|
+
}
|
|
2045
|
+
static [entityKind] = "Name";
|
|
2046
|
+
brand;
|
|
2047
|
+
getSQL() {
|
|
2048
|
+
return new SQL([this]);
|
|
2049
|
+
}
|
|
2050
|
+
};
|
|
2051
|
+
var noopDecoder = {
|
|
2052
|
+
mapFromDriverValue: (value) => value
|
|
2053
|
+
};
|
|
2054
|
+
var noopEncoder = {
|
|
2055
|
+
mapToDriverValue: (value) => value
|
|
2056
|
+
};
|
|
2057
|
+
var noopMapper = {
|
|
2058
|
+
...noopDecoder,
|
|
2059
|
+
...noopEncoder
|
|
2060
|
+
};
|
|
2061
|
+
var Param = class {
|
|
2062
|
+
/**
|
|
2063
|
+
* @param value - Parameter value
|
|
2064
|
+
* @param encoder - Encoder to convert the value to a driver parameter
|
|
2065
|
+
*/
|
|
2066
|
+
constructor(value, encoder = noopEncoder) {
|
|
2067
|
+
this.value = value;
|
|
2068
|
+
this.encoder = encoder;
|
|
2069
|
+
}
|
|
2070
|
+
static [entityKind] = "Param";
|
|
2071
|
+
brand;
|
|
2072
|
+
getSQL() {
|
|
2073
|
+
return new SQL([this]);
|
|
2074
|
+
}
|
|
2075
|
+
};
|
|
2076
|
+
function sql(strings, ...params) {
|
|
2077
|
+
const queryChunks = [];
|
|
2078
|
+
if (params.length > 0 || strings.length > 0 && strings[0] !== "") {
|
|
2079
|
+
queryChunks.push(new StringChunk(strings[0]));
|
|
2080
|
+
}
|
|
2081
|
+
for (const [paramIndex, param2] of params.entries()) {
|
|
2082
|
+
queryChunks.push(param2, new StringChunk(strings[paramIndex + 1]));
|
|
2083
|
+
}
|
|
2084
|
+
return new SQL(queryChunks);
|
|
2085
|
+
}
|
|
2086
|
+
((sql2) => {
|
|
2087
|
+
function empty() {
|
|
2088
|
+
return new SQL([]);
|
|
2089
|
+
}
|
|
2090
|
+
sql2.empty = empty;
|
|
2091
|
+
function fromList(list) {
|
|
2092
|
+
return new SQL(list);
|
|
2093
|
+
}
|
|
2094
|
+
sql2.fromList = fromList;
|
|
2095
|
+
function raw(str) {
|
|
2096
|
+
return new SQL([new StringChunk(str)]);
|
|
2097
|
+
}
|
|
2098
|
+
sql2.raw = raw;
|
|
2099
|
+
function join(chunks, separator) {
|
|
2100
|
+
const result = [];
|
|
2101
|
+
for (const [i, chunk] of chunks.entries()) {
|
|
2102
|
+
if (i > 0 && separator !== void 0) {
|
|
2103
|
+
result.push(separator);
|
|
2104
|
+
}
|
|
2105
|
+
result.push(chunk);
|
|
2106
|
+
}
|
|
2107
|
+
return new SQL(result);
|
|
2108
|
+
}
|
|
2109
|
+
sql2.join = join;
|
|
2110
|
+
function identifier(value) {
|
|
2111
|
+
return new Name(value);
|
|
2112
|
+
}
|
|
2113
|
+
sql2.identifier = identifier;
|
|
2114
|
+
function placeholder2(name2) {
|
|
2115
|
+
return new Placeholder(name2);
|
|
2116
|
+
}
|
|
2117
|
+
sql2.placeholder = placeholder2;
|
|
2118
|
+
function param2(value, encoder) {
|
|
2119
|
+
return new Param(value, encoder);
|
|
2120
|
+
}
|
|
2121
|
+
sql2.param = param2;
|
|
2122
|
+
})(sql || (sql = {}));
|
|
2123
|
+
((SQL2) => {
|
|
2124
|
+
class Aliased {
|
|
2125
|
+
constructor(sql2, fieldAlias) {
|
|
2126
|
+
this.sql = sql2;
|
|
2127
|
+
this.fieldAlias = fieldAlias;
|
|
2128
|
+
}
|
|
2129
|
+
static [entityKind] = "SQL.Aliased";
|
|
2130
|
+
/** @internal */
|
|
2131
|
+
isSelectionField = false;
|
|
2132
|
+
getSQL() {
|
|
2133
|
+
return this.sql;
|
|
2134
|
+
}
|
|
2135
|
+
/** @internal */
|
|
2136
|
+
clone() {
|
|
2137
|
+
return new Aliased(this.sql, this.fieldAlias);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
2140
|
+
SQL2.Aliased = Aliased;
|
|
2141
|
+
})(SQL || (SQL = {}));
|
|
2142
|
+
var Placeholder = class {
|
|
2143
|
+
constructor(name2) {
|
|
2144
|
+
this.name = name2;
|
|
2145
|
+
}
|
|
2146
|
+
static [entityKind] = "Placeholder";
|
|
2147
|
+
getSQL() {
|
|
2148
|
+
return new SQL([this]);
|
|
2149
|
+
}
|
|
2150
|
+
};
|
|
2151
|
+
var View = class {
|
|
2152
|
+
static [entityKind] = "View";
|
|
2153
|
+
/** @internal */
|
|
2154
|
+
[ViewBaseConfig];
|
|
2155
|
+
constructor({ name: name2, schema, selectedFields, query }) {
|
|
2156
|
+
this[ViewBaseConfig] = {
|
|
2157
|
+
name: name2,
|
|
2158
|
+
originalName: name2,
|
|
2159
|
+
schema,
|
|
2160
|
+
selectedFields,
|
|
2161
|
+
query,
|
|
2162
|
+
isExisting: !query,
|
|
2163
|
+
isAlias: false
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
getSQL() {
|
|
2167
|
+
return new SQL([this]);
|
|
2168
|
+
}
|
|
2169
|
+
};
|
|
2170
|
+
Column.prototype.getSQL = function() {
|
|
2171
|
+
return new SQL([this]);
|
|
2172
|
+
};
|
|
2173
|
+
Table.prototype.getSQL = function() {
|
|
2174
|
+
return new SQL([this]);
|
|
2175
|
+
};
|
|
2176
|
+
Subquery.prototype.getSQL = function() {
|
|
2177
|
+
return new SQL([this]);
|
|
2178
|
+
};
|
|
2179
|
+
|
|
2180
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/utils.js
|
|
2181
|
+
function getColumnNameAndConfig(a, b) {
|
|
2182
|
+
return {
|
|
2183
|
+
name: typeof a === "string" && a.length > 0 ? a : "",
|
|
2184
|
+
config: typeof a === "object" ? a : b
|
|
2185
|
+
};
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
|
|
2189
|
+
var ForeignKeyBuilder2 = class {
|
|
2190
|
+
static [entityKind] = "SQLiteForeignKeyBuilder";
|
|
2191
|
+
/** @internal */
|
|
2192
|
+
reference;
|
|
2193
|
+
/** @internal */
|
|
2194
|
+
_onUpdate;
|
|
2195
|
+
/** @internal */
|
|
2196
|
+
_onDelete;
|
|
2197
|
+
constructor(config, actions) {
|
|
2198
|
+
this.reference = () => {
|
|
2199
|
+
const { name, columns, foreignColumns } = config();
|
|
2200
|
+
return { name, columns, foreignTable: foreignColumns[0].table, foreignColumns };
|
|
2201
|
+
};
|
|
2202
|
+
if (actions) {
|
|
2203
|
+
this._onUpdate = actions.onUpdate;
|
|
2204
|
+
this._onDelete = actions.onDelete;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
onUpdate(action) {
|
|
2208
|
+
this._onUpdate = action;
|
|
2209
|
+
return this;
|
|
2210
|
+
}
|
|
2211
|
+
onDelete(action) {
|
|
2212
|
+
this._onDelete = action;
|
|
2213
|
+
return this;
|
|
2214
|
+
}
|
|
2215
|
+
/** @internal */
|
|
2216
|
+
build(table) {
|
|
2217
|
+
return new ForeignKey2(table, this);
|
|
2218
|
+
}
|
|
2219
|
+
};
|
|
2220
|
+
var ForeignKey2 = class {
|
|
2221
|
+
constructor(table, builder) {
|
|
2222
|
+
this.table = table;
|
|
2223
|
+
this.reference = builder.reference;
|
|
2224
|
+
this.onUpdate = builder._onUpdate;
|
|
2225
|
+
this.onDelete = builder._onDelete;
|
|
2226
|
+
}
|
|
2227
|
+
static [entityKind] = "SQLiteForeignKey";
|
|
2228
|
+
reference;
|
|
2229
|
+
onUpdate;
|
|
2230
|
+
onDelete;
|
|
2231
|
+
getName() {
|
|
2232
|
+
const { name, columns, foreignColumns } = this.reference();
|
|
2233
|
+
const columnNames = columns.map((column) => column.name);
|
|
2234
|
+
const foreignColumnNames = foreignColumns.map((column) => column.name);
|
|
2235
|
+
const chunks = [
|
|
2236
|
+
this.table[TableName],
|
|
2237
|
+
...columnNames,
|
|
2238
|
+
foreignColumns[0].table[TableName],
|
|
2239
|
+
...foreignColumnNames
|
|
2240
|
+
];
|
|
2241
|
+
return name ?? `${chunks.join("_")}_fk`;
|
|
2242
|
+
}
|
|
2243
|
+
};
|
|
2244
|
+
|
|
2245
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/unique-constraint.js
|
|
2246
|
+
function uniqueKeyName2(table, columns) {
|
|
2247
|
+
return `${table[TableName]}_${columns.join("_")}_unique`;
|
|
2248
|
+
}
|
|
2249
|
+
var UniqueConstraintBuilder2 = class {
|
|
2250
|
+
constructor(columns, name) {
|
|
2251
|
+
this.name = name;
|
|
2252
|
+
this.columns = columns;
|
|
2253
|
+
}
|
|
2254
|
+
static [entityKind] = "SQLiteUniqueConstraintBuilder";
|
|
2255
|
+
/** @internal */
|
|
2256
|
+
columns;
|
|
2257
|
+
/** @internal */
|
|
2258
|
+
build(table) {
|
|
2259
|
+
return new UniqueConstraint2(table, this.columns, this.name);
|
|
2260
|
+
}
|
|
2261
|
+
};
|
|
2262
|
+
var UniqueOnConstraintBuilder2 = class {
|
|
2263
|
+
static [entityKind] = "SQLiteUniqueOnConstraintBuilder";
|
|
2264
|
+
/** @internal */
|
|
2265
|
+
name;
|
|
2266
|
+
constructor(name) {
|
|
2267
|
+
this.name = name;
|
|
2268
|
+
}
|
|
2269
|
+
on(...columns) {
|
|
2270
|
+
return new UniqueConstraintBuilder2(columns, this.name);
|
|
2271
|
+
}
|
|
2272
|
+
};
|
|
2273
|
+
var UniqueConstraint2 = class {
|
|
2274
|
+
constructor(table, columns, name) {
|
|
2275
|
+
this.table = table;
|
|
2276
|
+
this.columns = columns;
|
|
2277
|
+
this.name = name ?? uniqueKeyName2(this.table, this.columns.map((column) => column.name));
|
|
2278
|
+
}
|
|
2279
|
+
static [entityKind] = "SQLiteUniqueConstraint";
|
|
2280
|
+
columns;
|
|
2281
|
+
name;
|
|
2282
|
+
getName() {
|
|
2283
|
+
return this.name;
|
|
2284
|
+
}
|
|
2285
|
+
};
|
|
2286
|
+
|
|
2287
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/common.js
|
|
2288
|
+
var SQLiteColumnBuilder = class extends ColumnBuilder {
|
|
2289
|
+
static [entityKind] = "SQLiteColumnBuilder";
|
|
2290
|
+
foreignKeyConfigs = [];
|
|
2291
|
+
references(ref, actions = {}) {
|
|
2292
|
+
this.foreignKeyConfigs.push({ ref, actions });
|
|
2293
|
+
return this;
|
|
2294
|
+
}
|
|
2295
|
+
unique(name) {
|
|
2296
|
+
this.config.isUnique = true;
|
|
2297
|
+
this.config.uniqueName = name;
|
|
2298
|
+
return this;
|
|
2299
|
+
}
|
|
2300
|
+
generatedAlwaysAs(as, config) {
|
|
2301
|
+
this.config.generated = {
|
|
2302
|
+
as,
|
|
2303
|
+
type: "always",
|
|
2304
|
+
mode: config?.mode ?? "virtual"
|
|
2305
|
+
};
|
|
2306
|
+
return this;
|
|
2307
|
+
}
|
|
2308
|
+
/** @internal */
|
|
2309
|
+
buildForeignKeys(column, table) {
|
|
2310
|
+
return this.foreignKeyConfigs.map(({ ref, actions }) => {
|
|
2311
|
+
return ((ref2, actions2) => {
|
|
2312
|
+
const builder = new ForeignKeyBuilder2(() => {
|
|
2313
|
+
const foreignColumn = ref2();
|
|
2314
|
+
return { columns: [column], foreignColumns: [foreignColumn] };
|
|
2315
|
+
});
|
|
2316
|
+
if (actions2.onUpdate) {
|
|
2317
|
+
builder.onUpdate(actions2.onUpdate);
|
|
2318
|
+
}
|
|
2319
|
+
if (actions2.onDelete) {
|
|
2320
|
+
builder.onDelete(actions2.onDelete);
|
|
2321
|
+
}
|
|
2322
|
+
return builder.build(table);
|
|
2323
|
+
})(ref, actions);
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
};
|
|
2327
|
+
var SQLiteColumn = class extends Column {
|
|
2328
|
+
constructor(table, config) {
|
|
2329
|
+
if (!config.uniqueName) {
|
|
2330
|
+
config.uniqueName = uniqueKeyName2(table, [config.name]);
|
|
2331
|
+
}
|
|
2332
|
+
super(table, config);
|
|
2333
|
+
this.table = table;
|
|
2334
|
+
}
|
|
2335
|
+
static [entityKind] = "SQLiteColumn";
|
|
2336
|
+
};
|
|
2337
|
+
|
|
2338
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/blob.js
|
|
2339
|
+
var SQLiteBigIntBuilder = class extends SQLiteColumnBuilder {
|
|
2340
|
+
static [entityKind] = "SQLiteBigIntBuilder";
|
|
2341
|
+
constructor(name) {
|
|
2342
|
+
super(name, "bigint", "SQLiteBigInt");
|
|
2343
|
+
}
|
|
2344
|
+
/** @internal */
|
|
2345
|
+
build(table) {
|
|
2346
|
+
return new SQLiteBigInt(table, this.config);
|
|
2347
|
+
}
|
|
2348
|
+
};
|
|
2349
|
+
var SQLiteBigInt = class extends SQLiteColumn {
|
|
2350
|
+
static [entityKind] = "SQLiteBigInt";
|
|
2351
|
+
getSQLType() {
|
|
2352
|
+
return "blob";
|
|
2353
|
+
}
|
|
2354
|
+
mapFromDriverValue(value) {
|
|
2355
|
+
return BigInt(Buffer.isBuffer(value) ? value.toString() : String.fromCodePoint(...value));
|
|
2356
|
+
}
|
|
2357
|
+
mapToDriverValue(value) {
|
|
2358
|
+
return Buffer.from(value.toString());
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
var SQLiteBlobJsonBuilder = class extends SQLiteColumnBuilder {
|
|
2362
|
+
static [entityKind] = "SQLiteBlobJsonBuilder";
|
|
2363
|
+
constructor(name) {
|
|
2364
|
+
super(name, "json", "SQLiteBlobJson");
|
|
2365
|
+
}
|
|
2366
|
+
/** @internal */
|
|
2367
|
+
build(table) {
|
|
2368
|
+
return new SQLiteBlobJson(
|
|
2369
|
+
table,
|
|
2370
|
+
this.config
|
|
2371
|
+
);
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
var SQLiteBlobJson = class extends SQLiteColumn {
|
|
2375
|
+
static [entityKind] = "SQLiteBlobJson";
|
|
2376
|
+
getSQLType() {
|
|
2377
|
+
return "blob";
|
|
2378
|
+
}
|
|
2379
|
+
mapFromDriverValue(value) {
|
|
2380
|
+
return JSON.parse(Buffer.isBuffer(value) ? value.toString() : String.fromCodePoint(...value));
|
|
2381
|
+
}
|
|
2382
|
+
mapToDriverValue(value) {
|
|
2383
|
+
return Buffer.from(JSON.stringify(value));
|
|
2384
|
+
}
|
|
2385
|
+
};
|
|
2386
|
+
var SQLiteBlobBufferBuilder = class extends SQLiteColumnBuilder {
|
|
2387
|
+
static [entityKind] = "SQLiteBlobBufferBuilder";
|
|
2388
|
+
constructor(name) {
|
|
2389
|
+
super(name, "buffer", "SQLiteBlobBuffer");
|
|
2390
|
+
}
|
|
2391
|
+
/** @internal */
|
|
2392
|
+
build(table) {
|
|
2393
|
+
return new SQLiteBlobBuffer(table, this.config);
|
|
2394
|
+
}
|
|
2395
|
+
};
|
|
2396
|
+
var SQLiteBlobBuffer = class extends SQLiteColumn {
|
|
2397
|
+
static [entityKind] = "SQLiteBlobBuffer";
|
|
2398
|
+
getSQLType() {
|
|
2399
|
+
return "blob";
|
|
2400
|
+
}
|
|
2401
|
+
};
|
|
2402
|
+
function blob(a, b) {
|
|
2403
|
+
const { name, config } = getColumnNameAndConfig(a, b);
|
|
2404
|
+
if (config?.mode === "json") {
|
|
2405
|
+
return new SQLiteBlobJsonBuilder(name);
|
|
2406
|
+
}
|
|
2407
|
+
if (config?.mode === "bigint") {
|
|
2408
|
+
return new SQLiteBigIntBuilder(name);
|
|
2409
|
+
}
|
|
2410
|
+
return new SQLiteBlobBufferBuilder(name);
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/custom.js
|
|
2414
|
+
var SQLiteCustomColumnBuilder = class extends SQLiteColumnBuilder {
|
|
2415
|
+
static [entityKind] = "SQLiteCustomColumnBuilder";
|
|
2416
|
+
constructor(name, fieldConfig, customTypeParams) {
|
|
2417
|
+
super(name, "custom", "SQLiteCustomColumn");
|
|
2418
|
+
this.config.fieldConfig = fieldConfig;
|
|
2419
|
+
this.config.customTypeParams = customTypeParams;
|
|
2420
|
+
}
|
|
2421
|
+
/** @internal */
|
|
2422
|
+
build(table) {
|
|
2423
|
+
return new SQLiteCustomColumn(
|
|
2424
|
+
table,
|
|
2425
|
+
this.config
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
};
|
|
2429
|
+
var SQLiteCustomColumn = class extends SQLiteColumn {
|
|
2430
|
+
static [entityKind] = "SQLiteCustomColumn";
|
|
2431
|
+
sqlName;
|
|
2432
|
+
mapTo;
|
|
2433
|
+
mapFrom;
|
|
2434
|
+
constructor(table, config) {
|
|
2435
|
+
super(table, config);
|
|
2436
|
+
this.sqlName = config.customTypeParams.dataType(config.fieldConfig);
|
|
2437
|
+
this.mapTo = config.customTypeParams.toDriver;
|
|
2438
|
+
this.mapFrom = config.customTypeParams.fromDriver;
|
|
2439
|
+
}
|
|
2440
|
+
getSQLType() {
|
|
2441
|
+
return this.sqlName;
|
|
2442
|
+
}
|
|
2443
|
+
mapFromDriverValue(value) {
|
|
2444
|
+
return typeof this.mapFrom === "function" ? this.mapFrom(value) : value;
|
|
2445
|
+
}
|
|
2446
|
+
mapToDriverValue(value) {
|
|
2447
|
+
return typeof this.mapTo === "function" ? this.mapTo(value) : value;
|
|
2448
|
+
}
|
|
2449
|
+
};
|
|
2450
|
+
function customType(customTypeParams) {
|
|
2451
|
+
return (a, b) => {
|
|
2452
|
+
const { name, config } = getColumnNameAndConfig(a, b);
|
|
2453
|
+
return new SQLiteCustomColumnBuilder(
|
|
2454
|
+
name,
|
|
2455
|
+
config,
|
|
2456
|
+
customTypeParams
|
|
2457
|
+
);
|
|
2458
|
+
};
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/integer.js
|
|
2462
|
+
var SQLiteBaseIntegerBuilder = class extends SQLiteColumnBuilder {
|
|
2463
|
+
static [entityKind] = "SQLiteBaseIntegerBuilder";
|
|
2464
|
+
constructor(name, dataType, columnType) {
|
|
2465
|
+
super(name, dataType, columnType);
|
|
2466
|
+
this.config.autoIncrement = false;
|
|
2467
|
+
}
|
|
2468
|
+
primaryKey(config) {
|
|
2469
|
+
if (config?.autoIncrement) {
|
|
2470
|
+
this.config.autoIncrement = true;
|
|
2471
|
+
}
|
|
2472
|
+
this.config.hasDefault = true;
|
|
2473
|
+
return super.primaryKey();
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
var SQLiteBaseInteger = class extends SQLiteColumn {
|
|
2477
|
+
static [entityKind] = "SQLiteBaseInteger";
|
|
2478
|
+
autoIncrement = this.config.autoIncrement;
|
|
2479
|
+
getSQLType() {
|
|
2480
|
+
return "integer";
|
|
2481
|
+
}
|
|
2482
|
+
};
|
|
2483
|
+
var SQLiteIntegerBuilder = class extends SQLiteBaseIntegerBuilder {
|
|
2484
|
+
static [entityKind] = "SQLiteIntegerBuilder";
|
|
2485
|
+
constructor(name) {
|
|
2486
|
+
super(name, "number", "SQLiteInteger");
|
|
2487
|
+
}
|
|
2488
|
+
build(table) {
|
|
2489
|
+
return new SQLiteInteger(
|
|
2490
|
+
table,
|
|
2491
|
+
this.config
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2494
|
+
};
|
|
2495
|
+
var SQLiteInteger = class extends SQLiteBaseInteger {
|
|
2496
|
+
static [entityKind] = "SQLiteInteger";
|
|
2497
|
+
};
|
|
2498
|
+
var SQLiteTimestampBuilder = class extends SQLiteBaseIntegerBuilder {
|
|
2499
|
+
static [entityKind] = "SQLiteTimestampBuilder";
|
|
2500
|
+
constructor(name, mode) {
|
|
2501
|
+
super(name, "date", "SQLiteTimestamp");
|
|
2502
|
+
this.config.mode = mode;
|
|
2503
|
+
}
|
|
2504
|
+
/**
|
|
2505
|
+
* @deprecated Use `default()` with your own expression instead.
|
|
2506
|
+
*
|
|
2507
|
+
* Adds `DEFAULT (cast((julianday('now') - 2440587.5)*86400000 as integer))` to the column, which is the current epoch timestamp in milliseconds.
|
|
2508
|
+
*/
|
|
2509
|
+
defaultNow() {
|
|
2510
|
+
return this.default(sql`(cast((julianday('now') - 2440587.5)*86400000 as integer))`);
|
|
2511
|
+
}
|
|
2512
|
+
build(table) {
|
|
2513
|
+
return new SQLiteTimestamp(
|
|
2514
|
+
table,
|
|
2515
|
+
this.config
|
|
2516
|
+
);
|
|
2517
|
+
}
|
|
2518
|
+
};
|
|
2519
|
+
var SQLiteTimestamp = class extends SQLiteBaseInteger {
|
|
2520
|
+
static [entityKind] = "SQLiteTimestamp";
|
|
2521
|
+
mode = this.config.mode;
|
|
2522
|
+
mapFromDriverValue(value) {
|
|
2523
|
+
if (this.config.mode === "timestamp") {
|
|
2524
|
+
return new Date(value * 1e3);
|
|
2525
|
+
}
|
|
2526
|
+
return new Date(value);
|
|
2527
|
+
}
|
|
2528
|
+
mapToDriverValue(value) {
|
|
2529
|
+
const unix = value.getTime();
|
|
2530
|
+
if (this.config.mode === "timestamp") {
|
|
2531
|
+
return Math.floor(unix / 1e3);
|
|
2532
|
+
}
|
|
2533
|
+
return unix;
|
|
2534
|
+
}
|
|
2535
|
+
};
|
|
2536
|
+
var SQLiteBooleanBuilder = class extends SQLiteBaseIntegerBuilder {
|
|
2537
|
+
static [entityKind] = "SQLiteBooleanBuilder";
|
|
2538
|
+
constructor(name, mode) {
|
|
2539
|
+
super(name, "boolean", "SQLiteBoolean");
|
|
2540
|
+
this.config.mode = mode;
|
|
2541
|
+
}
|
|
2542
|
+
build(table) {
|
|
2543
|
+
return new SQLiteBoolean(
|
|
2544
|
+
table,
|
|
2545
|
+
this.config
|
|
2546
|
+
);
|
|
2547
|
+
}
|
|
2548
|
+
};
|
|
2549
|
+
var SQLiteBoolean = class extends SQLiteBaseInteger {
|
|
2550
|
+
static [entityKind] = "SQLiteBoolean";
|
|
2551
|
+
mode = this.config.mode;
|
|
2552
|
+
mapFromDriverValue(value) {
|
|
2553
|
+
return Number(value) === 1;
|
|
2554
|
+
}
|
|
2555
|
+
mapToDriverValue(value) {
|
|
2556
|
+
return value ? 1 : 0;
|
|
2557
|
+
}
|
|
2558
|
+
};
|
|
2559
|
+
function integer(a, b) {
|
|
2560
|
+
const { name, config } = getColumnNameAndConfig(a, b);
|
|
2561
|
+
if (config?.mode === "timestamp" || config?.mode === "timestamp_ms") {
|
|
2562
|
+
return new SQLiteTimestampBuilder(name, config.mode);
|
|
2563
|
+
}
|
|
2564
|
+
if (config?.mode === "boolean") {
|
|
2565
|
+
return new SQLiteBooleanBuilder(name, config.mode);
|
|
2566
|
+
}
|
|
2567
|
+
return new SQLiteIntegerBuilder(name);
|
|
2568
|
+
}
|
|
2569
|
+
|
|
2570
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/numeric.js
|
|
2571
|
+
var SQLiteNumericBuilder = class extends SQLiteColumnBuilder {
|
|
2572
|
+
static [entityKind] = "SQLiteNumericBuilder";
|
|
2573
|
+
constructor(name) {
|
|
2574
|
+
super(name, "string", "SQLiteNumeric");
|
|
2575
|
+
}
|
|
2576
|
+
/** @internal */
|
|
2577
|
+
build(table) {
|
|
2578
|
+
return new SQLiteNumeric(
|
|
2579
|
+
table,
|
|
2580
|
+
this.config
|
|
2581
|
+
);
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
var SQLiteNumeric = class extends SQLiteColumn {
|
|
2585
|
+
static [entityKind] = "SQLiteNumeric";
|
|
2586
|
+
getSQLType() {
|
|
2587
|
+
return "numeric";
|
|
2588
|
+
}
|
|
2589
|
+
};
|
|
2590
|
+
function numeric(name) {
|
|
2591
|
+
return new SQLiteNumericBuilder(name ?? "");
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/real.js
|
|
2595
|
+
var SQLiteRealBuilder = class extends SQLiteColumnBuilder {
|
|
2596
|
+
static [entityKind] = "SQLiteRealBuilder";
|
|
2597
|
+
constructor(name) {
|
|
2598
|
+
super(name, "number", "SQLiteReal");
|
|
2599
|
+
}
|
|
2600
|
+
/** @internal */
|
|
2601
|
+
build(table) {
|
|
2602
|
+
return new SQLiteReal(table, this.config);
|
|
2603
|
+
}
|
|
2604
|
+
};
|
|
2605
|
+
var SQLiteReal = class extends SQLiteColumn {
|
|
2606
|
+
static [entityKind] = "SQLiteReal";
|
|
2607
|
+
getSQLType() {
|
|
2608
|
+
return "real";
|
|
2609
|
+
}
|
|
2610
|
+
};
|
|
2611
|
+
function real(name) {
|
|
2612
|
+
return new SQLiteRealBuilder(name ?? "");
|
|
2613
|
+
}
|
|
2614
|
+
|
|
2615
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/text.js
|
|
2616
|
+
var SQLiteTextBuilder = class extends SQLiteColumnBuilder {
|
|
2617
|
+
static [entityKind] = "SQLiteTextBuilder";
|
|
2618
|
+
constructor(name, config) {
|
|
2619
|
+
super(name, "string", "SQLiteText");
|
|
2620
|
+
this.config.enumValues = config.enum;
|
|
2621
|
+
this.config.length = config.length;
|
|
2622
|
+
}
|
|
2623
|
+
/** @internal */
|
|
2624
|
+
build(table) {
|
|
2625
|
+
return new SQLiteText(table, this.config);
|
|
2626
|
+
}
|
|
2627
|
+
};
|
|
2628
|
+
var SQLiteText = class extends SQLiteColumn {
|
|
2629
|
+
static [entityKind] = "SQLiteText";
|
|
2630
|
+
enumValues = this.config.enumValues;
|
|
2631
|
+
length = this.config.length;
|
|
2632
|
+
constructor(table, config) {
|
|
2633
|
+
super(table, config);
|
|
2634
|
+
}
|
|
2635
|
+
getSQLType() {
|
|
2636
|
+
return `text${this.config.length ? `(${this.config.length})` : ""}`;
|
|
2637
|
+
}
|
|
2638
|
+
};
|
|
2639
|
+
var SQLiteTextJsonBuilder = class extends SQLiteColumnBuilder {
|
|
2640
|
+
static [entityKind] = "SQLiteTextJsonBuilder";
|
|
2641
|
+
constructor(name) {
|
|
2642
|
+
super(name, "json", "SQLiteTextJson");
|
|
2643
|
+
}
|
|
2644
|
+
/** @internal */
|
|
2645
|
+
build(table) {
|
|
2646
|
+
return new SQLiteTextJson(
|
|
2647
|
+
table,
|
|
2648
|
+
this.config
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
};
|
|
2652
|
+
var SQLiteTextJson = class extends SQLiteColumn {
|
|
2653
|
+
static [entityKind] = "SQLiteTextJson";
|
|
2654
|
+
getSQLType() {
|
|
2655
|
+
return "text";
|
|
2656
|
+
}
|
|
2657
|
+
mapFromDriverValue(value) {
|
|
2658
|
+
return JSON.parse(value);
|
|
2659
|
+
}
|
|
2660
|
+
mapToDriverValue(value) {
|
|
2661
|
+
return JSON.stringify(value);
|
|
2662
|
+
}
|
|
2663
|
+
};
|
|
2664
|
+
function text(a, b = {}) {
|
|
2665
|
+
const { name, config } = getColumnNameAndConfig(a, b);
|
|
2666
|
+
if (config.mode === "json") {
|
|
2667
|
+
return new SQLiteTextJsonBuilder(name);
|
|
2668
|
+
}
|
|
2669
|
+
return new SQLiteTextBuilder(name, config);
|
|
2670
|
+
}
|
|
2671
|
+
|
|
2672
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/columns/all.js
|
|
2673
|
+
function getSQLiteColumnBuilders() {
|
|
2674
|
+
return {
|
|
2675
|
+
blob,
|
|
2676
|
+
customType,
|
|
2677
|
+
integer,
|
|
2678
|
+
numeric,
|
|
2679
|
+
real,
|
|
2680
|
+
text
|
|
2681
|
+
};
|
|
2682
|
+
}
|
|
2683
|
+
|
|
2684
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/table.js
|
|
2685
|
+
var InlineForeignKeys = /* @__PURE__ */ Symbol.for("drizzle:SQLiteInlineForeignKeys");
|
|
2686
|
+
var SQLiteTable = class extends Table {
|
|
2687
|
+
static [entityKind] = "SQLiteTable";
|
|
2688
|
+
/** @internal */
|
|
2689
|
+
static Symbol = Object.assign({}, Table.Symbol, {
|
|
2690
|
+
InlineForeignKeys
|
|
2691
|
+
});
|
|
2692
|
+
/** @internal */
|
|
2693
|
+
[Table.Symbol.Columns];
|
|
2694
|
+
/** @internal */
|
|
2695
|
+
[InlineForeignKeys] = [];
|
|
2696
|
+
/** @internal */
|
|
2697
|
+
[Table.Symbol.ExtraConfigBuilder] = void 0;
|
|
2698
|
+
};
|
|
2699
|
+
function sqliteTableBase(name, columns, extraConfig, schema, baseName = name) {
|
|
2700
|
+
const rawTable = new SQLiteTable(name, schema, baseName);
|
|
2701
|
+
const parsedColumns = typeof columns === "function" ? columns(getSQLiteColumnBuilders()) : columns;
|
|
2702
|
+
const builtColumns = Object.fromEntries(
|
|
2703
|
+
Object.entries(parsedColumns).map(([name2, colBuilderBase]) => {
|
|
2704
|
+
const colBuilder = colBuilderBase;
|
|
2705
|
+
colBuilder.setName(name2);
|
|
2706
|
+
const column = colBuilder.build(rawTable);
|
|
2707
|
+
rawTable[InlineForeignKeys].push(...colBuilder.buildForeignKeys(column, rawTable));
|
|
2708
|
+
return [name2, column];
|
|
2709
|
+
})
|
|
2710
|
+
);
|
|
2711
|
+
const table = Object.assign(rawTable, builtColumns);
|
|
2712
|
+
table[Table.Symbol.Columns] = builtColumns;
|
|
2713
|
+
table[Table.Symbol.ExtraConfigColumns] = builtColumns;
|
|
2714
|
+
if (extraConfig) {
|
|
2715
|
+
table[SQLiteTable.Symbol.ExtraConfigBuilder] = extraConfig;
|
|
2716
|
+
}
|
|
2717
|
+
return table;
|
|
2718
|
+
}
|
|
2719
|
+
var sqliteTable = (name, columns, extraConfig) => {
|
|
2720
|
+
return sqliteTableBase(name, columns, extraConfig);
|
|
2721
|
+
};
|
|
2722
|
+
|
|
2723
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/indexes.js
|
|
2724
|
+
var IndexBuilderOn = class {
|
|
2725
|
+
constructor(name, unique) {
|
|
2726
|
+
this.name = name;
|
|
2727
|
+
this.unique = unique;
|
|
2728
|
+
}
|
|
2729
|
+
static [entityKind] = "SQLiteIndexBuilderOn";
|
|
2730
|
+
on(...columns) {
|
|
2731
|
+
return new IndexBuilder(this.name, columns, this.unique);
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
var IndexBuilder = class {
|
|
2735
|
+
static [entityKind] = "SQLiteIndexBuilder";
|
|
2736
|
+
/** @internal */
|
|
2737
|
+
config;
|
|
2738
|
+
constructor(name, columns, unique) {
|
|
2739
|
+
this.config = {
|
|
2740
|
+
name,
|
|
2741
|
+
columns,
|
|
2742
|
+
unique,
|
|
2743
|
+
where: void 0
|
|
2744
|
+
};
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Condition for partial index.
|
|
2748
|
+
*/
|
|
2749
|
+
where(condition) {
|
|
2750
|
+
this.config.where = condition;
|
|
2751
|
+
return this;
|
|
2752
|
+
}
|
|
2753
|
+
/** @internal */
|
|
2754
|
+
build(table) {
|
|
2755
|
+
return new Index(this.config, table);
|
|
2756
|
+
}
|
|
2757
|
+
};
|
|
2758
|
+
var Index = class {
|
|
2759
|
+
static [entityKind] = "SQLiteIndex";
|
|
2760
|
+
config;
|
|
2761
|
+
constructor(config, table) {
|
|
2762
|
+
this.config = { ...config, table };
|
|
2763
|
+
}
|
|
2764
|
+
};
|
|
2765
|
+
function index(name) {
|
|
2766
|
+
return new IndexBuilderOn(name, false);
|
|
2767
|
+
}
|
|
2768
|
+
function uniqueIndex(name) {
|
|
2769
|
+
return new IndexBuilderOn(name, true);
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// ../../node_modules/.pnpm/drizzle-orm@0.36.4_@cloudflare+workers-types@4.20260416.2_@types+better-sqlite3@7.6.13__6bda8a68f3e10d0b03cd2031bac7989c/node_modules/drizzle-orm/sqlite-core/primary-keys.js
|
|
2773
|
+
function primaryKey(...config) {
|
|
2774
|
+
if (config[0].columns) {
|
|
2775
|
+
return new PrimaryKeyBuilder(config[0].columns, config[0].name);
|
|
2776
|
+
}
|
|
2777
|
+
return new PrimaryKeyBuilder(config);
|
|
2778
|
+
}
|
|
2779
|
+
var PrimaryKeyBuilder = class {
|
|
2780
|
+
static [entityKind] = "SQLitePrimaryKeyBuilder";
|
|
2781
|
+
/** @internal */
|
|
2782
|
+
columns;
|
|
2783
|
+
/** @internal */
|
|
2784
|
+
name;
|
|
2785
|
+
constructor(columns, name) {
|
|
2786
|
+
this.columns = columns;
|
|
2787
|
+
this.name = name;
|
|
2788
|
+
}
|
|
2789
|
+
/** @internal */
|
|
2790
|
+
build(table) {
|
|
2791
|
+
return new PrimaryKey(table, this.columns, this.name);
|
|
2792
|
+
}
|
|
2793
|
+
};
|
|
2794
|
+
var PrimaryKey = class {
|
|
2795
|
+
constructor(table, columns, name) {
|
|
2796
|
+
this.table = table;
|
|
2797
|
+
this.columns = columns;
|
|
2798
|
+
this.name = name;
|
|
2799
|
+
}
|
|
2800
|
+
static [entityKind] = "SQLitePrimaryKey";
|
|
2801
|
+
columns;
|
|
2802
|
+
name;
|
|
2803
|
+
getName() {
|
|
2804
|
+
return this.name ?? `${this.table[SQLiteTable.Symbol.Name]}_${this.columns.map((column) => column.name).join("_")}_pk`;
|
|
2805
|
+
}
|
|
2806
|
+
};
|
|
2807
|
+
|
|
2808
|
+
// ../core/src/db/schema.ts
|
|
2809
|
+
var projects = sqliteTable(
|
|
2810
|
+
"projects",
|
|
2811
|
+
{
|
|
2812
|
+
id: text("id").primaryKey(),
|
|
2813
|
+
name: text("name").notNull(),
|
|
2814
|
+
domain: text("domain"),
|
|
2815
|
+
ownerEmail: text("owner_email").notNull(),
|
|
2816
|
+
plan: text("plan", { enum: ["free", "paid"] }).notNull().default("free"),
|
|
2817
|
+
status: text("status", { enum: ["active", "inactive"] }).notNull().default("active"),
|
|
2818
|
+
// Stripe billing
|
|
2819
|
+
stripeCustomerId: text("stripe_customer_id").unique(),
|
|
2820
|
+
stripeSubscriptionId: text("stripe_subscription_id").unique(),
|
|
2821
|
+
stripeItemIdBase: text("stripe_item_id_base"),
|
|
2822
|
+
stripeItemIdExperiments: text("stripe_item_id_experiments"),
|
|
2823
|
+
stripeItemIdGates: text("stripe_item_id_gates"),
|
|
2824
|
+
stripeItemIdConfigs: text("stripe_item_id_configs"),
|
|
2825
|
+
subscriptionStatus: text("subscription_status", {
|
|
2826
|
+
enum: ["none", "trialing", "active", "past_due", "canceled", "incomplete"]
|
|
2827
|
+
}).notNull().default("none"),
|
|
2828
|
+
currentPeriodEnd: text("current_period_end"),
|
|
2829
|
+
trialEndsAt: text("trial_ends_at"),
|
|
2830
|
+
cancelAtPeriodEnd: integer("cancel_at_period_end").notNull().default(0),
|
|
2831
|
+
billingInterval: text("billing_interval", { enum: ["monthly", "annual"] }).notNull().default("monthly"),
|
|
2832
|
+
// Per-project module toggles. Control which surfaces (admin tabs, devtools
|
|
2833
|
+
// panels) are exposed for this project. All default to enabled so existing
|
|
2834
|
+
// projects keep their full feature set after the migration.
|
|
2835
|
+
moduleTranslations: integer("module_translations", { mode: "boolean" }).notNull().default(true),
|
|
2836
|
+
moduleConfigs: integer("module_configs", { mode: "boolean" }).notNull().default(true),
|
|
2837
|
+
moduleGates: integer("module_gates", { mode: "boolean" }).notNull().default(true),
|
|
2838
|
+
moduleExperiments: integer("module_experiments", { mode: "boolean" }).notNull().default(true),
|
|
2839
|
+
moduleFeedback: integer("module_feedback", { mode: "boolean" }).notNull().default(true),
|
|
2840
|
+
moduleUser: integer("module_user", { mode: "boolean" }).notNull().default(true),
|
|
2841
|
+
moduleEvents: integer("module_events", { mode: "boolean" }).notNull().default(true),
|
|
2842
|
+
createdAt: text("created_at").notNull(),
|
|
2843
|
+
updatedAt: text("updated_at").notNull()
|
|
2844
|
+
},
|
|
2845
|
+
(t) => ({
|
|
2846
|
+
// One project per (owner, domain): a single account can't accidentally
|
|
2847
|
+
// create two projects pointing at the same site. Multiple projects per
|
|
2848
|
+
// owner are still allowed across different domains, and `domain` may be
|
|
2849
|
+
// NULL for projects without a configured site (SQLite treats each NULL
|
|
2850
|
+
// as distinct under UNIQUE, so multiple no-domain projects per owner are
|
|
2851
|
+
// permitted).
|
|
2852
|
+
ownerDomainUniq: uniqueIndex("projects_owner_domain_uniq").on(t.ownerEmail, t.domain)
|
|
2853
|
+
})
|
|
2854
|
+
);
|
|
2855
|
+
var users = sqliteTable("users", {
|
|
2856
|
+
email: text("email").primaryKey(),
|
|
2857
|
+
termsAcceptedAt: text("terms_accepted_at"),
|
|
2858
|
+
termsVersion: text("terms_version"),
|
|
2859
|
+
onboardingCompletedAt: text("onboarding_completed_at"),
|
|
2860
|
+
createdAt: text("created_at").notNull(),
|
|
2861
|
+
updatedAt: text("updated_at").notNull()
|
|
2862
|
+
});
|
|
2863
|
+
var projectMembers = sqliteTable(
|
|
2864
|
+
"project_members",
|
|
2865
|
+
{
|
|
2866
|
+
id: text("id").primaryKey(),
|
|
2867
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
2868
|
+
email: text("email").notNull(),
|
|
2869
|
+
role: text("role", { enum: ["admin", "editor", "viewer"] }).notNull().default("editor"),
|
|
2870
|
+
status: text("status", { enum: ["active", "pending", "removed"] }).notNull().default("pending"),
|
|
2871
|
+
invitedByEmail: text("invited_by_email").notNull(),
|
|
2872
|
+
invitedAt: text("invited_at").notNull(),
|
|
2873
|
+
acceptedAt: text("accepted_at"),
|
|
2874
|
+
removedAt: text("removed_at")
|
|
2875
|
+
},
|
|
2876
|
+
(t) => ({
|
|
2877
|
+
projectIdx: index("project_members_project").on(t.projectId, t.status),
|
|
2878
|
+
projectEmailUniq: uniqueIndex("project_members_project_email").on(t.projectId, t.email)
|
|
2879
|
+
})
|
|
2880
|
+
);
|
|
2881
|
+
var sdkKeys = sqliteTable(
|
|
2882
|
+
"sdk_keys",
|
|
2883
|
+
{
|
|
2884
|
+
id: text("id").primaryKey(),
|
|
2885
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
2886
|
+
keyHash: text("key_hash").unique().notNull(),
|
|
2887
|
+
type: text("type", { enum: ["server", "client", "admin"] }).notNull(),
|
|
2888
|
+
scope: text("scope", { enum: ["experiments", "i18n"] }).notNull().default("experiments"),
|
|
2889
|
+
createdAt: text("created_at").notNull(),
|
|
2890
|
+
revokedAt: text("revoked_at"),
|
|
2891
|
+
expiresAt: text("expires_at"),
|
|
2892
|
+
// Email of the human who minted this key (admin keys via dashboard or
|
|
2893
|
+
// CLI device-flow). Used to authorize cross-project --project overrides
|
|
2894
|
+
// by joining against project_members. Null for older keys + for
|
|
2895
|
+
// service-to-service keys without a human owner.
|
|
2896
|
+
createdByEmail: text("created_by_email")
|
|
2897
|
+
},
|
|
2898
|
+
(t) => ({ hashIdx: index("sdk_keys_hash").on(t.keyHash) })
|
|
2899
|
+
);
|
|
2900
|
+
var cliAuthSessions = sqliteTable(
|
|
2901
|
+
"cli_auth_sessions",
|
|
2902
|
+
{
|
|
2903
|
+
state: text("state").primaryKey(),
|
|
2904
|
+
codeChallenge: text("code_challenge").notNull(),
|
|
2905
|
+
projectId: text("project_id").references(() => projects.id),
|
|
2906
|
+
tokenHash: text("token_hash"),
|
|
2907
|
+
status: text("status", { enum: ["pending", "complete", "expired"] }).notNull().default("pending"),
|
|
2908
|
+
createdAt: text("created_at").notNull(),
|
|
2909
|
+
expiresAt: text("expires_at").notNull(),
|
|
2910
|
+
completedAt: text("completed_at")
|
|
2911
|
+
},
|
|
2912
|
+
(t) => ({ expiresIdx: index("cli_auth_sessions_expires").on(t.expiresAt) })
|
|
2913
|
+
);
|
|
2914
|
+
var cliTokens = sqliteTable(
|
|
2915
|
+
"cli_tokens",
|
|
2916
|
+
{
|
|
2917
|
+
tokenId: text("token_id").primaryKey(),
|
|
2918
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
2919
|
+
userEmail: text("user_email").notNull(),
|
|
2920
|
+
issuedAt: text("issued_at").notNull(),
|
|
2921
|
+
expiresAt: text("expires_at").notNull(),
|
|
2922
|
+
revokedAt: text("revoked_at"),
|
|
2923
|
+
lastUsedAt: text("last_used_at")
|
|
2924
|
+
},
|
|
2925
|
+
(t) => ({
|
|
2926
|
+
projectIdx: index("cli_tokens_project").on(t.projectId),
|
|
2927
|
+
expiresIdx: index("cli_tokens_expires").on(t.expiresAt)
|
|
2928
|
+
})
|
|
2929
|
+
);
|
|
2930
|
+
var analysisFailures = sqliteTable("analysis_failures", {
|
|
2931
|
+
projectId: text("project_id").primaryKey().references(() => projects.id),
|
|
2932
|
+
failedAt: text("failed_at").notNull(),
|
|
2933
|
+
retryCount: integer("retry_count").notNull().default(3),
|
|
2934
|
+
messageBody: text("message_body"),
|
|
2935
|
+
resolvedAt: text("resolved_at")
|
|
2936
|
+
});
|
|
2937
|
+
var systemHealth = sqliteTable("system_health", {
|
|
2938
|
+
key: text("key").primaryKey(),
|
|
2939
|
+
lastFiredAt: text("last_fired_at").notNull(),
|
|
2940
|
+
projectsEnqueued: integer("projects_enqueued")
|
|
2941
|
+
});
|
|
2942
|
+
var auditLog = sqliteTable(
|
|
2943
|
+
"audit_log",
|
|
2944
|
+
{
|
|
2945
|
+
id: text("id").primaryKey(),
|
|
2946
|
+
projectId: text("project_id").notNull().references(() => projects.id),
|
|
2947
|
+
actorEmail: text("actor_email").notNull(),
|
|
2948
|
+
actorType: text("actor_type", { enum: ["user", "cli", "system"] }).notNull(),
|
|
2949
|
+
action: text("action").notNull(),
|
|
2950
|
+
resourceType: text("resource_type").notNull(),
|
|
2951
|
+
resourceId: text("resource_id"),
|
|
2952
|
+
payload: text("payload"),
|
|
2953
|
+
createdAt: text("created_at").notNull()
|
|
2954
|
+
},
|
|
2955
|
+
(t) => ({
|
|
2956
|
+
projectIdx: index("audit_log_project").on(t.projectId, t.createdAt),
|
|
2957
|
+
actorIdx: index("audit_log_actor").on(t.actorEmail),
|
|
2958
|
+
resourceIdx: index("audit_log_resource").on(t.resourceType, t.resourceId)
|
|
2959
|
+
})
|
|
2960
|
+
);
|
|
2961
|
+
var gates = sqliteTable(
|
|
2962
|
+
"gates",
|
|
2963
|
+
{
|
|
2964
|
+
id: text("id").primaryKey(),
|
|
2965
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
2966
|
+
name: text("name").notNull(),
|
|
2967
|
+
rules: text("rules", { mode: "json" }).$type().notNull(),
|
|
2968
|
+
rolloutPct: integer("rollout_pct").notNull().default(0),
|
|
2969
|
+
salt: text("salt").notNull(),
|
|
2970
|
+
enabled: integer("enabled").notNull().default(1),
|
|
2971
|
+
// Gatekeeper metadata + ordered sub-gate stack. Stack is null on rows that
|
|
2972
|
+
// predate the gatekeeper migration; eval falls back to rules+rolloutPct.
|
|
2973
|
+
title: text("title"),
|
|
2974
|
+
description: text("description"),
|
|
2975
|
+
folder: text("folder"),
|
|
2976
|
+
groupName: text("group_name"),
|
|
2977
|
+
ownerEmail: text("owner_email"),
|
|
2978
|
+
stack: text("stack", { mode: "json" }).$type(),
|
|
2979
|
+
updatedAt: text("updated_at").notNull(),
|
|
2980
|
+
deletedAt: text("deleted_at")
|
|
2981
|
+
},
|
|
2982
|
+
(t) => ({
|
|
2983
|
+
projectIdx: index("gates_project").on(t.projectId),
|
|
2984
|
+
projectNameUniq: uniqueIndex("gates_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
2985
|
+
})
|
|
2986
|
+
);
|
|
2987
|
+
var CONFIG_ENVS = ["dev", "staging", "prod"];
|
|
2988
|
+
var CONFIG_KINDS = ["config", "killswitch"];
|
|
2989
|
+
var DEFAULT_CONFIG_SCHEMA = {
|
|
2990
|
+
type: "object",
|
|
2991
|
+
properties: {},
|
|
2992
|
+
additionalProperties: true
|
|
2993
|
+
};
|
|
2994
|
+
var configs = sqliteTable(
|
|
2995
|
+
"configs",
|
|
2996
|
+
{
|
|
2997
|
+
id: text("id").primaryKey(),
|
|
2998
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
2999
|
+
name: text("name").notNull(),
|
|
3000
|
+
description: text("description"),
|
|
3001
|
+
kind: text("kind", { enum: CONFIG_KINDS }).notNull().default("config"),
|
|
3002
|
+
schemaJson: text("schema_json", { mode: "json" }).$type().notNull().default(DEFAULT_CONFIG_SCHEMA),
|
|
3003
|
+
updatedAt: text("updated_at").notNull(),
|
|
3004
|
+
deletedAt: text("deleted_at")
|
|
3005
|
+
},
|
|
3006
|
+
(t) => ({
|
|
3007
|
+
uniq: uniqueIndex("configs_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3008
|
+
})
|
|
3009
|
+
);
|
|
3010
|
+
var configValues = sqliteTable(
|
|
3011
|
+
"config_values",
|
|
3012
|
+
{
|
|
3013
|
+
id: text("id").primaryKey(),
|
|
3014
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3015
|
+
configId: text("config_id").notNull().references(() => configs.id, { onDelete: "cascade" }),
|
|
3016
|
+
env: text("env", { enum: CONFIG_ENVS }).notNull(),
|
|
3017
|
+
valueJson: text("value_json", { mode: "json" }).$type().notNull(),
|
|
3018
|
+
version: integer("version").notNull(),
|
|
3019
|
+
publishedAt: text("published_at").notNull(),
|
|
3020
|
+
publishedBy: text("published_by").notNull()
|
|
3021
|
+
},
|
|
3022
|
+
(t) => ({
|
|
3023
|
+
projectIdx: index("config_values_project").on(t.projectId),
|
|
3024
|
+
configEnvIdx: index("config_values_config_env").on(t.configId, t.env, t.version),
|
|
3025
|
+
uniq: uniqueIndex("config_values_config_env_version").on(t.configId, t.env, t.version)
|
|
3026
|
+
})
|
|
3027
|
+
);
|
|
3028
|
+
var configDrafts = sqliteTable(
|
|
3029
|
+
"config_drafts",
|
|
3030
|
+
{
|
|
3031
|
+
id: text("id").primaryKey(),
|
|
3032
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3033
|
+
configId: text("config_id").notNull().references(() => configs.id, { onDelete: "cascade" }),
|
|
3034
|
+
env: text("env", { enum: CONFIG_ENVS }).notNull(),
|
|
3035
|
+
valueJson: text("value_json", { mode: "json" }).$type().notNull(),
|
|
3036
|
+
baseVersion: integer("base_version").notNull(),
|
|
3037
|
+
authorEmail: text("author_email").notNull(),
|
|
3038
|
+
createdAt: text("created_at").notNull(),
|
|
3039
|
+
updatedAt: text("updated_at").notNull()
|
|
3040
|
+
},
|
|
3041
|
+
(t) => ({
|
|
3042
|
+
projectIdx: index("config_drafts_project").on(t.projectId),
|
|
3043
|
+
uniq: uniqueIndex("config_drafts_config_env").on(t.configId, t.env)
|
|
3044
|
+
})
|
|
3045
|
+
);
|
|
3046
|
+
var universes = sqliteTable(
|
|
3047
|
+
"universes",
|
|
3048
|
+
{
|
|
3049
|
+
id: text("id").primaryKey(),
|
|
3050
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3051
|
+
name: text("name").notNull(),
|
|
3052
|
+
unitType: text("unit_type").notNull().default("user_id"),
|
|
3053
|
+
holdoutRange: text("holdout_range", { mode: "json" }).$type(),
|
|
3054
|
+
createdAt: text("created_at").notNull(),
|
|
3055
|
+
deletedAt: text("deleted_at")
|
|
3056
|
+
},
|
|
3057
|
+
(t) => ({
|
|
3058
|
+
projectIdx: index("universes_project").on(t.projectId),
|
|
3059
|
+
projectUniq: uniqueIndex("universes_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3060
|
+
})
|
|
3061
|
+
);
|
|
3062
|
+
var experiments = sqliteTable(
|
|
3063
|
+
"experiments",
|
|
3064
|
+
{
|
|
3065
|
+
id: text("id").primaryKey(),
|
|
3066
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3067
|
+
name: text("name").notNull(),
|
|
3068
|
+
description: text("description"),
|
|
3069
|
+
tag: text("tag"),
|
|
3070
|
+
universe: text("universe").notNull(),
|
|
3071
|
+
targetingGate: text("targeting_gate"),
|
|
3072
|
+
allocationPct: integer("allocation_pct").notNull().default(0),
|
|
3073
|
+
salt: text("salt").notNull(),
|
|
3074
|
+
params: text("params", { mode: "json" }).$type().notNull(),
|
|
3075
|
+
groups: text("groups", { mode: "json" }).$type().notNull(),
|
|
3076
|
+
status: text("status", { enum: ["draft", "running", "stopped", "archived"] }).notNull().default("draft"),
|
|
3077
|
+
startedAt: text("started_at"),
|
|
3078
|
+
stoppedAt: text("stopped_at"),
|
|
3079
|
+
significanceThreshold: real("significance_threshold").notNull().default(0.05),
|
|
3080
|
+
minRuntimeDays: integer("min_runtime_days").notNull().default(0),
|
|
3081
|
+
minSampleSize: integer("min_sample_size").notNull().default(100),
|
|
3082
|
+
cupedFrozenAt: text("cuped_frozen_at"),
|
|
3083
|
+
sequentialTesting: integer("sequential_testing", { mode: "boolean" }).notNull().default(false),
|
|
3084
|
+
hashVersion: integer("hash_version").notNull().default(1),
|
|
3085
|
+
updatedAt: text("updated_at").notNull()
|
|
3086
|
+
},
|
|
3087
|
+
(t) => ({
|
|
3088
|
+
projectIdx: index("experiments_project").on(t.projectId),
|
|
3089
|
+
projectUniq: uniqueIndex("experiments_project_name").on(t.projectId, t.name)
|
|
3090
|
+
})
|
|
3091
|
+
);
|
|
3092
|
+
var events = sqliteTable(
|
|
3093
|
+
"events",
|
|
3094
|
+
{
|
|
3095
|
+
id: text("id").primaryKey(),
|
|
3096
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3097
|
+
name: text("name").notNull(),
|
|
3098
|
+
description: text("description"),
|
|
3099
|
+
properties: text("properties", { mode: "json" }).$type().notNull().default([]),
|
|
3100
|
+
pending: integer("pending").notNull().default(0),
|
|
3101
|
+
createdAt: text("created_at").notNull(),
|
|
3102
|
+
deletedAt: text("deleted_at")
|
|
3103
|
+
},
|
|
3104
|
+
(t) => ({
|
|
3105
|
+
projectIdx: index("events_project").on(t.projectId),
|
|
3106
|
+
projectUniq: uniqueIndex("events_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3107
|
+
})
|
|
3108
|
+
);
|
|
3109
|
+
var metrics = sqliteTable(
|
|
3110
|
+
"metrics",
|
|
3111
|
+
{
|
|
3112
|
+
id: text("id").primaryKey(),
|
|
3113
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3114
|
+
name: text("name").notNull(),
|
|
3115
|
+
eventName: text("event_name").notNull(),
|
|
3116
|
+
valuePath: text("value_path"),
|
|
3117
|
+
aggregation: text("aggregation", {
|
|
3118
|
+
enum: ["count_users", "count_events", "sum", "avg", "retention_Nd"]
|
|
3119
|
+
}).notNull().default("count_users"),
|
|
3120
|
+
winsorizePct: integer("winsorize_pct").notNull().default(99),
|
|
3121
|
+
minDetectableEffect: real("min_detectable_effect"),
|
|
3122
|
+
updatedAt: text("updated_at").notNull(),
|
|
3123
|
+
deletedAt: text("deleted_at")
|
|
3124
|
+
},
|
|
3125
|
+
(t) => ({
|
|
3126
|
+
uniq: uniqueIndex("metrics_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3127
|
+
})
|
|
3128
|
+
);
|
|
3129
|
+
var experimentMetrics = sqliteTable(
|
|
3130
|
+
"experiment_metrics",
|
|
3131
|
+
{
|
|
3132
|
+
id: text("id").primaryKey(),
|
|
3133
|
+
experimentId: text("experiment_id").notNull().references(() => experiments.id, { onDelete: "cascade" }),
|
|
3134
|
+
metricId: text("metric_id").notNull().references(() => metrics.id, { onDelete: "cascade" }),
|
|
3135
|
+
role: text("role", { enum: ["goal", "guardrail", "secondary"] }).notNull(),
|
|
3136
|
+
createdAt: integer("created_at").notNull()
|
|
3137
|
+
},
|
|
3138
|
+
(t) => ({ uniq: uniqueIndex("exp_metrics_uniq").on(t.experimentId, t.metricId) })
|
|
3139
|
+
);
|
|
3140
|
+
var experimentResults = sqliteTable(
|
|
3141
|
+
"experiment_results",
|
|
3142
|
+
{
|
|
3143
|
+
projectId: text("project_id").notNull(),
|
|
3144
|
+
experiment: text("experiment").notNull(),
|
|
3145
|
+
metric: text("metric").notNull(),
|
|
3146
|
+
groupName: text("group_name").notNull(),
|
|
3147
|
+
ds: text("ds").notNull(),
|
|
3148
|
+
n: integer("n"),
|
|
3149
|
+
mean: real("mean"),
|
|
3150
|
+
variance: real("variance"),
|
|
3151
|
+
delta: real("delta"),
|
|
3152
|
+
deltaPct: real("delta_pct"),
|
|
3153
|
+
ci95Low: real("ci_95_low"),
|
|
3154
|
+
ci95High: real("ci_95_high"),
|
|
3155
|
+
ci99Low: real("ci_99_low"),
|
|
3156
|
+
ci99High: real("ci_99_high"),
|
|
3157
|
+
pValue: real("p_value"),
|
|
3158
|
+
expectedN: integer("expected_n"),
|
|
3159
|
+
srmPValue: real("srm_p_value"),
|
|
3160
|
+
srmDetected: integer("srm_detected").notNull().default(0),
|
|
3161
|
+
msprtLambda: real("msprt_lambda"),
|
|
3162
|
+
msprtSignificant: integer("msprt_significant"),
|
|
3163
|
+
isFinal: integer("is_final").notNull().default(0),
|
|
3164
|
+
peekWarning: integer("peek_warning").notNull().default(0)
|
|
3165
|
+
},
|
|
3166
|
+
(t) => ({
|
|
3167
|
+
pk: primaryKey({ columns: [t.projectId, t.experiment, t.metric, t.groupName, t.ds] }),
|
|
3168
|
+
dsIdx: index("experiment_results_ds").on(t.projectId, t.ds)
|
|
3169
|
+
})
|
|
3170
|
+
);
|
|
3171
|
+
var userMetricBaseline = sqliteTable(
|
|
3172
|
+
"user_metric_baseline",
|
|
3173
|
+
{
|
|
3174
|
+
projectId: text("project_id").notNull(),
|
|
3175
|
+
userId: text("user_id").notNull(),
|
|
3176
|
+
metricName: text("metric_name").notNull(),
|
|
3177
|
+
avgValue: real("avg_value").notNull(),
|
|
3178
|
+
updatedDs: text("updated_ds").notNull()
|
|
3179
|
+
},
|
|
3180
|
+
(t) => ({ pk: primaryKey({ columns: [t.projectId, t.userId, t.metricName] }) })
|
|
3181
|
+
);
|
|
3182
|
+
var userAliases = sqliteTable(
|
|
3183
|
+
"user_aliases",
|
|
3184
|
+
{
|
|
3185
|
+
projectId: text("project_id").notNull(),
|
|
3186
|
+
anonymousId: text("anonymous_id").notNull(),
|
|
3187
|
+
userId: text("user_id").notNull(),
|
|
3188
|
+
createdAt: text("created_at").notNull()
|
|
3189
|
+
},
|
|
3190
|
+
(t) => ({
|
|
3191
|
+
pk: primaryKey({ columns: [t.projectId, t.anonymousId] }),
|
|
3192
|
+
userIdx: index("user_aliases_user").on(t.projectId, t.userId)
|
|
3193
|
+
})
|
|
3194
|
+
);
|
|
3195
|
+
var userAttributes = sqliteTable(
|
|
3196
|
+
"user_attributes",
|
|
3197
|
+
{
|
|
3198
|
+
id: text("id").primaryKey(),
|
|
3199
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3200
|
+
name: text("name").notNull(),
|
|
3201
|
+
type: text("type", { enum: ["string", "number", "boolean", "enum", "date"] }).notNull(),
|
|
3202
|
+
enumValues: text("enum_values", { mode: "json" }).$type(),
|
|
3203
|
+
required: integer("required").notNull().default(0),
|
|
3204
|
+
description: text("description"),
|
|
3205
|
+
sdkPath: text("sdk_path"),
|
|
3206
|
+
createdAt: text("created_at").notNull(),
|
|
3207
|
+
deletedAt: text("deleted_at")
|
|
3208
|
+
},
|
|
3209
|
+
(t) => ({
|
|
3210
|
+
projectIdx: index("user_attributes_project").on(t.projectId),
|
|
3211
|
+
projectUniq: uniqueIndex("user_attributes_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3212
|
+
})
|
|
3213
|
+
);
|
|
3214
|
+
var labelProfiles = sqliteTable(
|
|
3215
|
+
"label_profiles",
|
|
3216
|
+
{
|
|
3217
|
+
id: text("id").primaryKey(),
|
|
3218
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3219
|
+
name: text("name").notNull(),
|
|
3220
|
+
defaultChunkId: text("default_chunk_id"),
|
|
3221
|
+
createdAt: text("created_at").notNull(),
|
|
3222
|
+
deletedAt: text("deleted_at")
|
|
3223
|
+
},
|
|
3224
|
+
(t) => ({
|
|
3225
|
+
projectIdx: index("label_profiles_project").on(t.projectId),
|
|
3226
|
+
projectNameUniq: uniqueIndex("label_profiles_project_name").on(t.projectId, t.name).where(sql`deleted_at is null`)
|
|
3227
|
+
})
|
|
3228
|
+
);
|
|
3229
|
+
var labelChunks = sqliteTable(
|
|
3230
|
+
"label_chunks",
|
|
3231
|
+
{
|
|
3232
|
+
id: text("id").primaryKey(),
|
|
3233
|
+
projectId: text("project_id").notNull(),
|
|
3234
|
+
profileId: text("profile_id").notNull().references(() => labelProfiles.id, { onDelete: "cascade" }),
|
|
3235
|
+
name: text("name").notNull(),
|
|
3236
|
+
isIndex: integer("is_index").notNull().default(0),
|
|
3237
|
+
publishedUrl: text("published_url"),
|
|
3238
|
+
publishedHash: text("published_hash"),
|
|
3239
|
+
publishedAt: text("published_at"),
|
|
3240
|
+
etag: text("etag")
|
|
3241
|
+
},
|
|
3242
|
+
(t) => ({
|
|
3243
|
+
projectIdx: index("label_chunks_project").on(t.projectId),
|
|
3244
|
+
profileNameUniq: uniqueIndex("label_chunks_profile_name").on(t.profileId, t.name)
|
|
3245
|
+
})
|
|
3246
|
+
);
|
|
3247
|
+
var labelKeys = sqliteTable(
|
|
3248
|
+
"label_keys",
|
|
3249
|
+
{
|
|
3250
|
+
id: text("id").primaryKey(),
|
|
3251
|
+
projectId: text("project_id").notNull(),
|
|
3252
|
+
profileId: text("profile_id").notNull().references(() => labelProfiles.id, { onDelete: "cascade" }),
|
|
3253
|
+
chunkId: text("chunk_id").notNull().references(() => labelChunks.id, { onDelete: "cascade" }),
|
|
3254
|
+
key: text("key").notNull(),
|
|
3255
|
+
value: text("value").notNull(),
|
|
3256
|
+
description: text("description"),
|
|
3257
|
+
// JSON array of variable names extracted from the value's {{var}} placeholders.
|
|
3258
|
+
// `null` = no variables detected; `[]` = explicitly none (normalized).
|
|
3259
|
+
variables: text("variables", { mode: "json" }).$type(),
|
|
3260
|
+
updatedAt: text("updated_at").notNull(),
|
|
3261
|
+
updatedBy: text("updated_by").notNull()
|
|
3262
|
+
},
|
|
3263
|
+
(t) => ({
|
|
3264
|
+
projectIdx: index("label_keys_project").on(t.projectId),
|
|
3265
|
+
chunkIdx: index("label_keys_chunk").on(t.chunkId),
|
|
3266
|
+
profileKeyUniq: uniqueIndex("label_keys_profile_key").on(t.profileId, t.key)
|
|
3267
|
+
})
|
|
3268
|
+
);
|
|
3269
|
+
var labelDrafts = sqliteTable(
|
|
3270
|
+
"label_drafts",
|
|
3271
|
+
{
|
|
3272
|
+
id: text("id").primaryKey(),
|
|
3273
|
+
projectId: text("project_id").notNull(),
|
|
3274
|
+
profileId: text("profile_id").notNull().references(() => labelProfiles.id, { onDelete: "cascade" }),
|
|
3275
|
+
name: text("name").notNull(),
|
|
3276
|
+
sourceProfileId: text("source_profile_id").references(() => labelProfiles.id, {
|
|
3277
|
+
onDelete: "set null"
|
|
3278
|
+
}),
|
|
3279
|
+
createdBy: text("created_by").notNull(),
|
|
3280
|
+
status: text("status", { enum: ["open", "merged", "abandoned"] }).notNull().default("open"),
|
|
3281
|
+
createdAt: text("created_at").notNull(),
|
|
3282
|
+
publishedAt: text("published_at")
|
|
3283
|
+
},
|
|
3284
|
+
(t) => ({
|
|
3285
|
+
projectIdx: index("label_drafts_project").on(t.projectId),
|
|
3286
|
+
projectNameUniq: uniqueIndex("label_drafts_project_name").on(t.projectId, t.name)
|
|
3287
|
+
})
|
|
3288
|
+
);
|
|
3289
|
+
var labelDraftKeys = sqliteTable(
|
|
3290
|
+
"label_draft_keys",
|
|
3291
|
+
{
|
|
3292
|
+
id: text("id").primaryKey(),
|
|
3293
|
+
projectId: text("project_id").notNull(),
|
|
3294
|
+
draftId: text("draft_id").notNull().references(() => labelDrafts.id, { onDelete: "cascade" }),
|
|
3295
|
+
key: text("key").notNull(),
|
|
3296
|
+
value: text("value").notNull(),
|
|
3297
|
+
description: text("description"),
|
|
3298
|
+
variables: text("variables", { mode: "json" }).$type(),
|
|
3299
|
+
updatedBy: text("updated_by").notNull(),
|
|
3300
|
+
updatedAt: text("updated_at").notNull()
|
|
3301
|
+
},
|
|
3302
|
+
(t) => ({
|
|
3303
|
+
draftKeyUniq: uniqueIndex("label_draft_keys_draft_key").on(t.draftId, t.key),
|
|
3304
|
+
projectIdx: index("label_draft_keys_project").on(t.projectId)
|
|
3305
|
+
})
|
|
3306
|
+
);
|
|
3307
|
+
var BUG_STATUSES = ["open", "triaged", "in_progress", "resolved", "wont_fix"];
|
|
3308
|
+
var BUG_PRIORITIES = ["low", "medium", "high", "critical"];
|
|
3309
|
+
var bugReports = sqliteTable(
|
|
3310
|
+
"bug_reports",
|
|
3311
|
+
{
|
|
3312
|
+
id: text("id").primaryKey(),
|
|
3313
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3314
|
+
title: text("title").notNull(),
|
|
3315
|
+
stepsToReproduce: text("steps_to_reproduce").notNull().default(""),
|
|
3316
|
+
actualResult: text("actual_result").notNull().default(""),
|
|
3317
|
+
expectedResult: text("expected_result").notNull().default(""),
|
|
3318
|
+
status: text("status", { enum: BUG_STATUSES }).notNull().default("open"),
|
|
3319
|
+
priority: text("priority", { enum: BUG_PRIORITIES }),
|
|
3320
|
+
reporterEmail: text("reporter_email"),
|
|
3321
|
+
pageUrl: text("page_url"),
|
|
3322
|
+
userAgent: text("user_agent"),
|
|
3323
|
+
viewport: text("viewport"),
|
|
3324
|
+
context: text("context", { mode: "json" }).$type(),
|
|
3325
|
+
createdAt: text("created_at").notNull(),
|
|
3326
|
+
updatedAt: text("updated_at").notNull()
|
|
3327
|
+
},
|
|
3328
|
+
(t) => ({
|
|
3329
|
+
projectIdx: index("bug_reports_project").on(t.projectId, t.status),
|
|
3330
|
+
createdIdx: index("bug_reports_created").on(t.projectId, t.createdAt)
|
|
3331
|
+
})
|
|
3332
|
+
);
|
|
3333
|
+
var FEATURE_REQUEST_STATUSES = [
|
|
3334
|
+
"open",
|
|
3335
|
+
"considering",
|
|
3336
|
+
"planned",
|
|
3337
|
+
"shipped",
|
|
3338
|
+
"declined"
|
|
3339
|
+
];
|
|
3340
|
+
var FEATURE_REQUEST_IMPORTANCES = ["nice_to_have", "important", "critical"];
|
|
3341
|
+
var featureRequests = sqliteTable(
|
|
3342
|
+
"feature_requests",
|
|
3343
|
+
{
|
|
3344
|
+
id: text("id").primaryKey(),
|
|
3345
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3346
|
+
title: text("title").notNull(),
|
|
3347
|
+
description: text("description").notNull().default(""),
|
|
3348
|
+
useCase: text("use_case").notNull().default(""),
|
|
3349
|
+
importance: text("importance", { enum: FEATURE_REQUEST_IMPORTANCES }).notNull().default("nice_to_have"),
|
|
3350
|
+
status: text("status", { enum: FEATURE_REQUEST_STATUSES }).notNull().default("open"),
|
|
3351
|
+
reporterEmail: text("reporter_email"),
|
|
3352
|
+
pageUrl: text("page_url"),
|
|
3353
|
+
userAgent: text("user_agent"),
|
|
3354
|
+
context: text("context", { mode: "json" }).$type(),
|
|
3355
|
+
createdAt: text("created_at").notNull(),
|
|
3356
|
+
updatedAt: text("updated_at").notNull()
|
|
3357
|
+
},
|
|
3358
|
+
(t) => ({
|
|
3359
|
+
projectIdx: index("feature_requests_project").on(t.projectId, t.status),
|
|
3360
|
+
createdIdx: index("feature_requests_created").on(t.projectId, t.createdAt)
|
|
3361
|
+
})
|
|
3362
|
+
);
|
|
3363
|
+
var REPORT_KINDS = ["bug", "feature_request"];
|
|
3364
|
+
var ATTACHMENT_KINDS = ["screenshot", "recording", "file"];
|
|
3365
|
+
var reportAttachments = sqliteTable(
|
|
3366
|
+
"report_attachments",
|
|
3367
|
+
{
|
|
3368
|
+
id: text("id").primaryKey(),
|
|
3369
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3370
|
+
reportKind: text("report_kind", { enum: REPORT_KINDS }).notNull(),
|
|
3371
|
+
reportId: text("report_id").notNull(),
|
|
3372
|
+
kind: text("kind", { enum: ATTACHMENT_KINDS }).notNull(),
|
|
3373
|
+
filename: text("filename").notNull(),
|
|
3374
|
+
mimeType: text("mime_type").notNull(),
|
|
3375
|
+
sizeBytes: integer("size_bytes").notNull(),
|
|
3376
|
+
r2Key: text("r2_key").notNull(),
|
|
3377
|
+
createdAt: text("created_at").notNull()
|
|
3378
|
+
},
|
|
3379
|
+
(t) => ({
|
|
3380
|
+
reportIdx: index("report_attachments_report").on(t.projectId, t.reportKind, t.reportId)
|
|
3381
|
+
})
|
|
3382
|
+
);
|
|
3383
|
+
var CONNECTOR_PROVIDERS = ["google_sheets"];
|
|
3384
|
+
var connectors = sqliteTable(
|
|
3385
|
+
"connectors",
|
|
3386
|
+
{
|
|
3387
|
+
id: text("id").primaryKey(),
|
|
3388
|
+
projectId: text("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
|
3389
|
+
provider: text("provider", { enum: CONNECTOR_PROVIDERS }).notNull(),
|
|
3390
|
+
name: text("name").notNull(),
|
|
3391
|
+
enabled: integer("enabled", { mode: "boolean" }).notNull().default(true),
|
|
3392
|
+
events: text("events", { mode: "json" }).$type().notNull(),
|
|
3393
|
+
config: text("config", { mode: "json" }).$type().notNull(),
|
|
3394
|
+
// AES-GCM encrypted JSON: { refresh_token, access_token, expires_at, scope }
|
|
3395
|
+
credentialsCipher: text("credentials_cipher"),
|
|
3396
|
+
// Display label (e.g., the OAuth account email)
|
|
3397
|
+
accountLabel: text("account_label"),
|
|
3398
|
+
createdAt: text("created_at").notNull(),
|
|
3399
|
+
updatedAt: text("updated_at").notNull()
|
|
3400
|
+
},
|
|
3401
|
+
(t) => ({
|
|
3402
|
+
projectIdx: index("connectors_project").on(t.projectId, t.provider)
|
|
3403
|
+
})
|
|
3404
|
+
);
|
|
3405
|
+
var i18nUsageDaily = sqliteTable(
|
|
3406
|
+
"i18n_usage_daily",
|
|
3407
|
+
{
|
|
3408
|
+
projectId: text("project_id").notNull(),
|
|
3409
|
+
sdkKeyId: text("sdk_key_id").notNull(),
|
|
3410
|
+
date: text("date").notNull(),
|
|
3411
|
+
requestCount: integer("request_count").notNull().default(0)
|
|
3412
|
+
},
|
|
3413
|
+
(t) => ({
|
|
3414
|
+
pk: primaryKey({ columns: [t.projectId, t.sdkKeyId, t.date] }),
|
|
3415
|
+
projectDateIdx: index("i18n_usage_daily_project_date").on(t.projectId, t.date)
|
|
3416
|
+
})
|
|
3417
|
+
);
|
|
3418
|
+
var billingEvents = sqliteTable(
|
|
3419
|
+
"billing_events",
|
|
3420
|
+
{
|
|
3421
|
+
// Stripe event ID — INSERT OR IGNORE makes webhook handling idempotent
|
|
3422
|
+
id: text("id").primaryKey(),
|
|
3423
|
+
projectId: text("project_id").references(() => projects.id, { onDelete: "set null" }),
|
|
3424
|
+
type: text("type").notNull(),
|
|
3425
|
+
payload: text("payload", { mode: "json" }).$type().notNull(),
|
|
3426
|
+
receivedAt: text("received_at").notNull()
|
|
3427
|
+
},
|
|
3428
|
+
(t) => ({
|
|
3429
|
+
projectIdx: index("billing_events_project").on(t.projectId)
|
|
3430
|
+
})
|
|
3431
|
+
);
|
|
3432
|
+
|
|
3433
|
+
// ../core/src/schemas/configs.ts
|
|
3434
|
+
var SEGMENT = /^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/;
|
|
3435
|
+
var RESERVED_FOLDER = "_default";
|
|
3436
|
+
var configNameSchema = z5.string().max(128).refine(
|
|
3437
|
+
(s) => {
|
|
3438
|
+
const parts = s.split(".");
|
|
3439
|
+
if (parts.length !== 2) return false;
|
|
3440
|
+
return parts.every((p, i) => p === RESERVED_FOLDER && i === 0 ? true : SEGMENT.test(p));
|
|
3441
|
+
},
|
|
3442
|
+
{
|
|
3443
|
+
message: "Must be `folder.name` \u2014 exactly two lowercase segments separated by a dot (e.g. `pricing.tiers`)."
|
|
3444
|
+
}
|
|
3445
|
+
).describe(
|
|
3446
|
+
"Stable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create."
|
|
3447
|
+
);
|
|
3448
|
+
var configKindSchema = z5.enum(CONFIG_KINDS);
|
|
3449
|
+
var configEnvSchema = z5.enum(CONFIG_ENVS).describe("Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).");
|
|
3450
|
+
var jsonSchemaSchema = z5.record(z5.string(), z5.unknown()).refine((s) => s.type === "object", { message: "Top-level schema type must be 'object'" }).describe(
|
|
3451
|
+
"JSON Schema (draft 2020-12) describing the shape of the config value. Top-level `type` must be `'object'`; every published value is validated against this schema."
|
|
3452
|
+
);
|
|
3453
|
+
var initialValuesSchema = z5.union([z5.unknown(), z5.record(configEnvSchema, z5.unknown())]).describe(
|
|
3454
|
+
"Initial config value. Either a single JSON object applied to every env, or a `{ env: value }` map seeding per-env values. Must match `schema`. Defaults to `{}` on every env when omitted."
|
|
3455
|
+
);
|
|
3456
|
+
var configCreateSchema = z5.object({
|
|
3457
|
+
name: configNameSchema,
|
|
3458
|
+
description: z5.string().max(512).optional().describe("Optional free-form description shown in the dashboard. Max 512 chars."),
|
|
3459
|
+
schema: jsonSchemaSchema,
|
|
3460
|
+
value: initialValuesSchema.optional()
|
|
3461
|
+
}).describe("Body for `POST /api/admin/configs`. `name` + `schema` required.");
|
|
3462
|
+
var configUpdateSchema = z5.object({
|
|
3463
|
+
schema: jsonSchemaSchema.optional().describe(
|
|
3464
|
+
"Replacement schema. When supplied, the new schema is validated against every published value before it lands."
|
|
3465
|
+
),
|
|
3466
|
+
value: z5.unknown().optional().describe(
|
|
3467
|
+
"Flat value applied to **every** env. Publishes a new version per env. To target one env, use `PUT /{id}/drafts` then `POST /{id}/publish`."
|
|
3468
|
+
)
|
|
3469
|
+
}).describe(
|
|
3470
|
+
"Body for `PATCH /api/admin/configs/{id}`. Partial \u2014 only supplied fields change. `value` republishes on every env."
|
|
3471
|
+
);
|
|
3472
|
+
var configSchemaUpdateSchema = z5.object({ schema: jsonSchemaSchema }).describe("Schema-only update. Does not bump value versions or rebuild KV.");
|
|
3473
|
+
var configDraftUpsertSchema = z5.object({
|
|
3474
|
+
env: configEnvSchema,
|
|
3475
|
+
value: z5.unknown().describe("Draft value to stage on `env`. Validated against the config's current schema.")
|
|
3476
|
+
}).describe(
|
|
3477
|
+
"Body for `PUT /api/admin/configs/{id}/drafts`. Stages a value for a single env without publishing."
|
|
3478
|
+
);
|
|
3479
|
+
var configPublishSchema = z5.object({ env: configEnvSchema }).describe(
|
|
3480
|
+
"Body for `POST /api/admin/configs/{id}/publish` and `DELETE /api/admin/configs/{id}/drafts`. Names the target env."
|
|
3481
|
+
);
|
|
3482
|
+
var configEnvPublishedSchema = z5.object({
|
|
3483
|
+
version: z5.number().int().describe("Monotonically increasing version per env."),
|
|
3484
|
+
publishedAt: z5.string().describe("ISO-8601 publish timestamp."),
|
|
3485
|
+
publishedBy: z5.string().describe("Email/id of the actor who published this version.")
|
|
3486
|
+
});
|
|
3487
|
+
var configDraftSummarySchema = z5.object({
|
|
3488
|
+
updatedAt: z5.string().describe("ISO-8601 last-edit timestamp on the draft."),
|
|
3489
|
+
authorEmail: z5.string().describe("Email/id of the actor who last edited the draft."),
|
|
3490
|
+
baseVersion: z5.number().int().describe("Published version the draft was forked from.")
|
|
3491
|
+
});
|
|
3492
|
+
var configResponseSchema = z5.object({
|
|
3493
|
+
id: z5.string().describe("Stable opaque config id (`cfg_\u2026`)."),
|
|
3494
|
+
name: configNameSchema,
|
|
3495
|
+
description: z5.string().nullable(),
|
|
3496
|
+
schema: jsonSchemaSchema,
|
|
3497
|
+
updatedAt: z5.string().describe("ISO-8601 timestamp of last mutation."),
|
|
3498
|
+
envs: z5.record(configEnvSchema, configEnvPublishedSchema).describe("Per-env latest published version metadata."),
|
|
3499
|
+
drafts: z5.record(configEnvSchema, configDraftSummarySchema).describe("Per-env active drafts (if any)."),
|
|
3500
|
+
values: z5.record(configEnvSchema, z5.unknown()).optional().describe("Per-env latest published values (only returned by `GET /{id}`, not list)."),
|
|
3501
|
+
draftValues: z5.record(configEnvSchema, z5.unknown()).optional().describe("Per-env draft values (only returned by `GET /{id}`).")
|
|
3502
|
+
});
|
|
3503
|
+
var configCreateResponseSchema = z5.object({
|
|
3504
|
+
id: z5.string().describe("Newly assigned config id."),
|
|
3505
|
+
name: configNameSchema
|
|
3506
|
+
});
|
|
3507
|
+
var configUpdateResponseSchema = z5.object({
|
|
3508
|
+
id: z5.string().describe("Config id that was updated.")
|
|
3509
|
+
});
|
|
3510
|
+
var configDeleteResponseSchema = z5.object({ ok: z5.literal(true) });
|
|
3511
|
+
var configDraftSaveResponseSchema = z5.object({
|
|
3512
|
+
id: z5.string(),
|
|
3513
|
+
env: configEnvSchema,
|
|
3514
|
+
baseVersion: z5.number().int().describe("Published version the draft is based on."),
|
|
3515
|
+
updatedAt: z5.string()
|
|
3516
|
+
});
|
|
3517
|
+
var configDraftDiscardResponseSchema = z5.object({ ok: z5.literal(true) });
|
|
3518
|
+
var configPublishResponseSchema = z5.object({
|
|
3519
|
+
id: z5.string(),
|
|
3520
|
+
env: configEnvSchema,
|
|
3521
|
+
version: z5.number().int().describe("Newly published version on `env`.")
|
|
3522
|
+
});
|
|
3523
|
+
var configActivityEntrySchema = z5.object({
|
|
3524
|
+
id: z5.string(),
|
|
3525
|
+
action: z5.string().describe("Audit action (e.g. `config.create`, `config.publish`)."),
|
|
3526
|
+
actorEmail: z5.string(),
|
|
3527
|
+
actorType: z5.enum(["user", "cli", "system"]),
|
|
3528
|
+
payload: z5.unknown().nullable(),
|
|
3529
|
+
createdAt: z5.string().describe("ISO-8601 timestamp.")
|
|
3530
|
+
});
|
|
3531
|
+
var configActivityResponseSchema = z5.array(configActivityEntrySchema).describe("Recent audit rows for one config, newest first.");
|
|
3532
|
+
|
|
3533
|
+
// src/resources/configs.ts
|
|
3534
|
+
var configListResponseSchema = z6.object({
|
|
3535
|
+
data: z6.array(configResponseSchema),
|
|
3536
|
+
next_cursor: z6.string().nullable()
|
|
3537
|
+
});
|
|
3538
|
+
var BASE3 = "/api/admin/configs";
|
|
3539
|
+
function configsClient(t) {
|
|
3540
|
+
async function list(opts = {}) {
|
|
3541
|
+
const query = {};
|
|
3542
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
3543
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
3544
|
+
return t.request("GET", BASE3, void 0, query);
|
|
3545
|
+
}
|
|
3546
|
+
async function listAll() {
|
|
3547
|
+
const out = [];
|
|
3548
|
+
let cursor;
|
|
3549
|
+
do {
|
|
3550
|
+
const page = await list({ limit: 500, cursor });
|
|
3551
|
+
out.push(...page.data);
|
|
3552
|
+
cursor = page.next_cursor ?? void 0;
|
|
3553
|
+
} while (cursor);
|
|
3554
|
+
return out;
|
|
3555
|
+
}
|
|
3556
|
+
async function resolve(idOrName) {
|
|
3557
|
+
try {
|
|
3558
|
+
return await t.request("GET", `${BASE3}/${idOrName}`);
|
|
3559
|
+
} catch (err) {
|
|
3560
|
+
if (!(err instanceof ApiError) || err.status !== 404) throw err;
|
|
3561
|
+
}
|
|
3562
|
+
const all = await listAll();
|
|
3563
|
+
const found = all.find((c) => c.name === idOrName);
|
|
3564
|
+
if (!found) throw new ApiError(`Config '${idOrName}' not found`, 404);
|
|
3565
|
+
return found;
|
|
3566
|
+
}
|
|
3567
|
+
return {
|
|
3568
|
+
list,
|
|
3569
|
+
listAll,
|
|
3570
|
+
resolve,
|
|
3571
|
+
get: (id) => t.request("GET", `${BASE3}/${id}`),
|
|
3572
|
+
create: (input) => t.request("POST", BASE3, configCreateSchema.parse(input)),
|
|
3573
|
+
update: (id, input) => t.request("PATCH", `${BASE3}/${id}`, configUpdateSchema.parse(input)),
|
|
3574
|
+
delete: (id) => t.request("DELETE", `${BASE3}/${id}`),
|
|
3575
|
+
saveDraft: (id, input) => t.request("PUT", `${BASE3}/${id}/drafts`, configDraftUpsertSchema.parse(input)),
|
|
3576
|
+
discardDraft: (id, input) => t.request("DELETE", `${BASE3}/${id}/drafts`, configPublishSchema.parse(input)),
|
|
3577
|
+
publish: (id, input) => t.request("POST", `${BASE3}/${id}/publish`, configPublishSchema.parse(input)),
|
|
3578
|
+
activity: (id, limit) => t.request(
|
|
3579
|
+
"GET",
|
|
3580
|
+
`${BASE3}/${id}/activity`,
|
|
3581
|
+
void 0,
|
|
3582
|
+
limit ? { limit: String(limit) } : void 0
|
|
3583
|
+
)
|
|
3584
|
+
};
|
|
3585
|
+
}
|
|
3586
|
+
var SAMPLE_CFG = {
|
|
3587
|
+
id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
|
|
3588
|
+
name: "pricing.tiers",
|
|
3589
|
+
description: "Pricing tier definitions consumed by the checkout flow.",
|
|
3590
|
+
schema: {
|
|
3591
|
+
type: "object",
|
|
3592
|
+
properties: {
|
|
3593
|
+
tiers: { type: "array", items: { type: "object" } }
|
|
3594
|
+
},
|
|
3595
|
+
required: ["tiers"]
|
|
3596
|
+
},
|
|
3597
|
+
updatedAt: "2026-05-09T18:22:11.000Z",
|
|
3598
|
+
envs: {
|
|
3599
|
+
dev: { version: 5, publishedAt: "2026-05-09T18:22:11.000Z", publishedBy: "ana@example.com" },
|
|
3600
|
+
stage: { version: 4, publishedAt: "2026-05-08T11:05:22.000Z", publishedBy: "ana@example.com" },
|
|
3601
|
+
prod: { version: 4, publishedAt: "2026-05-08T11:05:22.000Z", publishedBy: "ana@example.com" }
|
|
3602
|
+
},
|
|
3603
|
+
drafts: {
|
|
3604
|
+
dev: {
|
|
3605
|
+
updatedAt: "2026-05-10T09:31:00.000Z",
|
|
3606
|
+
authorEmail: "bo@example.com",
|
|
3607
|
+
baseVersion: 5
|
|
3608
|
+
}
|
|
3609
|
+
}
|
|
3610
|
+
};
|
|
3611
|
+
var configsResource = {
|
|
3612
|
+
name: "configs",
|
|
3613
|
+
basePath: BASE3,
|
|
3614
|
+
describeOne: "config",
|
|
3615
|
+
describeMany: "configs",
|
|
3616
|
+
tag: {
|
|
3617
|
+
name: "Configs",
|
|
3618
|
+
description: [
|
|
3619
|
+
"Dynamic configs: JSON-Schema-validated structured values delivered to SDKs and editable per environment with a draft/publish workflow.",
|
|
3620
|
+
"",
|
|
3621
|
+
"**Identity.** Each config is keyed by `name` in `folder.name` form (e.g. `pricing.tiers`). Immutable after create.",
|
|
3622
|
+
"",
|
|
3623
|
+
"**Schema-first.** Every config carries a JSON Schema (draft 2020-12, top-level `type: 'object'`). Every published value is validated against it.",
|
|
3624
|
+
"",
|
|
3625
|
+
"**Drafts \u2192 publish.** Per-env edits go through `PUT /{id}/drafts` (stages a value) then `POST /{id}/publish` (promotes to a new version). The flat `PATCH /{id}` republishes on **every** env in one shot \u2014 bypassing drafts.",
|
|
3626
|
+
"",
|
|
3627
|
+
"**Versioning.** Each publish bumps the per-env `version` monotonically. SDKs deliver the latest published version for each env."
|
|
3628
|
+
].join("\n")
|
|
3629
|
+
},
|
|
3630
|
+
schemas: {
|
|
3631
|
+
create: configCreateSchema,
|
|
3632
|
+
update: configUpdateSchema,
|
|
3633
|
+
saveDraft: configDraftUpsertSchema,
|
|
3634
|
+
publish: configPublishSchema
|
|
3635
|
+
},
|
|
3636
|
+
actions: [
|
|
3637
|
+
{ name: "publish", description: "Publish a config draft for an env" },
|
|
3638
|
+
{ name: "saveDraft", description: "Save a draft value for an env" },
|
|
3639
|
+
{ name: "discardDraft", description: "Discard a draft for an env" }
|
|
3640
|
+
],
|
|
3641
|
+
endpoints: [
|
|
3642
|
+
{
|
|
3643
|
+
operationId: "listConfigs",
|
|
3644
|
+
method: "GET",
|
|
3645
|
+
path: "",
|
|
3646
|
+
summary: "List dynamic configs",
|
|
3647
|
+
description: "Returns a single page of configs ordered by `updated_at desc, id desc`. Each row includes the latest published `version` per env and any active drafts.",
|
|
3648
|
+
response: configListResponseSchema,
|
|
3649
|
+
examples: { response: { data: [SAMPLE_CFG], next_cursor: null } },
|
|
3650
|
+
useCase: "Snapshot every config in the project \u2014 e.g. CI check that asserts no env is stuck on a stale default or that every config has a published value on prod."
|
|
3651
|
+
},
|
|
3652
|
+
{
|
|
3653
|
+
operationId: "getConfig",
|
|
3654
|
+
method: "GET",
|
|
3655
|
+
path: "/{id}",
|
|
3656
|
+
summary: "Get one config",
|
|
3657
|
+
description: "Returns config metadata plus the latest published values per env and any active draft values. Use this to fetch the JSON the editor renders.",
|
|
3658
|
+
pathParams: { id: "Stable opaque config id (`cfg_\u2026`) or the config's `name`." },
|
|
3659
|
+
response: configResponseSchema,
|
|
3660
|
+
examples: {
|
|
3661
|
+
response: {
|
|
3662
|
+
...SAMPLE_CFG,
|
|
3663
|
+
values: {
|
|
3664
|
+
dev: { tiers: [{ name: "free" }, { name: "pro" }] },
|
|
3665
|
+
stage: { tiers: [{ name: "free" }, { name: "pro" }] },
|
|
3666
|
+
prod: { tiers: [{ name: "free" }, { name: "pro" }] }
|
|
3667
|
+
},
|
|
3668
|
+
draftValues: {
|
|
3669
|
+
dev: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] }
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
},
|
|
3673
|
+
useCase: "Fetch one config's current published values and any in-flight drafts."
|
|
3674
|
+
},
|
|
3675
|
+
{
|
|
3676
|
+
operationId: "createConfig",
|
|
3677
|
+
method: "POST",
|
|
3678
|
+
path: "",
|
|
3679
|
+
summary: "Create a dynamic config",
|
|
3680
|
+
description: [
|
|
3681
|
+
"Creates a new config with the given `schema`. The initial `value` (or an empty object) is published as version 1 on **every** env.",
|
|
3682
|
+
"",
|
|
3683
|
+
"Returns `409` if `name` already exists in the project, `400` if `value` doesn't validate against `schema`."
|
|
3684
|
+
].join("\n"),
|
|
3685
|
+
successStatus: 201,
|
|
3686
|
+
request: configCreateSchema,
|
|
3687
|
+
response: configCreateResponseSchema,
|
|
3688
|
+
examples: {
|
|
3689
|
+
requestExamples: {
|
|
3690
|
+
minimal: {
|
|
3691
|
+
summary: "Minimal \u2014 name + schema",
|
|
3692
|
+
description: "Smallest valid body. Initial value defaults to `{}` on every env.",
|
|
3693
|
+
value: {
|
|
3694
|
+
name: "pricing.tiers",
|
|
3695
|
+
schema: {
|
|
3696
|
+
type: "object",
|
|
3697
|
+
properties: { tiers: { type: "array" } },
|
|
3698
|
+
required: ["tiers"]
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
},
|
|
3702
|
+
seededValue: {
|
|
3703
|
+
summary: "Seeded \u2014 schema + initial value",
|
|
3704
|
+
description: "Seed the same JSON object on every env at version 1.",
|
|
3705
|
+
value: {
|
|
3706
|
+
name: "pricing.tiers",
|
|
3707
|
+
description: "Pricing tier definitions consumed by checkout.",
|
|
3708
|
+
schema: {
|
|
3709
|
+
type: "object",
|
|
3710
|
+
properties: { tiers: { type: "array" } },
|
|
3711
|
+
required: ["tiers"]
|
|
3712
|
+
},
|
|
3713
|
+
value: { tiers: [{ name: "free" }, { name: "pro" }] }
|
|
3714
|
+
}
|
|
3715
|
+
},
|
|
3716
|
+
perEnvSeed: {
|
|
3717
|
+
summary: "Per-env seed",
|
|
3718
|
+
description: "Pass a `{ env: value }` map to seed different initial values per env.",
|
|
3719
|
+
value: {
|
|
3720
|
+
name: "pricing.tiers",
|
|
3721
|
+
schema: {
|
|
3722
|
+
type: "object",
|
|
3723
|
+
properties: { tiers: { type: "array" } },
|
|
3724
|
+
required: ["tiers"]
|
|
3725
|
+
},
|
|
3726
|
+
value: {
|
|
3727
|
+
dev: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] },
|
|
3728
|
+
stage: { tiers: [{ name: "free" }, { name: "pro" }] },
|
|
3729
|
+
prod: { tiers: [{ name: "free" }, { name: "pro" }] }
|
|
3730
|
+
}
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
},
|
|
3734
|
+
response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4", name: "pricing.tiers" }
|
|
3735
|
+
},
|
|
3736
|
+
useCase: [
|
|
3737
|
+
"- **Minimal create** \u2014 `name` + `schema`. Initial value defaults to `{}`.",
|
|
3738
|
+
"- **Seeded create** \u2014 supply a flat `value` to publish the same object on every env.",
|
|
3739
|
+
"- **Per-env seed** \u2014 supply a `{ env: value }` map for different per-env starting values."
|
|
3740
|
+
].join("\n")
|
|
3741
|
+
},
|
|
3742
|
+
{
|
|
3743
|
+
operationId: "updateConfig",
|
|
3744
|
+
method: "PATCH",
|
|
3745
|
+
path: "/{id}",
|
|
3746
|
+
summary: "Update a dynamic config",
|
|
3747
|
+
description: [
|
|
3748
|
+
"Partial update. When `value` is supplied it is **republished on every env** (new version per env). When `schema` is supplied it replaces the current schema; every existing value is re-validated.",
|
|
3749
|
+
"",
|
|
3750
|
+
"For env-scoped edits, use the draft/publish flow (`PUT /{id}/drafts` then `POST /{id}/publish`) instead."
|
|
3751
|
+
].join("\n"),
|
|
3752
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3753
|
+
request: configUpdateSchema,
|
|
3754
|
+
response: configUpdateResponseSchema,
|
|
3755
|
+
examples: {
|
|
3756
|
+
requestExamples: {
|
|
3757
|
+
flatValue: {
|
|
3758
|
+
summary: "Flat value \u2014 republish on every env",
|
|
3759
|
+
description: "Sets the same JSON object as a new version on dev/stage/prod.",
|
|
3760
|
+
value: { value: { tiers: [{ name: "free" }, { name: "pro" }] } }
|
|
3761
|
+
},
|
|
3762
|
+
schemaOnly: {
|
|
3763
|
+
summary: "Schema-only update",
|
|
3764
|
+
description: "Replaces the schema. Existing values must still validate or the update fails.",
|
|
3765
|
+
value: {
|
|
3766
|
+
schema: {
|
|
3767
|
+
type: "object",
|
|
3768
|
+
properties: {
|
|
3769
|
+
tiers: { type: "array" },
|
|
3770
|
+
trial_days: { type: "integer" }
|
|
3771
|
+
},
|
|
3772
|
+
required: ["tiers"]
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
3776
|
+
},
|
|
3777
|
+
response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4" }
|
|
3778
|
+
},
|
|
3779
|
+
useCase: [
|
|
3780
|
+
'- **Republish flat value** \u2014 `{ "value": {\u2026} }` sets the same value on every env.',
|
|
3781
|
+
'- **Schema migration** \u2014 `{ "schema": {\u2026} }` replaces the schema; existing values are re-validated.',
|
|
3782
|
+
"- **Env-scoped edits** \u2014 use `PUT /{id}/drafts` + `POST /{id}/publish` instead of PATCH."
|
|
3783
|
+
].join("\n")
|
|
3784
|
+
},
|
|
3785
|
+
{
|
|
3786
|
+
operationId: "deleteConfig",
|
|
3787
|
+
method: "DELETE",
|
|
3788
|
+
path: "/{id}",
|
|
3789
|
+
summary: "Delete a dynamic config",
|
|
3790
|
+
description: "Soft-deletes the config and rebuilds the project's flags KV blob.",
|
|
3791
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3792
|
+
response: configDeleteResponseSchema,
|
|
3793
|
+
examples: { response: { ok: true } },
|
|
3794
|
+
useCase: "Tear down a config after its consumers have stopped reading it."
|
|
3795
|
+
},
|
|
3796
|
+
{
|
|
3797
|
+
operationId: "saveConfigDraft",
|
|
3798
|
+
method: "PUT",
|
|
3799
|
+
path: "/{id}/drafts",
|
|
3800
|
+
summary: "Save a draft value",
|
|
3801
|
+
description: [
|
|
3802
|
+
"Stages a value for one env without publishing. The draft is validated against the config's current schema and stored alongside the `baseVersion` it was forked from.",
|
|
3803
|
+
"",
|
|
3804
|
+
"Saving over an existing draft overwrites it. Use `POST /{id}/publish` to promote it to a new published version."
|
|
3805
|
+
].join("\n"),
|
|
3806
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3807
|
+
request: configDraftUpsertSchema,
|
|
3808
|
+
response: configDraftSaveResponseSchema,
|
|
3809
|
+
examples: {
|
|
3810
|
+
requestExamples: {
|
|
3811
|
+
stageDev: {
|
|
3812
|
+
summary: "Stage a dev draft",
|
|
3813
|
+
description: "Stages a new value on dev without touching stage/prod.",
|
|
3814
|
+
value: {
|
|
3815
|
+
env: "dev",
|
|
3816
|
+
value: { tiers: [{ name: "free" }, { name: "pro" }, { name: "enterprise" }] }
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
},
|
|
3820
|
+
response: {
|
|
3821
|
+
id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
|
|
3822
|
+
env: "dev",
|
|
3823
|
+
baseVersion: 5,
|
|
3824
|
+
updatedAt: "2026-05-10T09:31:00.000Z"
|
|
3825
|
+
}
|
|
3826
|
+
},
|
|
3827
|
+
useCase: "Iterate on a config value on dev without affecting prod \u2014 preview in staging, then publish."
|
|
3828
|
+
},
|
|
3829
|
+
{
|
|
3830
|
+
operationId: "discardConfigDraft",
|
|
3831
|
+
method: "DELETE",
|
|
3832
|
+
path: "/{id}/drafts",
|
|
3833
|
+
summary: "Discard a draft",
|
|
3834
|
+
description: "Drops the in-flight draft on one env. Published values are unaffected.",
|
|
3835
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3836
|
+
request: configPublishSchema,
|
|
3837
|
+
response: configDraftDiscardResponseSchema,
|
|
3838
|
+
examples: {
|
|
3839
|
+
requestExamples: {
|
|
3840
|
+
discardDev: {
|
|
3841
|
+
summary: "Discard the dev draft",
|
|
3842
|
+
description: "Drops the staged dev draft without publishing.",
|
|
3843
|
+
value: { env: "dev" }
|
|
3844
|
+
}
|
|
3845
|
+
},
|
|
3846
|
+
response: { ok: true }
|
|
3847
|
+
},
|
|
3848
|
+
useCase: "Abandon an in-progress draft after deciding not to ship it."
|
|
3849
|
+
},
|
|
3850
|
+
{
|
|
3851
|
+
operationId: "publishConfigDraft",
|
|
3852
|
+
method: "POST",
|
|
3853
|
+
path: "/{id}/publish",
|
|
3854
|
+
summary: "Publish a draft",
|
|
3855
|
+
description: [
|
|
3856
|
+
"Promotes the staged draft on one env to a new published version. The draft must still validate against the current schema.",
|
|
3857
|
+
"",
|
|
3858
|
+
"Returns `404` if there is no draft for the given env."
|
|
3859
|
+
].join("\n"),
|
|
3860
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3861
|
+
request: configPublishSchema,
|
|
3862
|
+
response: configPublishResponseSchema,
|
|
3863
|
+
examples: {
|
|
3864
|
+
requestExamples: {
|
|
3865
|
+
publishDev: {
|
|
3866
|
+
summary: "Publish the dev draft",
|
|
3867
|
+
description: "Promotes the dev draft to a new published version on dev.",
|
|
3868
|
+
value: { env: "dev" }
|
|
3869
|
+
},
|
|
3870
|
+
publishProd: {
|
|
3871
|
+
summary: "Publish to prod",
|
|
3872
|
+
description: "Promotes the prod draft after staging signed off.",
|
|
3873
|
+
value: { env: "prod" }
|
|
3874
|
+
}
|
|
3875
|
+
},
|
|
3876
|
+
response: { id: "cfg_01j7wae5h6j7k8l9m0n1p2q3r4", env: "dev", version: 6 }
|
|
3877
|
+
},
|
|
3878
|
+
useCase: "Ship a staged change once you've validated it on a lower env."
|
|
3879
|
+
},
|
|
3880
|
+
{
|
|
3881
|
+
operationId: "listConfigActivity",
|
|
3882
|
+
method: "GET",
|
|
3883
|
+
path: "/{id}/activity",
|
|
3884
|
+
summary: "List config activity",
|
|
3885
|
+
description: "Returns recent audit rows for one config (create, update, draft.save, publish, delete) ordered newest first. Use the `limit` query parameter to cap the result (1\u2013100, default 20).",
|
|
3886
|
+
pathParams: { id: "Stable opaque config id." },
|
|
3887
|
+
queryParams: {
|
|
3888
|
+
limit: {
|
|
3889
|
+
schema: z6.coerce.number().int().min(1).max(100).default(20),
|
|
3890
|
+
description: "Max rows to return (1\u2013100). Defaults to 20."
|
|
3891
|
+
}
|
|
3892
|
+
},
|
|
3893
|
+
response: configActivityResponseSchema,
|
|
3894
|
+
examples: {
|
|
3895
|
+
response: [
|
|
3896
|
+
{
|
|
3897
|
+
id: "act_01j7waf01a2b3c4d5e6f7g8h9i",
|
|
3898
|
+
action: "config.publish",
|
|
3899
|
+
actorEmail: "ana@example.com",
|
|
3900
|
+
actorType: "user",
|
|
3901
|
+
payload: { env: "dev", version: 6 },
|
|
3902
|
+
createdAt: "2026-05-10T09:31:42.000Z"
|
|
3903
|
+
}
|
|
3904
|
+
]
|
|
3905
|
+
},
|
|
3906
|
+
useCase: "Render the activity panel in the config editor or drive a slack notification on publish events."
|
|
3907
|
+
}
|
|
3908
|
+
]
|
|
3909
|
+
};
|
|
3910
|
+
|
|
3911
|
+
// src/resources/universes.ts
|
|
3912
|
+
import { z as z8 } from "zod";
|
|
3913
|
+
|
|
3914
|
+
// ../core/src/schemas/universes.ts
|
|
3915
|
+
import { z as z7 } from "zod";
|
|
3916
|
+
var universeNameSchema = z7.string().regex(/^[a-z0-9][a-z0-9_-]{0,63}$/).describe(
|
|
3917
|
+
"Stable universe key referenced by experiments via `universe: '<name>'`. Lowercase letters, digits, `_` or `-`, max 64 chars. Immutable after create."
|
|
3918
|
+
);
|
|
3919
|
+
var holdoutRangeSchema = z7.tuple([z7.number().int().min(0).max(9999), z7.number().int().min(0).max(9999)]).refine(([lo, hi]) => lo <= hi, "lo must be <= hi").nullable().describe(
|
|
3920
|
+
"Inclusive `[lo, hi]` bucket range (0\u20139999) reserved as the **holdout** \u2014 callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required."
|
|
3921
|
+
);
|
|
3922
|
+
var universeCreateSchema = z7.object({
|
|
3923
|
+
name: universeNameSchema,
|
|
3924
|
+
unit_type: z7.string().default("user_id").describe(
|
|
3925
|
+
"Unit of randomisation. Typically `user_id`. Use `account_id` to keep whole accounts in the same group across an experiment."
|
|
3926
|
+
),
|
|
3927
|
+
holdout_range: holdoutRangeSchema.default(null)
|
|
3928
|
+
}).describe("Body for `POST /api/admin/universes`. Only `name` is required.");
|
|
3929
|
+
var universeUpdateSchema = z7.object({
|
|
3930
|
+
holdout_range: holdoutRangeSchema.optional()
|
|
3931
|
+
}).describe(
|
|
3932
|
+
"Body for `PATCH /api/admin/universes/{id}`. Only `holdout_range` is mutable \u2014 name and unit_type are immutable after create."
|
|
3933
|
+
);
|
|
3934
|
+
var universeResponseSchema = z7.object({
|
|
3935
|
+
id: z7.string().describe("Stable opaque universe id."),
|
|
3936
|
+
name: universeNameSchema,
|
|
3937
|
+
unitType: z7.string().describe("Unit of randomisation. Snake-case in request (`unit_type`), camelCase in response."),
|
|
3938
|
+
holdoutRange: z7.tuple([z7.number(), z7.number()]).nullable().describe("Reserved holdout bucket range, or `null` if none."),
|
|
3939
|
+
createdAt: z7.string().describe("ISO-8601 timestamp of creation.")
|
|
3940
|
+
});
|
|
3941
|
+
var universeCreateResponseSchema = z7.object({
|
|
3942
|
+
id: z7.string().describe("Newly assigned universe id."),
|
|
3943
|
+
name: universeNameSchema
|
|
3944
|
+
});
|
|
3945
|
+
var universeUpdateResponseSchema = z7.object({
|
|
3946
|
+
id: z7.string().describe("Universe id that was updated.")
|
|
3947
|
+
});
|
|
3948
|
+
var universeDeleteResponseSchema = z7.object({ ok: z7.literal(true) });
|
|
3949
|
+
|
|
3950
|
+
// src/resources/universes.ts
|
|
3951
|
+
var universeListResponseSchema = z8.object({
|
|
3952
|
+
data: z8.array(universeResponseSchema),
|
|
3953
|
+
next_cursor: z8.string().nullable()
|
|
3954
|
+
});
|
|
3955
|
+
var BASE4 = "/api/admin/universes";
|
|
3956
|
+
function universesClient(t) {
|
|
3957
|
+
async function list(opts = {}) {
|
|
3958
|
+
const query = {};
|
|
3959
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
3960
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
3961
|
+
return t.request("GET", BASE4, void 0, query);
|
|
3962
|
+
}
|
|
3963
|
+
async function listAll() {
|
|
3964
|
+
const out = [];
|
|
3965
|
+
let cursor;
|
|
3966
|
+
do {
|
|
3967
|
+
const page = await list({ limit: 500, cursor });
|
|
3968
|
+
out.push(...page.data);
|
|
3969
|
+
cursor = page.next_cursor ?? void 0;
|
|
3970
|
+
} while (cursor);
|
|
3971
|
+
return out;
|
|
3972
|
+
}
|
|
3973
|
+
async function resolve(idOrName) {
|
|
3974
|
+
const all = await listAll();
|
|
3975
|
+
const found = all.find((u) => u.id === idOrName) ?? all.find((u) => u.name === idOrName);
|
|
3976
|
+
if (!found) throw new ApiError(`Universe '${idOrName}' not found`, 404);
|
|
3977
|
+
return found;
|
|
3978
|
+
}
|
|
3979
|
+
return {
|
|
3980
|
+
list,
|
|
3981
|
+
listAll,
|
|
3982
|
+
resolve,
|
|
3983
|
+
create: (input) => t.request("POST", BASE4, universeCreateSchema.parse(input)),
|
|
3984
|
+
update: (id, input) => t.request("PATCH", `${BASE4}/${id}`, universeUpdateSchema.parse(input)),
|
|
3985
|
+
delete: (id) => t.request("DELETE", `${BASE4}/${id}`)
|
|
3986
|
+
};
|
|
3987
|
+
}
|
|
3988
|
+
var universesResource = {
|
|
3989
|
+
name: "universes",
|
|
3990
|
+
basePath: BASE4,
|
|
3991
|
+
describeOne: "universe",
|
|
3992
|
+
describeMany: "universes",
|
|
3993
|
+
tag: {
|
|
3994
|
+
name: "Universes",
|
|
3995
|
+
description: [
|
|
3996
|
+
"Universes: the shared bucketing space all experiments draw from.",
|
|
3997
|
+
"",
|
|
3998
|
+
"**Identity.** Each universe is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Experiments reference it via `universe: '<name>'`. The name is immutable.",
|
|
3999
|
+
"",
|
|
4000
|
+
"**Unit of randomisation.** `unit_type` selects the attribute hashed into a 0\u20139999 bucket \u2014 default `user_id` for per-user randomisation, `account_id` to keep a whole account in the same group.",
|
|
4001
|
+
"",
|
|
4002
|
+
"**Holdout.** `holdout_range` is an inclusive `[lo, hi]` bucket range (0\u20139999) reserved for measurement \u2014 callers hashed into the holdout are excluded from every experiment in the universe. Pro plan or higher.",
|
|
4003
|
+
"",
|
|
4004
|
+
"**Deletion.** Blocked while any non-archived experiment references the universe \u2014 archive those first."
|
|
4005
|
+
].join("\n")
|
|
4006
|
+
},
|
|
4007
|
+
schemas: {
|
|
4008
|
+
create: universeCreateSchema,
|
|
4009
|
+
update: universeUpdateSchema
|
|
4010
|
+
},
|
|
4011
|
+
actions: [],
|
|
4012
|
+
endpoints: [
|
|
4013
|
+
{
|
|
4014
|
+
operationId: "listUniverses",
|
|
4015
|
+
method: "GET",
|
|
4016
|
+
path: "",
|
|
4017
|
+
summary: "List universes",
|
|
4018
|
+
description: "Returns a single page of universes ordered by `created_at desc, id desc`. The universes table has no `updated_at`, so this list is keyed on creation time.",
|
|
4019
|
+
response: universeListResponseSchema,
|
|
4020
|
+
examples: {
|
|
4021
|
+
response: {
|
|
4022
|
+
data: [
|
|
4023
|
+
{
|
|
4024
|
+
id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0",
|
|
4025
|
+
name: "primary_users",
|
|
4026
|
+
unitType: "user_id",
|
|
4027
|
+
holdoutRange: [9500, 9999],
|
|
4028
|
+
createdAt: "2026-04-12T10:14:08.000Z"
|
|
4029
|
+
}
|
|
4030
|
+
],
|
|
4031
|
+
next_cursor: null
|
|
4032
|
+
}
|
|
4033
|
+
},
|
|
4034
|
+
useCase: "Snapshot every universe in the project \u2014 for example to audit which `unit_type` and `holdout_range` are in use before launching a new experiment."
|
|
4035
|
+
},
|
|
4036
|
+
{
|
|
4037
|
+
operationId: "createUniverse",
|
|
4038
|
+
method: "POST",
|
|
4039
|
+
path: "",
|
|
4040
|
+
summary: "Create a universe",
|
|
4041
|
+
description: [
|
|
4042
|
+
"Creates a new universe. Only `name` is required \u2014 `unit_type` defaults to `user_id` and `holdout_range` defaults to `null` (no holdout).",
|
|
4043
|
+
"",
|
|
4044
|
+
"Returns `409` if `name` already exists in the project. Returns `403` if you supply `holdout_range` on a plan below Pro."
|
|
4045
|
+
].join("\n"),
|
|
4046
|
+
successStatus: 201,
|
|
4047
|
+
request: universeCreateSchema,
|
|
4048
|
+
response: universeCreateResponseSchema,
|
|
4049
|
+
examples: {
|
|
4050
|
+
requestExamples: {
|
|
4051
|
+
minimal: {
|
|
4052
|
+
summary: "Minimal \u2014 name only",
|
|
4053
|
+
description: "Smallest valid body. Creates a `user_id`-keyed universe with no holdout.",
|
|
4054
|
+
value: { name: "primary_users" }
|
|
4055
|
+
},
|
|
4056
|
+
holdout: {
|
|
4057
|
+
summary: "Holdout \u2014 reserve 5% for measurement",
|
|
4058
|
+
description: "Reserves buckets 9500\u20139999 (5% of callers) as the holdout. Pro plan or higher.",
|
|
4059
|
+
value: { name: "primary_users", holdout_range: [9500, 9999] }
|
|
4060
|
+
},
|
|
4061
|
+
accountLevel: {
|
|
4062
|
+
summary: "Account-level randomisation",
|
|
4063
|
+
description: "Bucket whole accounts together via `unit_type: 'account_id'`.",
|
|
4064
|
+
value: { name: "saas_accounts", unit_type: "account_id" }
|
|
4065
|
+
}
|
|
4066
|
+
},
|
|
4067
|
+
response: { id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0", name: "primary_users" }
|
|
4068
|
+
},
|
|
4069
|
+
useCase: [
|
|
4070
|
+
'- **Default universe** \u2014 `{ "name": "primary_users" }`. Per-user randomisation, no holdout.',
|
|
4071
|
+
"- **Reserved holdout** \u2014 supply `holdout_range` to carve out a measurement slice excluded from all experiments.",
|
|
4072
|
+
"- **Account-level** \u2014 `unit_type: 'account_id'` so multi-seat accounts see one consistent variant."
|
|
4073
|
+
].join("\n")
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
operationId: "updateUniverse",
|
|
4077
|
+
method: "PATCH",
|
|
4078
|
+
path: "/{id}",
|
|
4079
|
+
summary: "Update a universe",
|
|
4080
|
+
description: [
|
|
4081
|
+
"Partial update. Only `holdout_range` is mutable \u2014 `name` and `unit_type` are immutable after create.",
|
|
4082
|
+
"",
|
|
4083
|
+
'Pass `"holdout_range": null` to remove an existing holdout.'
|
|
4084
|
+
].join("\n"),
|
|
4085
|
+
pathParams: { id: "Stable opaque universe id (`uni_\u2026`)." },
|
|
4086
|
+
request: universeUpdateSchema,
|
|
4087
|
+
response: universeUpdateResponseSchema,
|
|
4088
|
+
examples: {
|
|
4089
|
+
requestExamples: {
|
|
4090
|
+
setHoldout: {
|
|
4091
|
+
summary: "Set holdout \u2014 reserve 5%",
|
|
4092
|
+
description: "Reserve buckets 9500\u20139999 as the holdout. Pro plan or higher.",
|
|
4093
|
+
value: { holdout_range: [9500, 9999] }
|
|
4094
|
+
},
|
|
4095
|
+
removeHoldout: {
|
|
4096
|
+
summary: "Remove holdout",
|
|
4097
|
+
description: "Disable the holdout entirely.",
|
|
4098
|
+
value: { holdout_range: null }
|
|
4099
|
+
}
|
|
4100
|
+
},
|
|
4101
|
+
response: { id: "uni_01j7w8a1b2c3d4e5f6g7h8i9j0" }
|
|
4102
|
+
},
|
|
4103
|
+
useCase: [
|
|
4104
|
+
"- **Adjust holdout** \u2014 change the reserved measurement slice without recreating experiments.",
|
|
4105
|
+
'- **Remove holdout** \u2014 `{ "holdout_range": null }`.'
|
|
4106
|
+
].join("\n")
|
|
4107
|
+
},
|
|
4108
|
+
{
|
|
4109
|
+
operationId: "deleteUniverse",
|
|
4110
|
+
method: "DELETE",
|
|
4111
|
+
path: "/{id}",
|
|
4112
|
+
summary: "Delete a universe",
|
|
4113
|
+
description: "Soft-deletes the universe. Returns `409` if any non-archived experiment still references it \u2014 archive those experiments first.",
|
|
4114
|
+
pathParams: { id: "Stable opaque universe id." },
|
|
4115
|
+
response: universeDeleteResponseSchema,
|
|
4116
|
+
examples: { response: { ok: true } },
|
|
4117
|
+
useCase: "Tear down a universe after every experiment that used it has been archived."
|
|
4118
|
+
}
|
|
4119
|
+
]
|
|
4120
|
+
};
|
|
4121
|
+
|
|
4122
|
+
// src/resources/killswitches.ts
|
|
4123
|
+
import { z as z10 } from "zod";
|
|
4124
|
+
|
|
4125
|
+
// ../core/src/schemas/killswitches.ts
|
|
4126
|
+
import { z as z9 } from "zod";
|
|
4127
|
+
var switchKeySchema = z9.string().max(64).regex(/^[a-z0-9](?:[a-z0-9_-]*[a-z0-9])?$/, {
|
|
4128
|
+
message: "Switch keys must be lowercase, no dots."
|
|
4129
|
+
}).describe(
|
|
4130
|
+
"Single-segment switch key (lowercase letters, digits, `_`/`-`; no dots). Used as the nested switch entry inside a killswitch's `switches` map."
|
|
4131
|
+
);
|
|
4132
|
+
var killswitchValueSchema = z9.object({
|
|
4133
|
+
value: z9.boolean().describe(
|
|
4134
|
+
"Default boolean delivered when no `switches` entry matches the caller's switch key."
|
|
4135
|
+
),
|
|
4136
|
+
switches: z9.record(switchKeySchema, z9.boolean()).optional().describe(
|
|
4137
|
+
"Per-switch overrides. When present, a matching key takes precedence over `value` on the client."
|
|
4138
|
+
)
|
|
4139
|
+
}).describe("Runtime killswitch payload returned to SDKs.");
|
|
4140
|
+
var killswitchCreateSchema = z9.object({
|
|
4141
|
+
name: configNameSchema.describe(
|
|
4142
|
+
"Stable killswitch key in `folder.name` form (two lowercase segments separated by a dot \u2014 e.g. `payments.checkout`). Immutable after create."
|
|
4143
|
+
),
|
|
4144
|
+
description: z9.string().max(512).optional().describe("Optional free-form description shown in the dashboard. Max 512 chars."),
|
|
4145
|
+
value: z9.boolean().optional().describe(
|
|
4146
|
+
"Default value applied to every env at creation. Defaults to `false`. Use `true` to ship the killswitch pre-tripped."
|
|
4147
|
+
),
|
|
4148
|
+
switches: z9.record(switchKeySchema, z9.boolean()).optional().describe(
|
|
4149
|
+
"Initial per-switch overrides applied to every env. Empty/omitted leaves the killswitch with only the flat `value`."
|
|
4150
|
+
)
|
|
4151
|
+
}).describe("Body for `POST /api/admin/killswitches`. Only `name` is required.");
|
|
4152
|
+
var killswitchUpdateSchema = z9.object({
|
|
4153
|
+
description: z9.string().max(512).nullish().describe("New description, or `null` to clear it. Max 512 chars."),
|
|
4154
|
+
value: z9.boolean().optional().describe(
|
|
4155
|
+
"Flat value applied to every env. Publishes a new version per env when set. Omit to leave values unchanged."
|
|
4156
|
+
),
|
|
4157
|
+
switches: z9.record(switchKeySchema, z9.boolean()).optional().describe(
|
|
4158
|
+
"Replace the switches map wholesale on every env. To edit a single entry on a single env use `PUT /{id}/switch` instead."
|
|
4159
|
+
)
|
|
4160
|
+
}).describe(
|
|
4161
|
+
"Body for `PATCH /api/admin/killswitches/{id}`. Partial \u2014 only supplied fields change. `switches` replaces wholesale (no merge)."
|
|
4162
|
+
);
|
|
4163
|
+
var killswitchSwitchSetSchema = z9.object({
|
|
4164
|
+
env: configEnvSchema.describe("Target environment (`dev`/`stage`/`prod`)."),
|
|
4165
|
+
switchKey: switchKeySchema.describe("Switch key to set."),
|
|
4166
|
+
value: z9.boolean().describe("New boolean value for this `switchKey` on this `env`.")
|
|
4167
|
+
}).describe(
|
|
4168
|
+
"Body for `PUT /api/admin/killswitches/{id}/switch`. Sets or updates one switch entry on one env."
|
|
4169
|
+
);
|
|
4170
|
+
var killswitchSwitchUnsetSchema = z9.object({
|
|
4171
|
+
env: configEnvSchema.describe("Target environment."),
|
|
4172
|
+
switchKey: switchKeySchema.describe("Switch key to remove.")
|
|
4173
|
+
}).describe(
|
|
4174
|
+
"Body for `DELETE /api/admin/killswitches/{id}/switch`. Removes one switch entry from one env."
|
|
4175
|
+
);
|
|
4176
|
+
var killswitchEnvEntrySchema = z9.object({
|
|
4177
|
+
value: z9.boolean().describe("Flat boolean published for this env."),
|
|
4178
|
+
switches: z9.record(switchKeySchema, z9.boolean()).optional(),
|
|
4179
|
+
version: z9.number().int().describe("Monotonically increasing version per env."),
|
|
4180
|
+
publishedAt: z9.string().describe("ISO-8601 publish timestamp for this version.")
|
|
4181
|
+
});
|
|
4182
|
+
var killswitchResponseSchema = z9.object({
|
|
4183
|
+
id: z9.string().describe("Stable opaque killswitch id."),
|
|
4184
|
+
name: configNameSchema,
|
|
4185
|
+
description: z9.string().nullable().describe("Free-form description or `null`."),
|
|
4186
|
+
updatedAt: z9.string().describe("ISO-8601 timestamp of last mutation."),
|
|
4187
|
+
envs: z9.record(configEnvSchema, killswitchEnvEntrySchema).describe("Per-env latest value, switches, version, and publish timestamp.")
|
|
4188
|
+
});
|
|
4189
|
+
var killswitchCreateResponseSchema = z9.object({
|
|
4190
|
+
id: z9.string().describe("Newly assigned killswitch id."),
|
|
4191
|
+
name: configNameSchema
|
|
4192
|
+
});
|
|
4193
|
+
var killswitchUpdateResponseSchema = z9.object({
|
|
4194
|
+
id: z9.string().describe("Killswitch id that was updated.")
|
|
4195
|
+
});
|
|
4196
|
+
var killswitchDeleteResponseSchema = z9.object({ ok: z9.literal(true) });
|
|
4197
|
+
var killswitchSwitchSetResponseSchema = z9.object({
|
|
4198
|
+
id: z9.string(),
|
|
4199
|
+
env: configEnvSchema,
|
|
4200
|
+
switchKey: switchKeySchema,
|
|
4201
|
+
value: z9.boolean()
|
|
4202
|
+
});
|
|
4203
|
+
var killswitchSwitchUnsetResponseSchema = z9.object({
|
|
4204
|
+
id: z9.string(),
|
|
4205
|
+
env: configEnvSchema,
|
|
4206
|
+
switchKey: switchKeySchema,
|
|
4207
|
+
removed: z9.boolean().describe("`true` if the entry existed and was removed, `false` if no-op.")
|
|
4208
|
+
});
|
|
4209
|
+
|
|
4210
|
+
// src/resources/killswitches.ts
|
|
4211
|
+
var killswitchListResponseSchema = z10.object({
|
|
4212
|
+
data: z10.array(killswitchResponseSchema),
|
|
4213
|
+
next_cursor: z10.string().nullable()
|
|
4214
|
+
});
|
|
4215
|
+
var BASE5 = "/api/admin/killswitches";
|
|
4216
|
+
function killswitchesClient(t) {
|
|
4217
|
+
async function list(opts = {}) {
|
|
4218
|
+
const query = {};
|
|
4219
|
+
if (opts.limit !== void 0) query.limit = String(opts.limit);
|
|
4220
|
+
if (opts.cursor) query.cursor = opts.cursor;
|
|
4221
|
+
return t.request("GET", BASE5, void 0, query);
|
|
4222
|
+
}
|
|
4223
|
+
async function listAll() {
|
|
4224
|
+
const out = [];
|
|
4225
|
+
let cursor;
|
|
4226
|
+
do {
|
|
4227
|
+
const page = await list({ limit: 500, cursor });
|
|
4228
|
+
out.push(...page.data);
|
|
4229
|
+
cursor = page.next_cursor ?? void 0;
|
|
4230
|
+
} while (cursor);
|
|
4231
|
+
return out;
|
|
4232
|
+
}
|
|
4233
|
+
async function resolve(idOrName) {
|
|
4234
|
+
try {
|
|
4235
|
+
return await t.request("GET", `${BASE5}/${idOrName}`);
|
|
4236
|
+
} catch (err) {
|
|
4237
|
+
if (!(err instanceof ApiError) || err.status !== 404) throw err;
|
|
4238
|
+
}
|
|
4239
|
+
const all = await listAll();
|
|
4240
|
+
const found = all.find((k) => k.name === idOrName);
|
|
4241
|
+
if (!found) throw new ApiError(`Killswitch '${idOrName}' not found`, 404);
|
|
4242
|
+
return found;
|
|
4243
|
+
}
|
|
4244
|
+
return {
|
|
4245
|
+
list,
|
|
4246
|
+
listAll,
|
|
4247
|
+
resolve,
|
|
4248
|
+
get: (id) => t.request("GET", `${BASE5}/${id}`),
|
|
4249
|
+
create: (input) => t.request("POST", BASE5, killswitchCreateSchema.parse(input)),
|
|
4250
|
+
update: (id, input) => t.request("PATCH", `${BASE5}/${id}`, killswitchUpdateSchema.parse(input)),
|
|
4251
|
+
delete: (id) => t.request("DELETE", `${BASE5}/${id}`),
|
|
4252
|
+
setSwitch: (id, input) => t.request("PUT", `${BASE5}/${id}/switch`, killswitchSwitchSetSchema.parse(input)),
|
|
4253
|
+
unsetSwitch: (id, input) => t.request("DELETE", `${BASE5}/${id}/switch`, killswitchSwitchUnsetSchema.parse(input))
|
|
4254
|
+
};
|
|
4255
|
+
}
|
|
4256
|
+
var SAMPLE_KS = {
|
|
4257
|
+
id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
|
|
4258
|
+
name: "payments.checkout",
|
|
4259
|
+
description: "Master kill for the checkout flow. Trip to fall back to the legacy provider.",
|
|
4260
|
+
updatedAt: "2026-05-09T18:22:11.000Z",
|
|
4261
|
+
envs: {
|
|
4262
|
+
dev: { value: false, version: 3, publishedAt: "2026-05-09T18:22:11.000Z" },
|
|
4263
|
+
stage: { value: false, version: 3, publishedAt: "2026-05-09T18:22:11.000Z" },
|
|
4264
|
+
prod: {
|
|
4265
|
+
value: false,
|
|
4266
|
+
switches: { eu_region: true },
|
|
4267
|
+
version: 5,
|
|
4268
|
+
publishedAt: "2026-05-09T18:22:11.000Z"
|
|
4269
|
+
}
|
|
4270
|
+
}
|
|
4271
|
+
};
|
|
4272
|
+
var killswitchesResource = {
|
|
4273
|
+
name: "killswitches",
|
|
4274
|
+
basePath: BASE5,
|
|
4275
|
+
describeOne: "killswitch",
|
|
4276
|
+
describeMany: "killswitches",
|
|
4277
|
+
tag: {
|
|
4278
|
+
name: "Killswitches",
|
|
4279
|
+
description: [
|
|
4280
|
+
"Killswitches: per-env boolean overrides for kill-style operational toggles. Optimised for incident response \u2014 no rules, no rollout, just a flat boolean (plus optional per-key overrides) versioned per environment.",
|
|
4281
|
+
"",
|
|
4282
|
+
"**Identity.** Each killswitch is keyed by `name` in `folder.name` form (e.g. `payments.checkout`). Immutable after create.",
|
|
4283
|
+
"",
|
|
4284
|
+
"**Per-env values.** Every killswitch stores one version stream per env (`dev`, `stage`, `prod`). A `PATCH` with `value`/`switches` applies to **every** env in one shot (publishes a new version per env). To touch a single env, use `PUT /{id}/switch` and friends.",
|
|
4285
|
+
"",
|
|
4286
|
+
"**Switches map.** Optional `switches: { key: bool }` overrides the flat `value` for specific named call sites \u2014 useful for region/feature-scoped kills.",
|
|
4287
|
+
"",
|
|
4288
|
+
"**Versioning.** Each publish (create, update, set-switch, unset-switch) bumps the per-env `version` monotonically. SDKs deliver the latest published version."
|
|
4289
|
+
].join("\n")
|
|
4290
|
+
},
|
|
4291
|
+
schemas: {
|
|
4292
|
+
create: killswitchCreateSchema,
|
|
4293
|
+
update: killswitchUpdateSchema,
|
|
4294
|
+
setSwitch: killswitchSwitchSetSchema,
|
|
4295
|
+
unsetSwitch: killswitchSwitchUnsetSchema
|
|
4296
|
+
},
|
|
4297
|
+
actions: [
|
|
4298
|
+
{ name: "setSwitch", description: "Set or update one switch entry on one env" },
|
|
4299
|
+
{ name: "unsetSwitch", description: "Remove one switch entry from one env" }
|
|
4300
|
+
],
|
|
4301
|
+
endpoints: [
|
|
4302
|
+
{
|
|
4303
|
+
operationId: "listKillswitches",
|
|
4304
|
+
method: "GET",
|
|
4305
|
+
path: "",
|
|
4306
|
+
summary: "List killswitches",
|
|
4307
|
+
description: "Returns a single page of killswitches ordered by `updated_at desc, id desc`. Each row includes the latest published `value`/`switches`/`version` per env.",
|
|
4308
|
+
response: killswitchListResponseSchema,
|
|
4309
|
+
examples: { response: { data: [SAMPLE_KS], next_cursor: null } },
|
|
4310
|
+
useCase: "Snapshot every killswitch in the project \u2014 e.g. to render an incident-response runbook listing every kill and its current trip state."
|
|
4311
|
+
},
|
|
4312
|
+
{
|
|
4313
|
+
operationId: "getKillswitch",
|
|
4314
|
+
method: "GET",
|
|
4315
|
+
path: "/{id}",
|
|
4316
|
+
summary: "Get one killswitch",
|
|
4317
|
+
description: "Returns the killswitch metadata plus the latest published `value`/`switches`/`version` per env.",
|
|
4318
|
+
pathParams: { id: "Stable opaque killswitch id (`ksw_\u2026`)." },
|
|
4319
|
+
response: killswitchResponseSchema,
|
|
4320
|
+
examples: { response: SAMPLE_KS },
|
|
4321
|
+
useCase: "Fetch the current state of one killswitch \u2014 e.g. to verify a trip propagated before declaring an incident mitigated."
|
|
4322
|
+
},
|
|
4323
|
+
{
|
|
4324
|
+
operationId: "createKillswitch",
|
|
4325
|
+
method: "POST",
|
|
4326
|
+
path: "",
|
|
4327
|
+
summary: "Create a killswitch",
|
|
4328
|
+
description: [
|
|
4329
|
+
"Creates a new killswitch with `value` (default `false`) applied to **every** env at version 1.",
|
|
4330
|
+
"",
|
|
4331
|
+
"Returns `409` if `name` already exists in the project."
|
|
4332
|
+
].join("\n"),
|
|
4333
|
+
successStatus: 201,
|
|
4334
|
+
request: killswitchCreateSchema,
|
|
4335
|
+
response: killswitchCreateResponseSchema,
|
|
4336
|
+
examples: {
|
|
4337
|
+
requestExamples: {
|
|
4338
|
+
minimal: {
|
|
4339
|
+
summary: "Minimal \u2014 name only, untripped",
|
|
4340
|
+
description: "Smallest valid body. Creates a killswitch with `value: false` on every env.",
|
|
4341
|
+
value: { name: "payments.checkout" }
|
|
4342
|
+
},
|
|
4343
|
+
preTripped: {
|
|
4344
|
+
summary: "Pre-tripped on create",
|
|
4345
|
+
description: "Ship the killswitch already tripped (`value: true`) \u2014 useful when provisioning a kill ahead of a deploy you want to ship with the feature off.",
|
|
4346
|
+
value: {
|
|
4347
|
+
name: "payments.checkout",
|
|
4348
|
+
description: "Master kill for the new checkout flow.",
|
|
4349
|
+
value: true
|
|
4350
|
+
}
|
|
4351
|
+
},
|
|
4352
|
+
withSwitches: {
|
|
4353
|
+
summary: "With per-region switches",
|
|
4354
|
+
description: "Seed a `switches` map so specific named call sites can be tripped independently of the flat `value`.",
|
|
4355
|
+
value: {
|
|
4356
|
+
name: "payments.checkout",
|
|
4357
|
+
value: false,
|
|
4358
|
+
switches: { eu_region: true, apac_region: false }
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
},
|
|
4362
|
+
response: { id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8", name: "payments.checkout" }
|
|
4363
|
+
},
|
|
4364
|
+
useCase: [
|
|
4365
|
+
'- **Untripped create** \u2014 `{ "name": "payments.checkout" }`. Provision the kill ahead of an incident.',
|
|
4366
|
+
'- **Pre-tripped** \u2014 `{ "value": true }` to ship the killswitch already engaged.',
|
|
4367
|
+
"- **With switches** \u2014 seed `switches` to carve out per-region/per-tenant kills from day one."
|
|
4368
|
+
].join("\n")
|
|
4369
|
+
},
|
|
4370
|
+
{
|
|
4371
|
+
operationId: "updateKillswitch",
|
|
4372
|
+
method: "PATCH",
|
|
4373
|
+
path: "/{id}",
|
|
4374
|
+
summary: "Update a killswitch",
|
|
4375
|
+
description: [
|
|
4376
|
+
"Partial update applied to **every** env. Setting `value`/`switches` publishes a new version per env. Description-only patches don't bump versions.",
|
|
4377
|
+
"",
|
|
4378
|
+
"To change a single switch on a single env, use `PUT /{id}/switch` instead."
|
|
4379
|
+
].join("\n"),
|
|
4380
|
+
pathParams: { id: "Stable opaque killswitch id." },
|
|
4381
|
+
request: killswitchUpdateSchema,
|
|
4382
|
+
response: killswitchUpdateResponseSchema,
|
|
4383
|
+
examples: {
|
|
4384
|
+
requestExamples: {
|
|
4385
|
+
trip: {
|
|
4386
|
+
summary: "Trip the killswitch on every env",
|
|
4387
|
+
description: "Sets `value: true` on dev/stage/prod, bumping each env's version by one.",
|
|
4388
|
+
value: { value: true }
|
|
4389
|
+
},
|
|
4390
|
+
untrip: {
|
|
4391
|
+
summary: "Untrip on every env",
|
|
4392
|
+
description: "Sets `value: false` on every env.",
|
|
4393
|
+
value: { value: false }
|
|
4394
|
+
},
|
|
4395
|
+
replaceSwitches: {
|
|
4396
|
+
summary: "Replace switches map",
|
|
4397
|
+
description: "Replaces the per-key overrides wholesale on every env. Pass `{}` to clear.",
|
|
4398
|
+
value: { switches: { eu_region: true, apac_region: true } }
|
|
4399
|
+
},
|
|
4400
|
+
updateDescription: {
|
|
4401
|
+
summary: "Description-only update",
|
|
4402
|
+
description: "Doesn't bump versions or trigger a republish \u2014 only metadata changes.",
|
|
4403
|
+
value: { description: "Master kill \u2014 owner: payments-platform" }
|
|
4404
|
+
}
|
|
4405
|
+
},
|
|
4406
|
+
response: { id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8" }
|
|
4407
|
+
},
|
|
4408
|
+
useCase: [
|
|
4409
|
+
'- **Trip everywhere** \u2014 `{ "value": true }`. Kills the feature across dev/stage/prod in one call.',
|
|
4410
|
+
'- **Untrip everywhere** \u2014 `{ "value": false }`.',
|
|
4411
|
+
"- **Replace switches** \u2014 send the full new map; per-key edits use `PUT /{id}/switch`.",
|
|
4412
|
+
"- **Update description** \u2014 metadata-only patches don't bump versions."
|
|
4413
|
+
].join("\n")
|
|
4414
|
+
},
|
|
4415
|
+
{
|
|
4416
|
+
operationId: "deleteKillswitch",
|
|
4417
|
+
method: "DELETE",
|
|
4418
|
+
path: "/{id}",
|
|
4419
|
+
summary: "Delete a killswitch",
|
|
4420
|
+
description: "Soft-deletes the killswitch and rebuilds the project's flags KV blob so SDKs stop seeing it.",
|
|
4421
|
+
pathParams: { id: "Stable opaque killswitch id." },
|
|
4422
|
+
response: killswitchDeleteResponseSchema,
|
|
4423
|
+
examples: { response: { ok: true } },
|
|
4424
|
+
useCase: "Tear down a killswitch after the feature it protected has been removed."
|
|
4425
|
+
},
|
|
4426
|
+
{
|
|
4427
|
+
operationId: "setKillswitchSwitch",
|
|
4428
|
+
method: "PUT",
|
|
4429
|
+
path: "/{id}/switch",
|
|
4430
|
+
summary: "Set one switch entry",
|
|
4431
|
+
description: [
|
|
4432
|
+
"Sets or updates a single `switchKey` on a single `env`. Publishes one new version on that env only \u2014 other envs untouched.",
|
|
4433
|
+
"",
|
|
4434
|
+
"Use this for surgical per-env, per-key flips during incident response (e.g. trip `eu_region` on prod without touching the flat `value` or other envs)."
|
|
4435
|
+
].join("\n"),
|
|
4436
|
+
pathParams: { id: "Stable opaque killswitch id." },
|
|
4437
|
+
request: killswitchSwitchSetSchema,
|
|
4438
|
+
response: killswitchSwitchSetResponseSchema,
|
|
4439
|
+
examples: {
|
|
4440
|
+
requestExamples: {
|
|
4441
|
+
tripRegion: {
|
|
4442
|
+
summary: "Trip eu_region on prod",
|
|
4443
|
+
description: "Sets `switches.eu_region = true` on prod only. Other envs and the flat `value` are unchanged.",
|
|
4444
|
+
value: { env: "prod", switchKey: "eu_region", value: true }
|
|
4445
|
+
},
|
|
4446
|
+
untripRegion: {
|
|
4447
|
+
summary: "Untrip eu_region on prod",
|
|
4448
|
+
description: "Flip the same entry back off without removing it from the map.",
|
|
4449
|
+
value: { env: "prod", switchKey: "eu_region", value: false }
|
|
4450
|
+
}
|
|
4451
|
+
},
|
|
4452
|
+
response: {
|
|
4453
|
+
id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
|
|
4454
|
+
env: "prod",
|
|
4455
|
+
switchKey: "eu_region",
|
|
4456
|
+
value: true
|
|
4457
|
+
}
|
|
4458
|
+
},
|
|
4459
|
+
useCase: [
|
|
4460
|
+
'- **Trip a region** \u2014 `{ "env": "prod", "switchKey": "eu_region", "value": true }`.',
|
|
4461
|
+
"- **Untrip without removing** \u2014 same payload with `value: false`. To remove the entry entirely use `DELETE /{id}/switch`."
|
|
4462
|
+
].join("\n")
|
|
4463
|
+
},
|
|
4464
|
+
{
|
|
4465
|
+
operationId: "unsetKillswitchSwitch",
|
|
4466
|
+
method: "DELETE",
|
|
4467
|
+
path: "/{id}/switch",
|
|
4468
|
+
summary: "Remove one switch entry",
|
|
4469
|
+
description: [
|
|
4470
|
+
"Removes a single `switchKey` from the `switches` map on a single `env`. Publishes a new version on that env.",
|
|
4471
|
+
"",
|
|
4472
|
+
"Returns `{ removed: false }` if the entry didn't exist (idempotent no-op)."
|
|
4473
|
+
].join("\n"),
|
|
4474
|
+
pathParams: { id: "Stable opaque killswitch id." },
|
|
4475
|
+
request: killswitchSwitchUnsetSchema,
|
|
4476
|
+
response: killswitchSwitchUnsetResponseSchema,
|
|
4477
|
+
examples: {
|
|
4478
|
+
requestExamples: {
|
|
4479
|
+
removeRegion: {
|
|
4480
|
+
summary: "Remove eu_region from prod",
|
|
4481
|
+
description: "Drops the `eu_region` override on prod so the flat `value` applies again.",
|
|
4482
|
+
value: { env: "prod", switchKey: "eu_region" }
|
|
4483
|
+
}
|
|
4484
|
+
},
|
|
4485
|
+
response: {
|
|
4486
|
+
id: "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
|
|
4487
|
+
env: "prod",
|
|
4488
|
+
switchKey: "eu_region",
|
|
4489
|
+
removed: true
|
|
4490
|
+
}
|
|
4491
|
+
},
|
|
4492
|
+
useCase: "Clean up a per-region override after the incident is resolved so the flat `value` governs again."
|
|
4493
|
+
}
|
|
4494
|
+
]
|
|
4495
|
+
};
|
|
4496
|
+
|
|
4497
|
+
// src/resources/index.ts
|
|
4498
|
+
function createAdminClient(transport) {
|
|
4499
|
+
return {
|
|
4500
|
+
transport,
|
|
4501
|
+
gates: gatesClient(transport),
|
|
4502
|
+
experiments: experimentsClient(transport),
|
|
4503
|
+
configs: configsClient(transport),
|
|
4504
|
+
universes: universesClient(transport),
|
|
4505
|
+
killswitches: killswitchesClient(transport)
|
|
4506
|
+
};
|
|
4507
|
+
}
|
|
4508
|
+
var RESOURCE_REGISTRY = [
|
|
4509
|
+
gatesResource,
|
|
4510
|
+
experimentsResource,
|
|
4511
|
+
configsResource,
|
|
4512
|
+
universesResource,
|
|
4513
|
+
killswitchesResource
|
|
4514
|
+
];
|
|
4515
|
+
export {
|
|
4516
|
+
RESOURCE_REGISTRY,
|
|
4517
|
+
configsClient,
|
|
4518
|
+
configsResource,
|
|
4519
|
+
createAdminClient,
|
|
4520
|
+
experimentsClient,
|
|
4521
|
+
experimentsResource,
|
|
4522
|
+
gatesClient,
|
|
4523
|
+
gatesResource,
|
|
4524
|
+
killswitchesClient,
|
|
4525
|
+
killswitchesResource,
|
|
4526
|
+
universesClient,
|
|
4527
|
+
universesResource
|
|
4528
|
+
};
|