zitejs 0.9.114 → 0.9.116
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/cjs/auth/useAuth.test.d.ts +1 -0
- package/dist/cjs/auth/useAuth.test.js +96 -0
- package/dist/cjs/backend/index.d.ts +13 -0
- package/dist/cjs/backend/index.js +63 -1
- package/dist/cjs/backend/index.test.d.ts +1 -0
- package/dist/cjs/backend/index.test.js +203 -0
- package/dist/cjs/bundle/index.js +2 -0
- package/dist/cjs/runtime/index.js +108 -22
- package/dist/cjs/runtime/index.test.d.ts +1 -0
- package/dist/cjs/runtime/index.test.js +133 -0
- package/dist/cjs/sync/lib.js +14 -0
- package/dist/cjs/sync/sdkNames.d.ts +2 -1
- package/dist/cjs/sync/sdkNames.js +17 -1
- package/dist/cjs/sync/sdkNames.test.js +48 -0
- package/dist/esm/auth/useAuth.test.d.ts +1 -0
- package/dist/esm/auth/useAuth.test.js +94 -0
- package/dist/esm/backend/index.d.ts +13 -0
- package/dist/esm/backend/index.js +63 -1
- package/dist/esm/backend/index.test.d.ts +1 -0
- package/dist/esm/backend/index.test.js +201 -0
- package/dist/esm/bundle/index.js +2 -0
- package/dist/esm/cli.js +0 -0
- package/dist/esm/runtime/index.js +108 -22
- package/dist/esm/runtime/index.test.d.ts +1 -0
- package/dist/esm/runtime/index.test.js +131 -0
- package/dist/esm/sync/lib.js +14 -0
- package/dist/esm/sync/sdkNames.d.ts +2 -1
- package/dist/esm/sync/sdkNames.js +17 -1
- package/dist/esm/sync/sdkNames.test.js +48 -0
- package/package.json +2 -2
- package/dist/cjs/api/index.js +0 -5
- package/dist/cjs/db/index.js +0 -5
- package/dist/esm/api/index.d.ts +0 -2
- package/dist/esm/api/index.js +0 -1
- package/dist/esm/db/index.d.ts +0 -2
- package/dist/esm/db/index.js +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
/**
|
|
5
|
+
* The signed-out redirect on internal apps.
|
|
6
|
+
*
|
|
7
|
+
* A separate file from `index.test.ts` because it has to replace
|
|
8
|
+
* `better-auth/react` wholesale to drive `useSession`, and that would strip the
|
|
9
|
+
* real client the export tests next door assert against.
|
|
10
|
+
*
|
|
11
|
+
* The value here is entirely in WHICH signed-out renders it reacts to. An
|
|
12
|
+
* external app has a real logged-out state, and an app built before the
|
|
13
|
+
* platform sent an access mode tells us nothing — redirecting either turns a
|
|
14
|
+
* working public page into a forced sign-in.
|
|
15
|
+
*/
|
|
16
|
+
const { useSessionMock } = vitest_1.vi.hoisted(() => ({ useSessionMock: vitest_1.vi.fn() }));
|
|
17
|
+
vitest_1.vi.mock('better-auth/react', () => ({
|
|
18
|
+
createAuthClient: () => ({
|
|
19
|
+
useSession: useSessionMock,
|
|
20
|
+
signIn: {},
|
|
21
|
+
signUp: {},
|
|
22
|
+
signOut: vitest_1.vi.fn(),
|
|
23
|
+
updateUser: vitest_1.vi.fn(),
|
|
24
|
+
}),
|
|
25
|
+
}));
|
|
26
|
+
vitest_1.vi.mock('better-auth/client/plugins', () => ({
|
|
27
|
+
magicLinkClient: () => ({}),
|
|
28
|
+
inferAdditionalFields: () => ({}),
|
|
29
|
+
}));
|
|
30
|
+
// `useAuth` takes only `useEffect` from React. Running it inline is the whole
|
|
31
|
+
// of what a render would do here, and avoids pulling in a renderer.
|
|
32
|
+
vitest_1.vi.mock('react', () => ({ useEffect: (fn) => fn() }));
|
|
33
|
+
const index_js_1 = require("./index.js");
|
|
34
|
+
const APP_URL = 'https://app.zite.so/dashboard';
|
|
35
|
+
function stubLocation(href) {
|
|
36
|
+
const location = { pathname: new URL(href).pathname, href };
|
|
37
|
+
vitest_1.vi.stubGlobal('window', { location });
|
|
38
|
+
return location;
|
|
39
|
+
}
|
|
40
|
+
const signedOut = { data: null, isPending: false };
|
|
41
|
+
(0, vitest_1.beforeEach)(() => {
|
|
42
|
+
delete process.env.ZITE_ACCESS_MODE;
|
|
43
|
+
});
|
|
44
|
+
(0, vitest_1.afterEach)(() => {
|
|
45
|
+
delete process.env.ZITE_ACCESS_MODE;
|
|
46
|
+
vitest_1.vi.unstubAllGlobals();
|
|
47
|
+
useSessionMock.mockReset();
|
|
48
|
+
});
|
|
49
|
+
const render = (session, { accessMode, href = APP_URL } = {}) => {
|
|
50
|
+
if (accessMode !== undefined)
|
|
51
|
+
process.env.ZITE_ACCESS_MODE = accessMode;
|
|
52
|
+
const location = stubLocation(href);
|
|
53
|
+
useSessionMock.mockReturnValue(session);
|
|
54
|
+
return { result: (0, index_js_1.useAuth)(), location };
|
|
55
|
+
};
|
|
56
|
+
(0, vitest_1.describe)('useAuth signed-out handling', () => {
|
|
57
|
+
(0, vitest_1.it)('sends a signed-out visitor on an internal app to sign-in', () => {
|
|
58
|
+
const { result, location } = render(signedOut, { accessMode: 'internal' });
|
|
59
|
+
(0, vitest_1.expect)(location.href.startsWith('/auth/login')).toBe(true);
|
|
60
|
+
// Reported as loading rather than signed out: the signed-out branch of an
|
|
61
|
+
// internal app is the empty screen this exists to prevent.
|
|
62
|
+
(0, vitest_1.expect)(result.isLoading).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
(0, vitest_1.it)('leaves a signed-out visitor on an external app alone', () => {
|
|
65
|
+
const { result, location } = render(signedOut, { accessMode: 'external' });
|
|
66
|
+
(0, vitest_1.expect)(location.href).toBe(APP_URL);
|
|
67
|
+
(0, vitest_1.expect)(result.isLoading).toBe(false);
|
|
68
|
+
(0, vitest_1.expect)(result.user).toBe(null);
|
|
69
|
+
});
|
|
70
|
+
// Every app published before the platform started sending an access mode.
|
|
71
|
+
(0, vitest_1.it)('leaves an app that never declared an access mode alone', () => {
|
|
72
|
+
const { result, location } = render(signedOut);
|
|
73
|
+
(0, vitest_1.expect)(location.href).toBe(APP_URL);
|
|
74
|
+
(0, vitest_1.expect)(result.isLoading).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
(0, vitest_1.it)('does not redirect while the session is still resolving', () => {
|
|
77
|
+
const { result, location } = render({ data: null, isPending: true }, { accessMode: 'internal' });
|
|
78
|
+
(0, vitest_1.expect)(location.href).toBe(APP_URL);
|
|
79
|
+
(0, vitest_1.expect)(result.isLoading).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
(0, vitest_1.it)('does not redirect a signed-in visitor', () => {
|
|
82
|
+
const { result, location } = render({ data: { user: { id: 'u1', email: 'a@b.com' } }, isPending: false }, { accessMode: 'internal' });
|
|
83
|
+
(0, vitest_1.expect)(location.href).toBe(APP_URL);
|
|
84
|
+
(0, vitest_1.expect)(result.isLoading).toBe(false);
|
|
85
|
+
(0, vitest_1.expect)(result.user).toMatchObject({ id: 'u1' });
|
|
86
|
+
});
|
|
87
|
+
// `loginWithRedirect` refuses to navigate away from an auth page, so claiming
|
|
88
|
+
// a redirect here would hang the caller on `isLoading` forever.
|
|
89
|
+
(0, vitest_1.it)('does not claim to be loading on an auth page it cannot leave', () => {
|
|
90
|
+
const { result } = render(signedOut, {
|
|
91
|
+
accessMode: 'internal',
|
|
92
|
+
href: 'https://app.zite.so/auth/login',
|
|
93
|
+
});
|
|
94
|
+
(0, vitest_1.expect)(result.isLoading).toBe(false);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -82,6 +82,12 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
82
82
|
* `{ user: null }` for a webhook fire exactly as it does for a cron one.
|
|
83
83
|
*/
|
|
84
84
|
webhook?: TWebhook;
|
|
85
|
+
/**
|
|
86
|
+
* Set by {@link createEndpoint}, not by hand. The worker validates
|
|
87
|
+
* `inputSchema` for bundles built before this did, and skips ones that
|
|
88
|
+
* declare it — parsing twice would apply a schema's transforms twice.
|
|
89
|
+
*/
|
|
90
|
+
validatesInput?: boolean;
|
|
85
91
|
execute: (params: {
|
|
86
92
|
input: TInput;
|
|
87
93
|
context: TSchedule extends ZiteSchedule ? ZiteRequestContext | ZiteScheduledContext : TWebhook extends ZiteWebhook ? ZiteRequestContext | ZiteScheduledContext : ZiteRequestContext;
|
|
@@ -89,4 +95,11 @@ export interface EndpointConfig<TInput = unknown, TOutput = unknown, TStream ext
|
|
|
89
95
|
stream: ZiteStreamInterface;
|
|
90
96
|
} : {})) => Promise<TOutput> | TOutput;
|
|
91
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Create an endpoint definition, parsing `input` against `inputSchema` before
|
|
100
|
+
* `execute` runs. Nothing downstream parses it — the worker calls `execute` on
|
|
101
|
+
* whatever this returns — so while this was a bare `return config` a required
|
|
102
|
+
* field could be missing and the endpoint ran anyway. Parsing here is also what
|
|
103
|
+
* makes `TRawInput` -> `TInput` (defaults, transforms) true at runtime.
|
|
104
|
+
*/
|
|
92
105
|
export declare function createEndpoint<TInput = unknown, TOutput = unknown, TStream extends boolean = false, TSchedule extends ZiteSchedule | undefined = undefined, TWebhook extends ZiteWebhook | undefined = undefined, TRawInput = TInput>(config: EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>): EndpointConfig<TInput, TOutput, TStream, TSchedule, TWebhook, TRawInput>;
|
|
@@ -31,6 +31,68 @@ class ZiteError extends Error {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
exports.ZiteError = ZiteError;
|
|
34
|
+
/** Injected by the runner on a platform-fired run — see `isPlatformTriggeredInput`. */
|
|
35
|
+
const PLATFORM_TRIGGER_KEYS = ["__cron", "__webhook"];
|
|
36
|
+
function isPlatformTriggeredInput(input) {
|
|
37
|
+
if (typeof input !== "object" || input === null)
|
|
38
|
+
return false;
|
|
39
|
+
return PLATFORM_TRIGGER_KEYS.some((key) => Object.prototype.hasOwnProperty.call(input, key));
|
|
40
|
+
}
|
|
41
|
+
function isIssueBearingError(error) {
|
|
42
|
+
return (typeof error === "object" &&
|
|
43
|
+
error !== null &&
|
|
44
|
+
Array.isArray(error.issues));
|
|
45
|
+
}
|
|
46
|
+
function describeIssues(error) {
|
|
47
|
+
return error.issues
|
|
48
|
+
.map((issue) => {
|
|
49
|
+
const path = issue.path.map((part) => String(part)).join(".");
|
|
50
|
+
return path ? `${path}: ${issue.message}` : issue.message;
|
|
51
|
+
})
|
|
52
|
+
.join("; ");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create an endpoint definition, parsing `input` against `inputSchema` before
|
|
56
|
+
* `execute` runs. Nothing downstream parses it — the worker calls `execute` on
|
|
57
|
+
* whatever this returns — so while this was a bare `return config` a required
|
|
58
|
+
* field could be missing and the endpoint ran anyway. Parsing here is also what
|
|
59
|
+
* makes `TRawInput` -> `TInput` (defaults, transforms) true at runtime.
|
|
60
|
+
*/
|
|
34
61
|
function createEndpoint(config) {
|
|
35
|
-
|
|
62
|
+
const { inputSchema, execute } = config;
|
|
63
|
+
const firesWithoutARequest = Boolean(config.schedule || config.webhook);
|
|
64
|
+
// An `inputSchema` that isn't a validator stays inert, as it was.
|
|
65
|
+
if (!inputSchema || typeof inputSchema.parse !== "function")
|
|
66
|
+
return config;
|
|
67
|
+
return {
|
|
68
|
+
...config,
|
|
69
|
+
validatesInput: true,
|
|
70
|
+
// `async` so a schema failure rejects: `execute` may be declared sync, and
|
|
71
|
+
// a sync throw would escape the caller's `.catch()`.
|
|
72
|
+
execute: async (params) => {
|
|
73
|
+
// Cron and webhook fires carry no request body, so a schema written for
|
|
74
|
+
// request callers doesn't describe them.
|
|
75
|
+
if (isPlatformTriggeredInput(params.input))
|
|
76
|
+
return execute(params);
|
|
77
|
+
// Same, for a fire with no input object to hold the sentinel: the runner
|
|
78
|
+
// only merges `__cron` into a plain object.
|
|
79
|
+
if (firesWithoutARequest && params.input === undefined) {
|
|
80
|
+
return execute(params);
|
|
81
|
+
}
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = inputSchema.parse(params.input);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new ZiteError({
|
|
88
|
+
code: "BAD_REQUEST",
|
|
89
|
+
message: isIssueBearingError(error)
|
|
90
|
+
? `Invalid input: ${describeIssues(error)}`
|
|
91
|
+
: `Invalid input: ${String(error?.message ?? error)}`,
|
|
92
|
+
userFacingMessage: "Some of the information sent with this request was missing or invalid.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return execute({ ...params, input: parsed });
|
|
96
|
+
},
|
|
97
|
+
};
|
|
36
98
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const vitest_1 = require("vitest");
|
|
4
|
+
const zod_1 = require("zod");
|
|
5
|
+
const index_js_1 = require("./index.js");
|
|
6
|
+
const index_js_2 = require("../runtime/index.js");
|
|
7
|
+
const requestContext = { user: { id: "u1", email: "a@b.c" } };
|
|
8
|
+
(0, vitest_1.afterEach)(() => {
|
|
9
|
+
vitest_1.vi.unstubAllGlobals();
|
|
10
|
+
});
|
|
11
|
+
(0, vitest_1.describe)("createEndpoint input validation", () => {
|
|
12
|
+
(0, vitest_1.it)("rejects input that does not satisfy inputSchema, without running execute", async () => {
|
|
13
|
+
const execute = vitest_1.vi.fn();
|
|
14
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
15
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
16
|
+
execute,
|
|
17
|
+
});
|
|
18
|
+
await (0, vitest_1.expect)(endpoint.execute({ input: {}, context: requestContext })).rejects.toBeInstanceOf(index_js_1.ZiteError);
|
|
19
|
+
(0, vitest_1.expect)(execute).not.toHaveBeenCalled();
|
|
20
|
+
});
|
|
21
|
+
(0, vitest_1.it)("surfaces the failure as BAD_REQUEST and names the offending field", async () => {
|
|
22
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
23
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
24
|
+
execute: async () => "unreachable",
|
|
25
|
+
});
|
|
26
|
+
await (0, vitest_1.expect)(endpoint.execute({ input: {}, context: requestContext })).rejects.toMatchObject({
|
|
27
|
+
code: "BAD_REQUEST",
|
|
28
|
+
message: vitest_1.expect.stringContaining("token"),
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)("rejects a call with no input at all", async () => {
|
|
32
|
+
const execute = vitest_1.vi.fn();
|
|
33
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
34
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
35
|
+
execute,
|
|
36
|
+
});
|
|
37
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
38
|
+
input: undefined,
|
|
39
|
+
context: requestContext,
|
|
40
|
+
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
41
|
+
(0, vitest_1.expect)(execute).not.toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
(0, vitest_1.it)("hands execute the parsed value, so schema defaults finally apply", async () => {
|
|
44
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
45
|
+
inputSchema: zod_1.z.object({
|
|
46
|
+
token: zod_1.z.string(),
|
|
47
|
+
limit: zod_1.z.number().default(10),
|
|
48
|
+
}),
|
|
49
|
+
execute: async ({ input }) => input,
|
|
50
|
+
});
|
|
51
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
52
|
+
input: { token: "abc" },
|
|
53
|
+
context: requestContext,
|
|
54
|
+
})).resolves.toEqual({ token: "abc", limit: 10 });
|
|
55
|
+
});
|
|
56
|
+
(0, vitest_1.it)("passes valid input through untouched", async () => {
|
|
57
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
58
|
+
inputSchema: zod_1.z.object({
|
|
59
|
+
token: zod_1.z.string().min(10),
|
|
60
|
+
action: zod_1.z.enum(["load", "accept"]),
|
|
61
|
+
}),
|
|
62
|
+
execute: async ({ input }) => input.action,
|
|
63
|
+
});
|
|
64
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
65
|
+
input: { token: "b69779ae-eeea", action: "load" },
|
|
66
|
+
context: requestContext,
|
|
67
|
+
})).resolves.toBe("load");
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)("declares that it validates, so the worker does not parse a second time", () => {
|
|
70
|
+
const withSchema = (0, index_js_1.createEndpoint)({
|
|
71
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string() }),
|
|
72
|
+
execute: async () => null,
|
|
73
|
+
});
|
|
74
|
+
const withoutSchema = (0, index_js_1.createEndpoint)({ execute: async () => null });
|
|
75
|
+
(0, vitest_1.expect)(withSchema.validatesInput).toBe(true);
|
|
76
|
+
(0, vitest_1.expect)(withoutSchema.validatesInput).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
(0, vitest_1.it)("leaves an endpoint without an inputSchema exactly as it was", async () => {
|
|
79
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
80
|
+
execute: async ({ input }) => input,
|
|
81
|
+
});
|
|
82
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
83
|
+
input: { anything: true },
|
|
84
|
+
context: requestContext,
|
|
85
|
+
})).resolves.toEqual({ anything: true });
|
|
86
|
+
});
|
|
87
|
+
(0, vitest_1.it)("preserves the stream handle for streaming endpoints", async () => {
|
|
88
|
+
const stream = { write: async () => { }, forward: async () => "" };
|
|
89
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
90
|
+
stream: true,
|
|
91
|
+
inputSchema: zod_1.z.object({ prompt: zod_1.z.string() }),
|
|
92
|
+
execute: async (params) => params.stream === stream,
|
|
93
|
+
});
|
|
94
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
95
|
+
input: { prompt: "hi" },
|
|
96
|
+
context: requestContext,
|
|
97
|
+
stream,
|
|
98
|
+
})).resolves.toBe(true);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
(0, vitest_1.describe)("platform-triggered runs", () => {
|
|
102
|
+
(0, vitest_1.it)("does not validate a cron fire, and leaves __cron readable", async () => {
|
|
103
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
104
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
105
|
+
schedule: {
|
|
106
|
+
scheduleType: "recurring",
|
|
107
|
+
schedule: { frequency: "hourly", interval: 6 },
|
|
108
|
+
timezone: "UTC",
|
|
109
|
+
},
|
|
110
|
+
execute: async ({ input }) => input,
|
|
111
|
+
});
|
|
112
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
113
|
+
input: { __cron: { scheduleId: "s1" } },
|
|
114
|
+
context: { user: null },
|
|
115
|
+
})).resolves.toEqual({ __cron: { scheduleId: "s1" } });
|
|
116
|
+
});
|
|
117
|
+
(0, vitest_1.it)("does not validate a scheduled fire that carries no input object", async () => {
|
|
118
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
119
|
+
inputSchema: zod_1.z.string(),
|
|
120
|
+
schedule: {
|
|
121
|
+
scheduleType: "recurring",
|
|
122
|
+
schedule: { frequency: "hourly", interval: 6 },
|
|
123
|
+
timezone: "UTC",
|
|
124
|
+
},
|
|
125
|
+
execute: async ({ input }) => input ?? "(no input)",
|
|
126
|
+
});
|
|
127
|
+
await (0, vitest_1.expect)(endpoint.execute({ input: undefined, context: { user: null } })).resolves.toBe("(no input)");
|
|
128
|
+
});
|
|
129
|
+
(0, vitest_1.it)("still validates an ordinary endpoint called with no input", async () => {
|
|
130
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
131
|
+
inputSchema: zod_1.z.string(),
|
|
132
|
+
execute: async ({ input }) => input,
|
|
133
|
+
});
|
|
134
|
+
await (0, vitest_1.expect)(endpoint.execute({ input: undefined, context: requestContext })).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
135
|
+
});
|
|
136
|
+
(0, vitest_1.it)("does not validate a webhook fire", async () => {
|
|
137
|
+
const endpoint = (0, index_js_1.createEndpoint)({
|
|
138
|
+
inputSchema: zod_1.z.object({ token: zod_1.z.string().min(10) }),
|
|
139
|
+
webhook: {},
|
|
140
|
+
execute: async ({ input }) => input,
|
|
141
|
+
});
|
|
142
|
+
await (0, vitest_1.expect)(endpoint.execute({
|
|
143
|
+
input: { __webhook: { headers: {} }, id: 7 },
|
|
144
|
+
context: { user: null },
|
|
145
|
+
})).resolves.toEqual({ __webhook: { headers: {} }, id: 7 });
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
/**
|
|
149
|
+
* The failure this was written for: an endpoint that looks a customer up by a
|
|
150
|
+
* token, called with no token. The undefined was dropped on the way to the
|
|
151
|
+
* database and the unfiltered lookup returned the first row of the table.
|
|
152
|
+
* Either guard alone stops it; they fail at different layers, so both are here.
|
|
153
|
+
*/
|
|
154
|
+
(0, vitest_1.describe)("regression: token lookup on an endpoint called without a token", () => {
|
|
155
|
+
function buildEndpoint(onMatch) {
|
|
156
|
+
const clients = (0, index_js_2.createTableClient)("Clients");
|
|
157
|
+
return (0, index_js_1.createEndpoint)({
|
|
158
|
+
inputSchema: zod_1.z.object({
|
|
159
|
+
token: zod_1.z.string().min(10),
|
|
160
|
+
action: zod_1.z.enum(["load", "accept"]),
|
|
161
|
+
}),
|
|
162
|
+
execute: async ({ input }) => {
|
|
163
|
+
const row = await clients.findOne({ filters: { token: input.token } });
|
|
164
|
+
if (!row)
|
|
165
|
+
return "invalid_token";
|
|
166
|
+
onMatch(row.nom);
|
|
167
|
+
return "ok";
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
(0, vitest_1.it)("stops at the schema, so the database is never reached", async () => {
|
|
172
|
+
const dispatched = [];
|
|
173
|
+
vitest_1.vi.stubGlobal("__wrapSdkCall", async (_i, _c, m) => {
|
|
174
|
+
dispatched.push(m);
|
|
175
|
+
return { id: "1", token: "", nom: "an unrelated customer" };
|
|
176
|
+
});
|
|
177
|
+
const matched = [];
|
|
178
|
+
await (0, vitest_1.expect)(buildEndpoint((nom) => matched.push(nom)).execute({
|
|
179
|
+
input: {},
|
|
180
|
+
context: requestContext,
|
|
181
|
+
})).rejects.toMatchObject({ code: "BAD_REQUEST" });
|
|
182
|
+
(0, vitest_1.expect)(dispatched).toEqual([]);
|
|
183
|
+
(0, vitest_1.expect)(matched).toEqual([]);
|
|
184
|
+
});
|
|
185
|
+
(0, vitest_1.it)("stops at the query too, if an undefined token reaches it another way", async () => {
|
|
186
|
+
const dispatched = [];
|
|
187
|
+
vitest_1.vi.stubGlobal("__wrapSdkCall", async (_i, _c, m) => {
|
|
188
|
+
dispatched.push(m);
|
|
189
|
+
return { id: "1", token: "", nom: "an unrelated customer" };
|
|
190
|
+
});
|
|
191
|
+
const warn = vitest_1.vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
192
|
+
const clients = (0, index_js_2.createTableClient)("Clients");
|
|
193
|
+
const token = undefined;
|
|
194
|
+
// Answers "no match" rather than throwing: `if (!row) return
|
|
195
|
+
// { status: 'invalid_token' }` is what the endpoint already does, so this
|
|
196
|
+
// makes the incident path correct without the app changing anything.
|
|
197
|
+
const row = await clients.findOne({ filters: { token } });
|
|
198
|
+
(0, vitest_1.expect)(row).toBeUndefined();
|
|
199
|
+
(0, vitest_1.expect)(dispatched).toEqual([]);
|
|
200
|
+
(0, vitest_1.expect)(warn).toHaveBeenCalled();
|
|
201
|
+
warn.mockRestore();
|
|
202
|
+
});
|
|
203
|
+
});
|
package/dist/cjs/bundle/index.js
CHANGED
|
@@ -94,6 +94,8 @@ const PREBUNDLED_LIBS = {
|
|
|
94
94
|
'intercom-client': '__intercom__.js',
|
|
95
95
|
'@google/generative-ai': '__gemini__.js',
|
|
96
96
|
'@elevenlabs/elevenlabs-js': '__elevenlabs__.js',
|
|
97
|
+
'resend': '__resend__.js',
|
|
98
|
+
'@clickhouse/client-web': '__clickhouse__.js',
|
|
97
99
|
};
|
|
98
100
|
exports.BASE_BUILD_OPTIONS = {
|
|
99
101
|
bundle: true,
|
|
@@ -8,6 +8,78 @@ exports.createAirtableClient = createAirtableClient;
|
|
|
8
8
|
exports.createEmailClient = createEmailClient;
|
|
9
9
|
const sdkCall_js_1 = require("../internal/sdkCall.js");
|
|
10
10
|
const DB_INTEGRATION_ID = "databases";
|
|
11
|
+
function isPlainObject(value) {
|
|
12
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Not thrown — the miss is the answer. Logged so the author can still find out
|
|
16
|
+
* their value was missing, since a silent null looks exactly like a legitimate
|
|
17
|
+
* no-match and would otherwise hide the bug that caused it.
|
|
18
|
+
*/
|
|
19
|
+
function warnEmptySelector(className, method, detail) {
|
|
20
|
+
console.warn(`${className}.${method}: ${detail} is undefined, so this query would have ` +
|
|
21
|
+
"run unnarrowed and returned an arbitrary record. Returning no match " +
|
|
22
|
+
"instead. Check the value before querying, or pass null to match " +
|
|
23
|
+
"records where the field is empty.");
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Whether a selector was written with nothing in it.
|
|
27
|
+
*
|
|
28
|
+
* `JSON.stringify` drops an undefined value, so `{ filters: { token: undefined } }`
|
|
29
|
+
* reaches base-runner as `{ filters: {} }` — a valid request for no filter.
|
|
30
|
+
* `findOne` answers that with the first row of the table, and the caller reads
|
|
31
|
+
* it back as a match.
|
|
32
|
+
*
|
|
33
|
+
* `findOne` treats it as "matched nothing" instead, which is what asking for
|
|
34
|
+
* `token === undefined` should mean. Callers already handle the miss —
|
|
35
|
+
* `if (!row) return { status: 'invalid_token' }` — so the shape that caused the
|
|
36
|
+
* incident becomes correct on its own, with no app change and nothing to break.
|
|
37
|
+
*
|
|
38
|
+
* **`findOne` only.** On `findAll`, "no filter" is a coherent request, and
|
|
39
|
+
* `filters: { status: maybeUndefined }` meaning "don't narrow on status" is an
|
|
40
|
+
* idiom that type-checks today (`RecordFilters` is a `?:` map, and apps build
|
|
41
|
+
* without `exactOptionalPropertyTypes`). It is also what Prisma does with
|
|
42
|
+
* `undefined` in a `where`. Only `findOne` has no sane reading, because an
|
|
43
|
+
* unnarrowed single-row query returns an arbitrary record rather than everything.
|
|
44
|
+
*
|
|
45
|
+
* A key the caller never wrote is untouched: `findOne({ filters: {} })` still
|
|
46
|
+
* means "no filter" and still returns the first row. Only a key written with
|
|
47
|
+
* nothing in it counts. (Unknown filter keys have the same consequence — see
|
|
48
|
+
* `RecordFilters` — but those are `keyof T` and caught at build time.)
|
|
49
|
+
*/
|
|
50
|
+
function selectorMatchesNothing(className, method, params) {
|
|
51
|
+
if (!params)
|
|
52
|
+
return false;
|
|
53
|
+
const { filters } = params;
|
|
54
|
+
if (isPlainObject(filters)) {
|
|
55
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
56
|
+
if (value === undefined) {
|
|
57
|
+
warnEmptySelector(className, method, `filter "${key}"`);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// `{ field: { contains: undefined } }` collapses to `{ field: {} }` — no
|
|
61
|
+
// condition on that field at all.
|
|
62
|
+
if (isPlainObject(value)) {
|
|
63
|
+
for (const [operator, operand] of Object.entries(value)) {
|
|
64
|
+
if (operand === undefined) {
|
|
65
|
+
warnEmptySelector(className, method, `filter "${key}.${operator}"`);
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// An undefined `id` leaves the query with no selector at all. Only counts
|
|
73
|
+
// when nothing else narrows it, so `findOne({ id: maybeId, filters: {...} })`
|
|
74
|
+
// still runs on the filters.
|
|
75
|
+
const wroteId = Object.prototype.hasOwnProperty.call(params, "id");
|
|
76
|
+
const hasFilters = isPlainObject(filters) && Object.keys(filters).length > 0;
|
|
77
|
+
if (wroteId && params.id === undefined && !hasFilters) {
|
|
78
|
+
warnEmptySelector(className, method, "`id`");
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
11
83
|
function getBaseId() {
|
|
12
84
|
try {
|
|
13
85
|
return (globalThis.__ZITE_EXECUTION_CONFIG__?.baseId ??
|
|
@@ -34,16 +106,22 @@ function resolveTableId(className) {
|
|
|
34
106
|
}
|
|
35
107
|
function createTableClient(className) {
|
|
36
108
|
return {
|
|
37
|
-
findAll: (params) =>
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
109
|
+
findAll: async (params) => {
|
|
110
|
+
return (0, sdkCall_js_1.getSdkCall)()(DB_INTEGRATION_ID, className, "findAll", {
|
|
111
|
+
baseId: getBaseId(),
|
|
112
|
+
tableId: resolveTableId(className),
|
|
113
|
+
...params,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
findOne: async (params) => {
|
|
117
|
+
if (selectorMatchesNothing(className, "findOne", params))
|
|
118
|
+
return undefined;
|
|
119
|
+
return (0, sdkCall_js_1.getSdkCall)()(DB_INTEGRATION_ID, className, "findOne", {
|
|
120
|
+
baseId: getBaseId(),
|
|
121
|
+
tableId: resolveTableId(className),
|
|
122
|
+
...params,
|
|
123
|
+
});
|
|
124
|
+
},
|
|
47
125
|
create: (params) => (0, sdkCall_js_1.getSdkCall)()(DB_INTEGRATION_ID, className, "create", {
|
|
48
126
|
baseId: getBaseId(),
|
|
49
127
|
tableId: resolveTableId(className),
|
|
@@ -74,10 +152,12 @@ function createSqlClient() {
|
|
|
74
152
|
}
|
|
75
153
|
function createAuthClient() {
|
|
76
154
|
return {
|
|
77
|
-
findAllUsers: (options) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
155
|
+
findAllUsers: async (options) => {
|
|
156
|
+
return (0, sdkCall_js_1.getSdkCall)()(DB_INTEGRATION_ID, "Auth", "findAllUsers", {
|
|
157
|
+
baseId: getBaseId(),
|
|
158
|
+
...options,
|
|
159
|
+
});
|
|
160
|
+
},
|
|
81
161
|
updateUserProfile: (userId, profile) => (0, sdkCall_js_1.getSdkCall)()(DB_INTEGRATION_ID, "Auth", "updateUserProfile", {
|
|
82
162
|
baseId: getBaseId(),
|
|
83
163
|
userId,
|
|
@@ -87,14 +167,20 @@ function createAuthClient() {
|
|
|
87
167
|
}
|
|
88
168
|
function createAirtableClient(integrationId, className, implicitParams) {
|
|
89
169
|
return {
|
|
90
|
-
findAll: (params) =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
170
|
+
findAll: async (params) => {
|
|
171
|
+
return (0, sdkCall_js_1.getSdkCall)()(integrationId, className, "findAll", {
|
|
172
|
+
...implicitParams,
|
|
173
|
+
...params,
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
findOne: async (params) => {
|
|
177
|
+
if (selectorMatchesNothing(className, "findOne", params))
|
|
178
|
+
return undefined;
|
|
179
|
+
return (0, sdkCall_js_1.getSdkCall)()(integrationId, className, "findOne", {
|
|
180
|
+
...implicitParams,
|
|
181
|
+
...params,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
98
184
|
create: (params) => (0, sdkCall_js_1.getSdkCall)()(integrationId, className, "create", {
|
|
99
185
|
...implicitParams,
|
|
100
186
|
...params,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|