@stardeck-customer-apps/testing 0.1.1
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/SKILL.md +118 -0
- package/dist/config.d.mts +16 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +57 -0
- package/dist/config.mjs +32 -0
- package/dist/index.d.mts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +923 -0
- package/dist/index.mjs +878 -0
- package/dist/next/headers-shim.d.mts +38 -0
- package/dist/next/headers-shim.d.ts +38 -0
- package/dist/next/headers-shim.js +114 -0
- package/dist/next/headers-shim.mjs +86 -0
- package/dist/server-only-shim.d.mts +2 -0
- package/dist/server-only-shim.d.ts +2 -0
- package/dist/server-only-shim.js +18 -0
- package/dist/server-only-shim.mjs +0 -0
- package/dist/setup.d.mts +2 -0
- package/dist/setup.d.ts +2 -0
- package/dist/setup.js +658 -0
- package/dist/setup.mjs +634 -0
- package/package.json +91 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
CONTROL_PLANE_TEST_URL: () => CONTROL_PLANE_TEST_URL,
|
|
34
|
+
DATA_STORE_TEST_HOST: () => DATA_STORE_TEST_HOST,
|
|
35
|
+
DEFAULT_SCHEMA_PATH: () => DEFAULT_SCHEMA_PATH,
|
|
36
|
+
TEST_ENV_DEFAULTS: () => TEST_ENV_DEFAULTS,
|
|
37
|
+
WORKFLOW_NAME_PREFIX: () => WORKFLOW_NAME_PREFIX,
|
|
38
|
+
callRoute: () => callRoute,
|
|
39
|
+
createTestApp: () => createTestApp,
|
|
40
|
+
describeWorkflow: () => describeWorkflow,
|
|
41
|
+
parseWorkflowName: () => parseWorkflowName
|
|
42
|
+
});
|
|
43
|
+
module.exports = __toCommonJS(index_exports);
|
|
44
|
+
|
|
45
|
+
// src/test-app.ts
|
|
46
|
+
var import_node_crypto3 = __toESM(require("crypto"));
|
|
47
|
+
var import_node_fs = require("fs");
|
|
48
|
+
var import_node_path = require("path");
|
|
49
|
+
|
|
50
|
+
// src/pglite/database.ts
|
|
51
|
+
var import_pglite = require("@electric-sql/pglite");
|
|
52
|
+
var import_citext = require("@electric-sql/pglite/contrib/citext");
|
|
53
|
+
var import_pg_trgm = require("@electric-sql/pglite/contrib/pg_trgm");
|
|
54
|
+
var import_pgcrypto = require("@electric-sql/pglite/contrib/pgcrypto");
|
|
55
|
+
var import_uuid_ossp = require("@electric-sql/pglite/contrib/uuid_ossp");
|
|
56
|
+
async function createPgliteDatabase() {
|
|
57
|
+
const db = new import_pglite.PGlite({
|
|
58
|
+
extensions: { pg_trgm: import_pg_trgm.pg_trgm, pgcrypto: import_pgcrypto.pgcrypto, uuid_ossp: import_uuid_ossp.uuid_ossp, citext: import_citext.citext }
|
|
59
|
+
});
|
|
60
|
+
await db.waitReady;
|
|
61
|
+
return db;
|
|
62
|
+
}
|
|
63
|
+
async function applySchema(db, schemaSql) {
|
|
64
|
+
const trimmed = schemaSql.trim();
|
|
65
|
+
if (!trimmed) return;
|
|
66
|
+
await db.exec(trimmed);
|
|
67
|
+
}
|
|
68
|
+
async function dropAllTables(db) {
|
|
69
|
+
await db.exec(`DROP SCHEMA public CASCADE; CREATE SCHEMA public;`);
|
|
70
|
+
}
|
|
71
|
+
var identityParsers = null;
|
|
72
|
+
function getIdentityParsers() {
|
|
73
|
+
if (!identityParsers) {
|
|
74
|
+
identityParsers = {};
|
|
75
|
+
const identity = (value) => value;
|
|
76
|
+
for (let oid = 1; oid <= 13e3; oid++) {
|
|
77
|
+
identityParsers[oid] = identity;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return identityParsers;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// src/global-singleton.ts
|
|
84
|
+
function globalSingleton(key, create) {
|
|
85
|
+
const symbol = /* @__PURE__ */ Symbol.for(`stardeck-testing.${key}`);
|
|
86
|
+
const holder = globalThis;
|
|
87
|
+
holder[symbol] ??= create();
|
|
88
|
+
return holder[symbol];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/state.ts
|
|
92
|
+
var state = globalSingleton("state", () => ({
|
|
93
|
+
db: null,
|
|
94
|
+
currentUser: null,
|
|
95
|
+
sessions: /* @__PURE__ */ new Map(),
|
|
96
|
+
refreshSessions: /* @__PURE__ */ new Map(),
|
|
97
|
+
emails: [],
|
|
98
|
+
emailCounter: 0,
|
|
99
|
+
allowNetwork: false
|
|
100
|
+
}));
|
|
101
|
+
function requireDb() {
|
|
102
|
+
if (!state.db) {
|
|
103
|
+
throw new Error(
|
|
104
|
+
"[stardeck-testing] No active test app. Call `await createTestApp(...)` in beforeAll before using Stardeck SDKs in tests."
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return state.db;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// src/constants.ts
|
|
111
|
+
var TEST_DOMAIN_SUFFIX = ".stardeck.test";
|
|
112
|
+
var CONTROL_PLANE_TEST_URL = "https://control-plane.stardeck.test";
|
|
113
|
+
var DATA_STORE_TEST_HOST = "db.stardeck.test";
|
|
114
|
+
var TEST_ENV_DEFAULTS = {
|
|
115
|
+
CONTROL_PLANE_URL: CONTROL_PLANE_TEST_URL,
|
|
116
|
+
DEPLOYMENT_SECRET: "stardeck-test-deployment-secret",
|
|
117
|
+
ORGANIZATION_ID: "00000000-0000-4000-8000-00000000000a",
|
|
118
|
+
PROJECT_ID: "00000000-0000-4000-8000-00000000000b",
|
|
119
|
+
DEPLOYMENT_ID: "00000000-0000-4000-8000-00000000000c",
|
|
120
|
+
DATA_STORE_URL: `postgresql://test:test@${DATA_STORE_TEST_HOST}/main`
|
|
121
|
+
};
|
|
122
|
+
var DEFAULT_TEST_USER = {
|
|
123
|
+
id: "test-user-1",
|
|
124
|
+
email: "test-user@example.com",
|
|
125
|
+
name: "Test User",
|
|
126
|
+
role: "member",
|
|
127
|
+
permissions: []
|
|
128
|
+
};
|
|
129
|
+
var DEFAULT_SCHEMA_PATH = "./src/generated/data-store-schema.sql";
|
|
130
|
+
|
|
131
|
+
// src/simulator/hmac.ts
|
|
132
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
133
|
+
var TIMESTAMP_TOLERANCE_SECONDS = 300;
|
|
134
|
+
function verifyDeploymentAuthHeader(secret, header) {
|
|
135
|
+
const dotIndex = header.lastIndexOf(".");
|
|
136
|
+
if (dotIndex === -1) return null;
|
|
137
|
+
const payloadB64 = header.slice(0, dotIndex);
|
|
138
|
+
const signature = header.slice(dotIndex + 1);
|
|
139
|
+
let payloadJson;
|
|
140
|
+
try {
|
|
141
|
+
payloadJson = Buffer.from(payloadB64, "base64").toString("utf-8");
|
|
142
|
+
} catch {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const expected = import_node_crypto.default.createHmac("sha256", secret).update(payloadJson).digest("hex");
|
|
146
|
+
const expectedBuf = Buffer.from(expected, "utf-8");
|
|
147
|
+
const actualBuf = Buffer.from(signature, "utf-8");
|
|
148
|
+
if (expectedBuf.length !== actualBuf.length || !import_node_crypto.default.timingSafeEqual(expectedBuf, actualBuf)) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
let payload;
|
|
152
|
+
try {
|
|
153
|
+
payload = JSON.parse(payloadJson);
|
|
154
|
+
} catch {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (payload.type !== "deployment-request") return null;
|
|
158
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
159
|
+
if (Math.abs(now - payload.timestamp) > TIMESTAMP_TOLERANCE_SECONDS) return null;
|
|
160
|
+
return payload;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/simulator/http.ts
|
|
164
|
+
function json(body, status = 200) {
|
|
165
|
+
return new Response(JSON.stringify(body), {
|
|
166
|
+
status,
|
|
167
|
+
headers: { "Content-Type": "application/json" }
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function success(data) {
|
|
171
|
+
return json({ success: true, data });
|
|
172
|
+
}
|
|
173
|
+
function failure(error, status = 400) {
|
|
174
|
+
return json({ success: false, error }, status);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/simulator/data-store.ts
|
|
178
|
+
function quoteIdent(name) {
|
|
179
|
+
return `"${name.replace(/"/g, '""')}"`;
|
|
180
|
+
}
|
|
181
|
+
function normalizeIdent(name) {
|
|
182
|
+
return name.toLowerCase().replace(/\s+/g, "_");
|
|
183
|
+
}
|
|
184
|
+
async function getTableColumns(db, tableName) {
|
|
185
|
+
const { rows } = await db.query(
|
|
186
|
+
`SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = $1 ORDER BY ordinal_position`,
|
|
187
|
+
[tableName]
|
|
188
|
+
);
|
|
189
|
+
if (rows.length === 0) return null;
|
|
190
|
+
return rows.map((r) => r.column_name);
|
|
191
|
+
}
|
|
192
|
+
async function getPrimaryKeyColumns(db, tableName) {
|
|
193
|
+
const { rows } = await db.query(
|
|
194
|
+
`SELECT ku.column_name
|
|
195
|
+
FROM information_schema.table_constraints tc
|
|
196
|
+
JOIN information_schema.key_column_usage ku
|
|
197
|
+
ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
|
|
198
|
+
WHERE tc.constraint_type = 'PRIMARY KEY'
|
|
199
|
+
AND tc.table_schema = 'public'
|
|
200
|
+
AND tc.table_name = $1`,
|
|
201
|
+
[tableName]
|
|
202
|
+
);
|
|
203
|
+
return rows.map((r) => r.column_name);
|
|
204
|
+
}
|
|
205
|
+
function coerceNumericFields(result) {
|
|
206
|
+
const numericFields = result.fields.filter((f) => f.dataTypeID === 1700).map((f) => f.name);
|
|
207
|
+
if (numericFields.length === 0) return result.rows;
|
|
208
|
+
return result.rows.map((row) => {
|
|
209
|
+
const coerced = { ...row };
|
|
210
|
+
for (const name of numericFields) {
|
|
211
|
+
if (typeof coerced[name] === "string") {
|
|
212
|
+
coerced[name] = parseFloat(coerced[name]);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return coerced;
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
function buildWhereClause(filters, validColumns) {
|
|
219
|
+
const conditions = [];
|
|
220
|
+
const params = [];
|
|
221
|
+
let paramIndex = 1;
|
|
222
|
+
for (const filter of filters) {
|
|
223
|
+
if (!validColumns.includes(filter.column)) continue;
|
|
224
|
+
const col = quoteIdent(filter.column);
|
|
225
|
+
switch (filter.operator) {
|
|
226
|
+
case "eq":
|
|
227
|
+
conditions.push(`${col} = $${paramIndex++}`);
|
|
228
|
+
params.push(filter.value);
|
|
229
|
+
break;
|
|
230
|
+
case "neq":
|
|
231
|
+
conditions.push(`${col} != $${paramIndex++}`);
|
|
232
|
+
params.push(filter.value);
|
|
233
|
+
break;
|
|
234
|
+
case "contains": {
|
|
235
|
+
conditions.push(`${col}::text ILIKE $${paramIndex++}`);
|
|
236
|
+
const escaped = String(filter.value ?? "").replace(/[%_\\]/g, "\\$&");
|
|
237
|
+
params.push(`%${escaped}%`);
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "gt":
|
|
241
|
+
conditions.push(`${col} > $${paramIndex++}`);
|
|
242
|
+
params.push(filter.value);
|
|
243
|
+
break;
|
|
244
|
+
case "lt":
|
|
245
|
+
conditions.push(`${col} < $${paramIndex++}`);
|
|
246
|
+
params.push(filter.value);
|
|
247
|
+
break;
|
|
248
|
+
case "is_null":
|
|
249
|
+
conditions.push(`${col} IS NULL`);
|
|
250
|
+
break;
|
|
251
|
+
case "is_not_null":
|
|
252
|
+
conditions.push(`${col} IS NOT NULL`);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (conditions.length === 0) return { clause: "", params: [] };
|
|
257
|
+
return { clause: `WHERE ${conditions.join(" AND ")}`, params };
|
|
258
|
+
}
|
|
259
|
+
async function handleQuery(db, body) {
|
|
260
|
+
if (typeof body.tableName !== "string" || body.tableName.length === 0) {
|
|
261
|
+
return failure("Invalid request body", 400);
|
|
262
|
+
}
|
|
263
|
+
const tableName = body.tableName;
|
|
264
|
+
const limit = body.limit === void 0 ? 50 : Number(body.limit);
|
|
265
|
+
const offset = body.offset === void 0 ? 0 : Number(body.offset);
|
|
266
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
|
|
267
|
+
return failure("Invalid request body", 400);
|
|
268
|
+
}
|
|
269
|
+
if (!Number.isInteger(offset) || offset < 0) {
|
|
270
|
+
return failure("Invalid request body", 400);
|
|
271
|
+
}
|
|
272
|
+
if (body.orderDir !== void 0 && body.orderDir !== "asc" && body.orderDir !== "desc") {
|
|
273
|
+
return failure("Invalid request body", 400);
|
|
274
|
+
}
|
|
275
|
+
const orderBy = body.orderBy ? String(body.orderBy) : void 0;
|
|
276
|
+
const orderDir = body.orderDir === "desc" ? "desc" : "asc";
|
|
277
|
+
const filters = body.filters ?? [];
|
|
278
|
+
const validColumns = await getTableColumns(db, tableName);
|
|
279
|
+
if (!validColumns) return failure(`Table "${tableName}" not found`, 404);
|
|
280
|
+
if (orderBy && !validColumns.includes(orderBy)) {
|
|
281
|
+
return failure(`Column "${orderBy}" not found in table "${tableName}"`);
|
|
282
|
+
}
|
|
283
|
+
const { clause: whereClause, params: whereParams } = buildWhereClause(filters, validColumns);
|
|
284
|
+
const orderClause = orderBy ? `ORDER BY ${quoteIdent(orderBy)} ${orderDir === "desc" ? "DESC" : "ASC"}` : "";
|
|
285
|
+
const nextParamIdx = whereParams.length + 1;
|
|
286
|
+
const quotedTable = quoteIdent(tableName);
|
|
287
|
+
const preparedWhereParams = whereParams.map(prepareParam);
|
|
288
|
+
const dataResult = await db.query(
|
|
289
|
+
`SELECT * FROM ${quotedTable} ${whereClause} ${orderClause} LIMIT $${nextParamIdx} OFFSET $${nextParamIdx + 1}`,
|
|
290
|
+
[...preparedWhereParams, limit, offset]
|
|
291
|
+
);
|
|
292
|
+
const countResult = await db.query(
|
|
293
|
+
`SELECT count(*)::int as total FROM ${quotedTable} ${whereClause}`,
|
|
294
|
+
preparedWhereParams
|
|
295
|
+
);
|
|
296
|
+
return success({
|
|
297
|
+
rows: coerceNumericFields(dataResult),
|
|
298
|
+
columns: validColumns,
|
|
299
|
+
total: countResult.rows[0]?.total ?? 0,
|
|
300
|
+
limit,
|
|
301
|
+
offset
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function buildPkWhere(primaryKey, pkColumns, startParamIdx) {
|
|
305
|
+
const conditions = [];
|
|
306
|
+
const params = [];
|
|
307
|
+
let paramIdx = startParamIdx;
|
|
308
|
+
for (const col of pkColumns) {
|
|
309
|
+
if (!(col in primaryKey)) {
|
|
310
|
+
throw new Error(`Missing primary key column: ${col}`);
|
|
311
|
+
}
|
|
312
|
+
conditions.push(`${quoteIdent(col)} = $${paramIdx++}`);
|
|
313
|
+
params.push(primaryKey[col]);
|
|
314
|
+
}
|
|
315
|
+
return { clause: conditions.join(" AND "), params };
|
|
316
|
+
}
|
|
317
|
+
async function handleMutate(db, body) {
|
|
318
|
+
if (typeof body.tableName !== "string" || body.tableName.length === 0) {
|
|
319
|
+
return failure("Invalid request body", 400);
|
|
320
|
+
}
|
|
321
|
+
const tableName = body.tableName;
|
|
322
|
+
const operation = String(body.operation ?? "");
|
|
323
|
+
if (!["insert", "update", "delete"].includes(operation)) {
|
|
324
|
+
return failure("Invalid request body", 400);
|
|
325
|
+
}
|
|
326
|
+
const columns = await getTableColumns(db, tableName);
|
|
327
|
+
if (!columns) return failure(`Table "${tableName}" not found`, 404);
|
|
328
|
+
const pkColumns = await getPrimaryKeyColumns(db, tableName);
|
|
329
|
+
switch (operation) {
|
|
330
|
+
case "insert": {
|
|
331
|
+
const row = body.row ?? {};
|
|
332
|
+
const insertColumns = Object.keys(row).filter((col) => columns.includes(col));
|
|
333
|
+
let sql;
|
|
334
|
+
let values;
|
|
335
|
+
if (insertColumns.length === 0) {
|
|
336
|
+
sql = `INSERT INTO ${quoteIdent(tableName)} DEFAULT VALUES RETURNING *`;
|
|
337
|
+
values = [];
|
|
338
|
+
} else {
|
|
339
|
+
values = insertColumns.map((col) => prepareParam(row[col]));
|
|
340
|
+
const colList = insertColumns.map((c) => quoteIdent(c)).join(", ");
|
|
341
|
+
const paramList = insertColumns.map((_, i) => `$${i + 1}`).join(", ");
|
|
342
|
+
sql = `INSERT INTO ${quoteIdent(tableName)} (${colList}) VALUES (${paramList}) RETURNING *`;
|
|
343
|
+
}
|
|
344
|
+
const result = await db.query(sql, values);
|
|
345
|
+
return success({ row: result.rows[0] });
|
|
346
|
+
}
|
|
347
|
+
case "update": {
|
|
348
|
+
const column = String(body.column ?? "");
|
|
349
|
+
if (!columns.includes(column)) {
|
|
350
|
+
return failure(`Column "${column}" not found in table "${tableName}"`);
|
|
351
|
+
}
|
|
352
|
+
if (pkColumns.length === 0) {
|
|
353
|
+
return failure(`Table "${tableName}" has no primary key`);
|
|
354
|
+
}
|
|
355
|
+
const { clause: pkClause, params: pkParams } = buildPkWhere(
|
|
356
|
+
body.primaryKey ?? {},
|
|
357
|
+
pkColumns,
|
|
358
|
+
2
|
|
359
|
+
);
|
|
360
|
+
const updateValue = body.value === "" ? null : prepareParam(body.value);
|
|
361
|
+
const result = await db.query(
|
|
362
|
+
`UPDATE ${quoteIdent(tableName)} SET ${quoteIdent(column)} = $1 WHERE ${pkClause} RETURNING *`,
|
|
363
|
+
[updateValue, ...pkParams.map(prepareParam)]
|
|
364
|
+
);
|
|
365
|
+
if (result.rows.length === 0) return failure("Row not found", 404);
|
|
366
|
+
return success({ row: result.rows[0] });
|
|
367
|
+
}
|
|
368
|
+
case "delete": {
|
|
369
|
+
if (pkColumns.length === 0) {
|
|
370
|
+
return failure(`Table "${tableName}" has no primary key`);
|
|
371
|
+
}
|
|
372
|
+
const { clause: pkClause, params: pkParams } = buildPkWhere(
|
|
373
|
+
body.primaryKey ?? {},
|
|
374
|
+
pkColumns,
|
|
375
|
+
1
|
|
376
|
+
);
|
|
377
|
+
const result = await db.query(
|
|
378
|
+
`DELETE FROM ${quoteIdent(tableName)} WHERE ${pkClause} RETURNING *`,
|
|
379
|
+
pkParams.map(prepareParam)
|
|
380
|
+
);
|
|
381
|
+
if (result.rows.length === 0) return failure("Row not found", 404);
|
|
382
|
+
return success({ deleted: true, row: result.rows[0] });
|
|
383
|
+
}
|
|
384
|
+
default:
|
|
385
|
+
return failure(`Unknown operation "${operation}"`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function prepareParam(value) {
|
|
389
|
+
if (value instanceof Date) return value.toISOString();
|
|
390
|
+
if (Array.isArray(value)) return encodeArrayLiteral(value);
|
|
391
|
+
if (value !== null && typeof value === "object") return JSON.stringify(value);
|
|
392
|
+
return value;
|
|
393
|
+
}
|
|
394
|
+
function encodeArrayLiteral(values) {
|
|
395
|
+
const items = values.map((v) => {
|
|
396
|
+
if (v === null || v === void 0) return "NULL";
|
|
397
|
+
if (Array.isArray(v)) return quoteArrayElement(encodeArrayLiteral(v));
|
|
398
|
+
if (v instanceof Date) return quoteArrayElement(v.toISOString());
|
|
399
|
+
if (typeof v === "object") return quoteArrayElement(JSON.stringify(v));
|
|
400
|
+
return quoteArrayElement(String(v));
|
|
401
|
+
});
|
|
402
|
+
return `{${items.join(",")}}`;
|
|
403
|
+
}
|
|
404
|
+
function quoteArrayElement(raw) {
|
|
405
|
+
return `"${raw.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
406
|
+
}
|
|
407
|
+
async function handleGetSchema(db) {
|
|
408
|
+
const { rows } = await db.query(
|
|
409
|
+
`SELECT
|
|
410
|
+
t.table_name,
|
|
411
|
+
c.column_name,
|
|
412
|
+
c.data_type,
|
|
413
|
+
c.is_nullable,
|
|
414
|
+
c.column_default,
|
|
415
|
+
CASE WHEN pk.column_name IS NOT NULL THEN true ELSE false END as is_primary_key
|
|
416
|
+
FROM information_schema.tables t
|
|
417
|
+
JOIN information_schema.columns c
|
|
418
|
+
ON t.table_name = c.table_name AND t.table_schema = c.table_schema
|
|
419
|
+
LEFT JOIN (
|
|
420
|
+
SELECT ku.column_name, ku.table_name
|
|
421
|
+
FROM information_schema.table_constraints tc
|
|
422
|
+
JOIN information_schema.key_column_usage ku
|
|
423
|
+
ON tc.constraint_name = ku.constraint_name AND tc.table_schema = ku.table_schema
|
|
424
|
+
WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'
|
|
425
|
+
) pk ON pk.table_name = c.table_name AND pk.column_name = c.column_name
|
|
426
|
+
WHERE t.table_schema = 'public'
|
|
427
|
+
AND t.table_type = 'BASE TABLE'
|
|
428
|
+
AND t.table_name NOT LIKE '\\_deleted\\_%'
|
|
429
|
+
ORDER BY t.table_name, c.ordinal_position`
|
|
430
|
+
);
|
|
431
|
+
const tables = /* @__PURE__ */ new Map();
|
|
432
|
+
for (const row of rows) {
|
|
433
|
+
if (!tables.has(row.table_name)) tables.set(row.table_name, []);
|
|
434
|
+
const cols = tables.get(row.table_name);
|
|
435
|
+
cols.push({
|
|
436
|
+
columnName: row.column_name,
|
|
437
|
+
dataType: row.data_type,
|
|
438
|
+
isNullable: row.is_nullable === "YES",
|
|
439
|
+
columnDefault: row.column_default,
|
|
440
|
+
isPrimaryKey: row.is_primary_key,
|
|
441
|
+
position: cols.length
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return success({
|
|
445
|
+
tables: Array.from(tables, ([tableName, columns]) => ({ tableName, columns }))
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
var FIELD_TYPE_TO_PG = {
|
|
449
|
+
text: "text",
|
|
450
|
+
long_text: "text",
|
|
451
|
+
number: "double precision",
|
|
452
|
+
boolean: "boolean",
|
|
453
|
+
date: "date",
|
|
454
|
+
datetime: "timestamptz",
|
|
455
|
+
select: "text",
|
|
456
|
+
multi_select: "jsonb",
|
|
457
|
+
url: "text",
|
|
458
|
+
email: "text",
|
|
459
|
+
phone: "text",
|
|
460
|
+
currency: "numeric",
|
|
461
|
+
rating: "integer",
|
|
462
|
+
relation: "uuid",
|
|
463
|
+
file_ref: "jsonb",
|
|
464
|
+
file_refs: "jsonb",
|
|
465
|
+
json: "jsonb"
|
|
466
|
+
};
|
|
467
|
+
function columnDdl(column) {
|
|
468
|
+
const pgType = FIELD_TYPE_TO_PG[column.fieldType];
|
|
469
|
+
if (!pgType) {
|
|
470
|
+
throw new Error(
|
|
471
|
+
`Unsupported fieldType "${column.fieldType}" in test simulator. Supported: ${Object.keys(FIELD_TYPE_TO_PG).join(", ")}`
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
let ddl = `${quoteIdent(normalizeIdent(column.name))} ${pgType}`;
|
|
475
|
+
if (column.nullable === false) ddl += " NOT NULL";
|
|
476
|
+
return ddl;
|
|
477
|
+
}
|
|
478
|
+
async function handleCreateTable(db, body) {
|
|
479
|
+
const name = normalizeIdent(String(body.name ?? ""));
|
|
480
|
+
const columns = body.columns ?? [];
|
|
481
|
+
if (!name) return failure("Table name is required");
|
|
482
|
+
try {
|
|
483
|
+
const columnDefs = [
|
|
484
|
+
`"id" uuid PRIMARY KEY DEFAULT gen_random_uuid()`,
|
|
485
|
+
...columns.map(columnDdl),
|
|
486
|
+
`"created_at" timestamptz NOT NULL DEFAULT now()`,
|
|
487
|
+
`"updated_at" timestamptz NOT NULL DEFAULT now()`
|
|
488
|
+
];
|
|
489
|
+
await db.exec(`CREATE TABLE ${quoteIdent(name)} (
|
|
490
|
+
${columnDefs.join(",\n ")}
|
|
491
|
+
)`);
|
|
492
|
+
} catch (error) {
|
|
493
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
494
|
+
}
|
|
495
|
+
return success({ tableName: name });
|
|
496
|
+
}
|
|
497
|
+
async function handleAddColumn(db, body) {
|
|
498
|
+
const tableName = normalizeIdent(String(body.tableName ?? ""));
|
|
499
|
+
const column = body.column;
|
|
500
|
+
if (!tableName || !column) return failure("tableName and column are required");
|
|
501
|
+
try {
|
|
502
|
+
await db.exec(`ALTER TABLE ${quoteIdent(tableName)} ADD COLUMN ${columnDdl(column)}`);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
505
|
+
}
|
|
506
|
+
return success({ columnName: normalizeIdent(column.name) });
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// src/simulator/neon-proxy.ts
|
|
510
|
+
var BOOL_OID = 16;
|
|
511
|
+
var PG_BOOL_TRUE = /* @__PURE__ */ new Set(["true", "t", "1", "yes", "y", "on"]);
|
|
512
|
+
var PG_BOOL_FALSE = /* @__PURE__ */ new Set(["false", "f", "0", "no", "n", "off"]);
|
|
513
|
+
var neonParamSerializers = {
|
|
514
|
+
[BOOL_OID]: (value) => {
|
|
515
|
+
if (typeof value === "boolean") return value ? "t" : "f";
|
|
516
|
+
const normalized = String(value).trim().toLowerCase();
|
|
517
|
+
if (PG_BOOL_TRUE.has(normalized)) return "t";
|
|
518
|
+
if (PG_BOOL_FALSE.has(normalized)) return "f";
|
|
519
|
+
throw new Error(`Invalid input for boolean type: ${JSON.stringify(value)}`);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
async function handleNeonSql(db, request) {
|
|
523
|
+
const body = await request.json();
|
|
524
|
+
if (body.queries) {
|
|
525
|
+
return json(
|
|
526
|
+
{
|
|
527
|
+
message: "Transactions over the Neon HTTP driver are not supported (matches production neon-http behavior). Use single statements in tests."
|
|
528
|
+
},
|
|
529
|
+
400
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
if (!body.query) {
|
|
533
|
+
return json({ message: "Missing query" }, 400);
|
|
534
|
+
}
|
|
535
|
+
const arrayMode = (request.headers.get("Neon-Array-Mode") ?? request.headers.get("neon-array-mode")) === "true";
|
|
536
|
+
try {
|
|
537
|
+
const result = await db.query(body.query, body.params ?? [], {
|
|
538
|
+
parsers: getIdentityParsers(),
|
|
539
|
+
serializers: neonParamSerializers,
|
|
540
|
+
rowMode: arrayMode ? "array" : "object"
|
|
541
|
+
});
|
|
542
|
+
const command = body.query.trim().split(/\s+/)[0]?.toUpperCase() ?? "SELECT";
|
|
543
|
+
return json({
|
|
544
|
+
command,
|
|
545
|
+
rowCount: result.affectedRows || result.rows.length,
|
|
546
|
+
fields: result.fields.map((f) => ({ name: f.name, dataTypeID: f.dataTypeID })),
|
|
547
|
+
rows: result.rows,
|
|
548
|
+
rowAsArray: arrayMode
|
|
549
|
+
});
|
|
550
|
+
} catch (error) {
|
|
551
|
+
const err = error;
|
|
552
|
+
return json({ message: err.message, code: err.code }, 400);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// src/simulator/auth.ts
|
|
557
|
+
var import_node_crypto2 = __toESM(require("crypto"));
|
|
558
|
+
function handleAuthVerify(request) {
|
|
559
|
+
const authHeader = request.headers.get("Authorization");
|
|
560
|
+
const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
561
|
+
const user = token ? state.sessions.get(token) : null;
|
|
562
|
+
if (!user) {
|
|
563
|
+
return json({ authenticated: false, user: null }, 401);
|
|
564
|
+
}
|
|
565
|
+
return json({ authenticated: true, user });
|
|
566
|
+
}
|
|
567
|
+
function handleAuthRefresh(request) {
|
|
568
|
+
const authHeader = request.headers.get("Authorization");
|
|
569
|
+
const refreshToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
570
|
+
const user = refreshToken ? state.refreshSessions.get(refreshToken) : null;
|
|
571
|
+
if (!user || !refreshToken) {
|
|
572
|
+
return json({ error: "invalid_grant" }, 401);
|
|
573
|
+
}
|
|
574
|
+
const accessToken = `test-access-${import_node_crypto2.default.randomUUID()}`;
|
|
575
|
+
state.sessions.set(accessToken, user);
|
|
576
|
+
return json({
|
|
577
|
+
access_token: accessToken,
|
|
578
|
+
refresh_token: refreshToken,
|
|
579
|
+
token_type: "Bearer",
|
|
580
|
+
expires_in: 3600,
|
|
581
|
+
refresh_token_expires_in: 86400
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// src/simulator/email.ts
|
|
586
|
+
function toArray(value) {
|
|
587
|
+
if (!value) return [];
|
|
588
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
589
|
+
}
|
|
590
|
+
async function handleEmailSend(request) {
|
|
591
|
+
const body = await request.json();
|
|
592
|
+
const from = body.from ?? {};
|
|
593
|
+
if (!from.name || !from.prefix || !body.to || !body.subject) {
|
|
594
|
+
return failure("Missing required email fields");
|
|
595
|
+
}
|
|
596
|
+
state.emailCounter += 1;
|
|
597
|
+
const resendId = `test-email-${state.emailCounter}`;
|
|
598
|
+
const fromAddress = `${from.name} <${from.prefix}@test.stardeck.email>`;
|
|
599
|
+
const email = {
|
|
600
|
+
resendId,
|
|
601
|
+
fromAddress,
|
|
602
|
+
from: { name: from.name, prefix: from.prefix },
|
|
603
|
+
to: toArray(body.to),
|
|
604
|
+
cc: toArray(body.cc),
|
|
605
|
+
bcc: toArray(body.bcc),
|
|
606
|
+
subject: String(body.subject),
|
|
607
|
+
html: body.html ? String(body.html) : void 0,
|
|
608
|
+
text: body.text ? String(body.text) : void 0,
|
|
609
|
+
replyTo: body.replyTo ? String(body.replyTo) : void 0,
|
|
610
|
+
attachments: (body.attachments ?? []).map((a) => ({
|
|
611
|
+
filename: String(a.filename ?? ""),
|
|
612
|
+
contentType: a.contentType ? String(a.contentType) : void 0
|
|
613
|
+
})),
|
|
614
|
+
sentAt: /* @__PURE__ */ new Date()
|
|
615
|
+
};
|
|
616
|
+
state.emails.push(email);
|
|
617
|
+
return success({ resendId, fromAddress });
|
|
618
|
+
}
|
|
619
|
+
function createInbox() {
|
|
620
|
+
return {
|
|
621
|
+
all: () => [...state.emails],
|
|
622
|
+
to: (address) => state.emails.filter(
|
|
623
|
+
(e) => e.to.includes(address) || e.cc.includes(address) || e.bcc.includes(address)
|
|
624
|
+
),
|
|
625
|
+
latest: (address) => {
|
|
626
|
+
const pool = address ? state.emails.filter(
|
|
627
|
+
(e) => e.to.includes(address) || e.cc.includes(address) || e.bcc.includes(address)
|
|
628
|
+
) : state.emails;
|
|
629
|
+
return pool[pool.length - 1];
|
|
630
|
+
},
|
|
631
|
+
clear: () => {
|
|
632
|
+
state.emails = [];
|
|
633
|
+
},
|
|
634
|
+
get count() {
|
|
635
|
+
return state.emails.length;
|
|
636
|
+
}
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// src/simulator/router.ts
|
|
641
|
+
var fetchHolder = globalSingleton("fetch-holder", () => ({
|
|
642
|
+
originalFetch: null
|
|
643
|
+
}));
|
|
644
|
+
function isLocalHost(hostname) {
|
|
645
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "0.0.0.0";
|
|
646
|
+
}
|
|
647
|
+
async function handleSimulatedRequest(request, url) {
|
|
648
|
+
if (url.pathname === "/sql") {
|
|
649
|
+
return handleNeonSql(requireDb(), request);
|
|
650
|
+
}
|
|
651
|
+
const authMatch = url.pathname.match(/^\/api\/deployments\/[^/]+\/auth\/(verify|refresh)$/);
|
|
652
|
+
if (authMatch) {
|
|
653
|
+
return authMatch[1] === "verify" ? handleAuthVerify(request) : handleAuthRefresh(request);
|
|
654
|
+
}
|
|
655
|
+
const dataStoreMatch = url.pathname.match(/^\/api\/data-stores\/[^/]+(\/.*)?$/);
|
|
656
|
+
const isEmail = url.pathname === "/api/email/send";
|
|
657
|
+
if (dataStoreMatch || isEmail) {
|
|
658
|
+
const authHeader = request.headers.get("X-Stardeck-Auth");
|
|
659
|
+
if (!authHeader) {
|
|
660
|
+
return failure("Missing authentication header", 401);
|
|
661
|
+
}
|
|
662
|
+
const secret = process.env.DEPLOYMENT_SECRET;
|
|
663
|
+
if (!secret || !verifyDeploymentAuthHeader(secret, authHeader)) {
|
|
664
|
+
return failure("Invalid authentication", 401);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (isEmail && request.method === "POST") {
|
|
668
|
+
return handleEmailSend(request);
|
|
669
|
+
}
|
|
670
|
+
if (dataStoreMatch) {
|
|
671
|
+
const subPath = dataStoreMatch[1] ?? "";
|
|
672
|
+
const db = requireDb();
|
|
673
|
+
const readBody = async () => await request.json();
|
|
674
|
+
if (subPath === "/query" && request.method === "POST") {
|
|
675
|
+
return handleQuery(db, await readBody());
|
|
676
|
+
}
|
|
677
|
+
if (subPath === "/mutate" && request.method === "POST") {
|
|
678
|
+
return handleMutate(db, await readBody());
|
|
679
|
+
}
|
|
680
|
+
if (subPath === "/schema" && request.method === "GET") {
|
|
681
|
+
return handleGetSchema(db);
|
|
682
|
+
}
|
|
683
|
+
if (subPath === "/schema/tables" && request.method === "POST") {
|
|
684
|
+
return handleCreateTable(db, await readBody());
|
|
685
|
+
}
|
|
686
|
+
if (subPath === "/schema/columns" && request.method === "POST") {
|
|
687
|
+
return handleAddColumn(db, await readBody());
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
return failure(
|
|
691
|
+
`[stardeck-testing] No simulator for ${request.method} ${url.pathname}. Supported: data-store query/mutate/schema, email send, auth verify/refresh, Neon /sql.`,
|
|
692
|
+
404
|
|
693
|
+
);
|
|
694
|
+
}
|
|
695
|
+
function installFetchRouter() {
|
|
696
|
+
if (fetchHolder.originalFetch) return;
|
|
697
|
+
fetchHolder.originalFetch = globalThis.fetch;
|
|
698
|
+
globalThis.fetch = async (input, init) => {
|
|
699
|
+
const request = input instanceof Request ? input : new Request(input, init);
|
|
700
|
+
const merged = input instanceof Request && init ? new Request(request, init) : request;
|
|
701
|
+
const url = new URL(merged.url);
|
|
702
|
+
if (url.hostname.endsWith(TEST_DOMAIN_SUFFIX)) {
|
|
703
|
+
return handleSimulatedRequest(merged, url);
|
|
704
|
+
}
|
|
705
|
+
if (isLocalHost(url.hostname) || state.allowNetwork) {
|
|
706
|
+
return fetchHolder.originalFetch(merged);
|
|
707
|
+
}
|
|
708
|
+
throw new Error(
|
|
709
|
+
`[stardeck-testing] Blocked outbound request to ${url.origin}${url.pathname}. Tests run offline by default for determinism. If this request is intentional, pass { allowNetwork: true } to createTestApp(), or point the code at a simulated service.`
|
|
710
|
+
);
|
|
711
|
+
};
|
|
712
|
+
}
|
|
713
|
+
function uninstallFetchRouter() {
|
|
714
|
+
if (fetchHolder.originalFetch) {
|
|
715
|
+
globalThis.fetch = fetchHolder.originalFetch;
|
|
716
|
+
fetchHolder.originalFetch = null;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/test-app.ts
|
|
721
|
+
function applyTestEnv() {
|
|
722
|
+
for (const [key, value] of Object.entries(TEST_ENV_DEFAULTS)) {
|
|
723
|
+
process.env[key] = value;
|
|
724
|
+
}
|
|
725
|
+
delete process.env.DISABLE_AUTH;
|
|
726
|
+
delete process.env.NEXT_PUBLIC_DISABLE_AUTH;
|
|
727
|
+
delete process.env.SANDBOX_MODE;
|
|
728
|
+
}
|
|
729
|
+
function buildUser(partial) {
|
|
730
|
+
return {
|
|
731
|
+
...DEFAULT_TEST_USER,
|
|
732
|
+
organizationId: process.env.ORGANIZATION_ID ?? null,
|
|
733
|
+
projectId: process.env.PROJECT_ID ?? null,
|
|
734
|
+
...partial
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
function loadSchemaSql(options) {
|
|
738
|
+
if (options.schemaSql !== void 0) return options.schemaSql;
|
|
739
|
+
if (options.schema === null) return null;
|
|
740
|
+
const schemaPath = (0, import_node_path.resolve)(process.cwd(), options.schema ?? DEFAULT_SCHEMA_PATH);
|
|
741
|
+
if (!(0, import_node_fs.existsSync)(schemaPath)) {
|
|
742
|
+
if (options.schema) {
|
|
743
|
+
throw new Error(
|
|
744
|
+
`[stardeck-testing] Schema snapshot not found at ${schemaPath}. Run \`npx stardeck-data-store generate-types\` to regenerate it.`
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
return (0, import_node_fs.readFileSync)(schemaPath, "utf-8");
|
|
750
|
+
}
|
|
751
|
+
async function createTestApp(options = {}) {
|
|
752
|
+
if (state.db) {
|
|
753
|
+
throw new Error(
|
|
754
|
+
"[stardeck-testing] A test app is already active in this test file. Call `app.close()` first, or share one app per file."
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
applyTestEnv();
|
|
758
|
+
state.allowNetwork = options.allowNetwork ?? false;
|
|
759
|
+
installFetchRouter();
|
|
760
|
+
const db = await createPgliteDatabase();
|
|
761
|
+
state.db = db;
|
|
762
|
+
const initialize = async () => {
|
|
763
|
+
const schemaSql = loadSchemaSql(options);
|
|
764
|
+
if (schemaSql) await applySchema(db, schemaSql);
|
|
765
|
+
if (options.seed) await options.seed(db);
|
|
766
|
+
};
|
|
767
|
+
try {
|
|
768
|
+
await initialize();
|
|
769
|
+
} catch (error) {
|
|
770
|
+
state.db = null;
|
|
771
|
+
await db.close().catch(() => {
|
|
772
|
+
});
|
|
773
|
+
throw error;
|
|
774
|
+
}
|
|
775
|
+
const inbox = createInbox();
|
|
776
|
+
const app = {
|
|
777
|
+
db,
|
|
778
|
+
inbox,
|
|
779
|
+
async query(sql, params = []) {
|
|
780
|
+
const result = await db.query(sql, params);
|
|
781
|
+
return result.rows;
|
|
782
|
+
},
|
|
783
|
+
asUser(user) {
|
|
784
|
+
const fullUser = buildUser(user);
|
|
785
|
+
state.currentUser = fullUser;
|
|
786
|
+
return fullUser;
|
|
787
|
+
},
|
|
788
|
+
signOut() {
|
|
789
|
+
state.currentUser = null;
|
|
790
|
+
},
|
|
791
|
+
issueSession(user) {
|
|
792
|
+
const fullUser = buildUser(user);
|
|
793
|
+
const tokens = {
|
|
794
|
+
accessToken: `test-access-${import_node_crypto3.default.randomUUID()}`,
|
|
795
|
+
refreshToken: `test-refresh-${import_node_crypto3.default.randomUUID()}`
|
|
796
|
+
};
|
|
797
|
+
state.sessions.set(tokens.accessToken, fullUser);
|
|
798
|
+
state.refreshSessions.set(tokens.refreshToken, fullUser);
|
|
799
|
+
return tokens;
|
|
800
|
+
},
|
|
801
|
+
async reset() {
|
|
802
|
+
await dropAllTables(db);
|
|
803
|
+
await initialize();
|
|
804
|
+
state.currentUser = null;
|
|
805
|
+
state.sessions.clear();
|
|
806
|
+
state.refreshSessions.clear();
|
|
807
|
+
state.emails = [];
|
|
808
|
+
state.emailCounter = 0;
|
|
809
|
+
},
|
|
810
|
+
async close() {
|
|
811
|
+
state.db = null;
|
|
812
|
+
state.currentUser = null;
|
|
813
|
+
state.sessions.clear();
|
|
814
|
+
state.refreshSessions.clear();
|
|
815
|
+
state.emails = [];
|
|
816
|
+
uninstallFetchRouter();
|
|
817
|
+
await db.close();
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
return app;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// src/next/headers-shim.ts
|
|
824
|
+
var import_node_async_hooks = require("async_hooks");
|
|
825
|
+
var requestScopeStorage = globalSingleton(
|
|
826
|
+
"request-scope",
|
|
827
|
+
() => new import_node_async_hooks.AsyncLocalStorage()
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
// src/next/call-route.ts
|
|
831
|
+
function parseCookieHeader(header) {
|
|
832
|
+
const map = /* @__PURE__ */ new Map();
|
|
833
|
+
if (!header) return map;
|
|
834
|
+
for (const part of header.split(";")) {
|
|
835
|
+
const eq = part.indexOf("=");
|
|
836
|
+
if (eq === -1) continue;
|
|
837
|
+
map.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
838
|
+
}
|
|
839
|
+
return map;
|
|
840
|
+
}
|
|
841
|
+
async function importNextServer() {
|
|
842
|
+
try {
|
|
843
|
+
return await import("next/server.js");
|
|
844
|
+
} catch {
|
|
845
|
+
return await import("next/server");
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
async function callRoute(handler, options = {}) {
|
|
849
|
+
const { NextRequest } = await importNextServer();
|
|
850
|
+
const path = options.path ?? "/api/test-route";
|
|
851
|
+
const url = new URL(`http://localhost:3333${path}`);
|
|
852
|
+
for (const [key, value] of Object.entries(options.searchParams ?? {})) {
|
|
853
|
+
url.searchParams.set(key, value);
|
|
854
|
+
}
|
|
855
|
+
const method = options.method ?? (options.body !== void 0 ? "POST" : "GET");
|
|
856
|
+
const headers = new Headers(options.headers);
|
|
857
|
+
const user = options.user !== void 0 ? options.user : state.currentUser;
|
|
858
|
+
if (user && !headers.has("x-stardeck-user")) {
|
|
859
|
+
headers.set("x-stardeck-user", JSON.stringify(user));
|
|
860
|
+
}
|
|
861
|
+
if (options.body !== void 0 && !headers.has("Content-Type")) {
|
|
862
|
+
headers.set("Content-Type", "application/json");
|
|
863
|
+
}
|
|
864
|
+
const cookiePairs = Object.entries(options.cookies ?? {});
|
|
865
|
+
if (cookiePairs.length > 0) {
|
|
866
|
+
const existing = headers.get("cookie");
|
|
867
|
+
const cookieString = cookiePairs.map(([k, v]) => `${k}=${v}`).join("; ");
|
|
868
|
+
headers.set("cookie", existing ? `${existing}; ${cookieString}` : cookieString);
|
|
869
|
+
}
|
|
870
|
+
const request = new NextRequest(url, {
|
|
871
|
+
method,
|
|
872
|
+
headers,
|
|
873
|
+
body: options.body !== void 0 ? JSON.stringify(options.body) : void 0
|
|
874
|
+
});
|
|
875
|
+
const scope = {
|
|
876
|
+
headers,
|
|
877
|
+
cookies: parseCookieHeader(headers.get("cookie"))
|
|
878
|
+
};
|
|
879
|
+
try {
|
|
880
|
+
return await requestScopeStorage.run(
|
|
881
|
+
scope,
|
|
882
|
+
() => Promise.resolve(
|
|
883
|
+
handler(request, {
|
|
884
|
+
params: Promise.resolve(options.params ?? {})
|
|
885
|
+
})
|
|
886
|
+
)
|
|
887
|
+
);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
const redirect = decodeNextRedirect(error);
|
|
890
|
+
if (redirect) return redirect;
|
|
891
|
+
throw error;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
function decodeNextRedirect(error) {
|
|
895
|
+
const digest = error?.digest;
|
|
896
|
+
if (typeof digest !== "string" || !digest.startsWith("NEXT_REDIRECT")) return null;
|
|
897
|
+
const parts = digest.split(";");
|
|
898
|
+
const location = parts[2] ?? "/";
|
|
899
|
+
const status = Number(parts[3]) || 307;
|
|
900
|
+
return new Response(null, { status, headers: { location } });
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// src/workflow.ts
|
|
904
|
+
var import_vitest = require("vitest");
|
|
905
|
+
function describeWorkflow(name, fn) {
|
|
906
|
+
(0, import_vitest.describe)(`workflow:${name}`, fn);
|
|
907
|
+
}
|
|
908
|
+
var WORKFLOW_NAME_PREFIX = "workflow:";
|
|
909
|
+
function parseWorkflowName(describeTitle) {
|
|
910
|
+
return describeTitle.startsWith(WORKFLOW_NAME_PREFIX) ? describeTitle.slice(WORKFLOW_NAME_PREFIX.length) : null;
|
|
911
|
+
}
|
|
912
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
913
|
+
0 && (module.exports = {
|
|
914
|
+
CONTROL_PLANE_TEST_URL,
|
|
915
|
+
DATA_STORE_TEST_HOST,
|
|
916
|
+
DEFAULT_SCHEMA_PATH,
|
|
917
|
+
TEST_ENV_DEFAULTS,
|
|
918
|
+
WORKFLOW_NAME_PREFIX,
|
|
919
|
+
callRoute,
|
|
920
|
+
createTestApp,
|
|
921
|
+
describeWorkflow,
|
|
922
|
+
parseWorkflowName
|
|
923
|
+
});
|