bunderstack 0.21.0 → 0.22.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/CHANGELOG.md +38 -0
- package/README.md +24 -8
- package/dist/api/builder.d.ts +2 -2
- package/dist/api/builder.js +1 -1
- package/dist/api/builder.js.map +1 -1
- package/dist/api/context.d.ts +1 -1
- package/dist/api/context.d.ts.map +1 -1
- package/dist/api/context.js.map +1 -1
- package/dist/auth.d.ts +8 -0
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +14 -0
- package/dist/auth.js.map +1 -1
- package/dist/backend-internals.d.ts +32 -0
- package/dist/backend-internals.d.ts.map +1 -0
- package/dist/backend-internals.js +2 -0
- package/dist/backend-internals.js.map +1 -0
- package/dist/backend.d.ts +26 -0
- package/dist/backend.d.ts.map +1 -0
- package/dist/backend.js +53 -0
- package/dist/backend.js.map +1 -0
- package/dist/blueprint-generator.d.ts.map +1 -1
- package/dist/blueprint-generator.js +41 -51
- package/dist/blueprint-generator.js.map +1 -1
- package/dist/config.d.ts +0 -6
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js.map +1 -1
- package/dist/database/adapter.d.ts +10 -3
- package/dist/database/adapter.d.ts.map +1 -1
- package/dist/database/adapter.js.map +1 -1
- package/dist/database/bun-sql.d.ts.map +1 -1
- package/dist/database/bun-sql.js +1 -3
- package/dist/database/bun-sql.js.map +1 -1
- package/dist/database/bun-sqlite.d.ts +8 -0
- package/dist/database/bun-sqlite.d.ts.map +1 -0
- package/dist/database/bun-sqlite.js +56 -0
- package/dist/database/bun-sqlite.js.map +1 -0
- package/dist/database/libsql.d.ts.map +1 -1
- package/dist/database/libsql.js +25 -3
- package/dist/database/libsql.js.map +1 -1
- package/dist/database/pglite.d.ts.map +1 -1
- package/dist/database/pglite.js +25 -4
- package/dist/database/pglite.js.map +1 -1
- package/dist/database/postgres-js.d.ts.map +1 -1
- package/dist/database/postgres-js.js +1 -3
- package/dist/database/postgres-js.js.map +1 -1
- package/dist/db.d.ts +1 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +5 -3
- package/dist/db.js.map +1 -1
- package/dist/email.d.ts +3 -1
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +9 -2
- package/dist/email.js.map +1 -1
- package/dist/env.d.ts +1 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +1 -4
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +3 -108
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -484
- package/dist/index.js.map +1 -1
- package/dist/jobs/define.d.ts +1 -1
- package/dist/jobs/define.d.ts.map +1 -1
- package/dist/jobs/define.js.map +1 -1
- package/dist/jobs/index.js +1 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/jobs/queue.d.ts +1 -1
- package/dist/jobs/queue.d.ts.map +1 -1
- package/dist/jobs/queue.js +1 -2
- package/dist/jobs/queue.js.map +1 -1
- package/dist/jobs/worker.d.ts +9 -0
- package/dist/jobs/worker.d.ts.map +1 -1
- package/dist/jobs/worker.js +32 -3
- package/dist/jobs/worker.js.map +1 -1
- package/dist/provision-internals.d.ts +1 -1
- package/dist/provision-internals.js +1 -1
- package/dist/provision-internals.js.map +1 -1
- package/dist/provision.d.ts +2 -0
- package/dist/provision.d.ts.map +1 -1
- package/dist/provision.js +24 -3
- package/dist/provision.js.map +1 -1
- package/dist/query/infer.d.ts +1 -1
- package/dist/query/infer.d.ts.map +1 -1
- package/dist/query/infer.js.map +1 -1
- package/dist/runtime.d.ts +149 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +522 -0
- package/dist/runtime.js.map +1 -0
- package/dist/schema-export-pg.js +1 -1
- package/dist/schema-export-pg.js.map +1 -1
- package/dist/start/index.d.ts +3 -3
- package/dist/start/index.d.ts.map +1 -1
- package/dist/start/index.js +3 -3
- package/dist/start/index.js.map +1 -1
- package/dist/storage/background.d.ts +3 -0
- package/dist/storage/background.d.ts.map +1 -0
- package/dist/storage/background.js +3 -0
- package/dist/storage/background.js.map +1 -0
- package/dist/testing/auth.d.ts +57 -0
- package/dist/testing/auth.d.ts.map +1 -0
- package/dist/testing/auth.js +51 -0
- package/dist/testing/auth.js.map +1 -0
- package/dist/testing/client.d.ts +6 -0
- package/dist/testing/client.d.ts.map +1 -0
- package/dist/testing/client.js +9 -0
- package/dist/testing/client.js.map +1 -0
- package/dist/testing/database.d.ts +6 -0
- package/dist/testing/database.d.ts.map +1 -0
- package/dist/testing/database.js +8 -0
- package/dist/testing/database.js.map +1 -0
- package/dist/testing/email.d.ts +15 -0
- package/dist/testing/email.d.ts.map +1 -0
- package/dist/testing/email.js +32 -0
- package/dist/testing/email.js.map +1 -0
- package/dist/testing/fixture.d.ts +33 -0
- package/dist/testing/fixture.d.ts.map +1 -0
- package/dist/testing/fixture.js +125 -0
- package/dist/testing/fixture.js.map +1 -0
- package/dist/testing/jobs.d.ts +32 -0
- package/dist/testing/jobs.d.ts.map +1 -0
- package/dist/testing/jobs.js +67 -0
- package/dist/testing/jobs.js.map +1 -0
- package/dist/testing/storage.d.ts +7 -0
- package/dist/testing/storage.d.ts.map +1 -0
- package/dist/testing/storage.js +29 -0
- package/dist/testing/storage.js.map +1 -0
- package/dist/testing.d.ts +9 -26
- package/dist/testing.d.ts.map +1 -1
- package/dist/testing.js +3 -9
- package/dist/testing.js.map +1 -1
- package/llms.txt +43 -13
- package/package.json +23 -15
- package/skills/creating-bunderstack-apps/references/application-structure.md +16 -12
- package/skills/creating-bunderstack-apps/references/runtime-integrations.md +4 -1
- package/skills/migrating-to-bunderstack/SKILL.md +7 -6
- package/skills/migrating-to-bunderstack/references/audit-checklist.md +4 -4
- package/skills/migrating-to-bunderstack/references/runtime-replacements.md +55 -49
package/dist/start/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
-
import { type AnyBunderstackApp, type
|
|
2
|
+
import { type AnyBunderstackApp, type BunderstackClient } from '../query/index.js';
|
|
3
3
|
export { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
4
4
|
export type BunderstackStartOptions = {
|
|
5
5
|
/** API mount point. Defaults to '/api'. */
|
|
@@ -9,7 +9,7 @@ export type BunderstackStartOptions = {
|
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
11
|
* One-call client setup for TanStack Start apps: SSR-aware fetch, sensible
|
|
12
|
-
* QueryClient defaults, and a
|
|
12
|
+
* QueryClient defaults, and a typed query client whose procedures and buckets are
|
|
13
13
|
* inferred from the server app type.
|
|
14
14
|
*
|
|
15
15
|
* @example
|
|
@@ -20,7 +20,7 @@ export type BunderstackStartOptions = {
|
|
|
20
20
|
*/
|
|
21
21
|
export declare function bunderstackStart<TApp extends AnyBunderstackApp>(options?: BunderstackStartOptions): {
|
|
22
22
|
createQueryClient: () => QueryClient;
|
|
23
|
-
createApi: (queryClient: QueryClient) =>
|
|
23
|
+
createApi: (queryClient: QueryClient) => BunderstackClient<TApp>;
|
|
24
24
|
};
|
|
25
25
|
type StartRequestContext = {
|
|
26
26
|
request: Request;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAA;AAGvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,MAAM,MAAM,uBAAuB,GAAG;IACpC,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,wDAAwD;IACxD,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,CAAA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,SAAS,iBAAiB,EAC7D,OAAO,GAAE,uBAA4B;;6BAUV,WAAW,KAAG,iBAAiB,CAAC,IAAI,CAAC;EAOjE;AAED,KAAK,mBAAmB,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,CAAA;AAC/C,KAAK,YAAY,GAAG,CAAC,GAAG,EAAE,mBAAmB,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;AAEnE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE;IACrC,OAAO,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC7C,GAAG;IACF,GAAG,EAAE,YAAY,CAAA;IACjB,IAAI,EAAE,YAAY,CAAA;IAClB,KAAK,EAAE,YAAY,CAAA;IACnB,MAAM,EAAE,YAAY,CAAA;CACrB,CAGA;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,IAAI,CAAC,EAAE,MAAM,CAAA;CACd,CAAA;AAED,KAAK,UAAU,GAAG;IAChB,IAAI,EAAE;QACJ,GAAG,EAAE;YACH,UAAU,EAAE,CAAC,IAAI,EAAE;gBACjB,OAAO,EAAE,OAAO,CAAA;aACjB,KAAK,OAAO,CAAC;gBAAE,IAAI,EAAE,WAAW,GAAG,IAAI,CAAA;aAAE,GAAG,IAAI,CAAC,CAAA;SACnD,CAAA;KACF,CAAA;CACF,CAAA;AAED,iFAAiF;AACjF,wBAAsB,cAAc,CAClC,GAAG,EAAE,UAAU,EACf,OAAO,EAAE,OAAO,GACf,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAG7B"}
|
package/dist/start/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { QueryClient } from '@tanstack/react-query';
|
|
2
|
-
import {
|
|
2
|
+
import { createClient, } from '../query/index.js';
|
|
3
3
|
import { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
4
4
|
export { createIsomorphicFetch } from './isomorphic-fetch.js';
|
|
5
5
|
/**
|
|
6
6
|
* One-call client setup for TanStack Start apps: SSR-aware fetch, sensible
|
|
7
|
-
* QueryClient defaults, and a
|
|
7
|
+
* QueryClient defaults, and a typed query client whose procedures and buckets are
|
|
8
8
|
* inferred from the server app type.
|
|
9
9
|
*
|
|
10
10
|
* @example
|
|
@@ -21,7 +21,7 @@ export function bunderstackStart(options = {}) {
|
|
|
21
21
|
queries: { staleTime: options.staleTime ?? 30_000 },
|
|
22
22
|
},
|
|
23
23
|
}),
|
|
24
|
-
createApi: (queryClient) =>
|
|
24
|
+
createApi: (queryClient) => createClient({
|
|
25
25
|
queryClient,
|
|
26
26
|
fetch: isoFetch,
|
|
27
27
|
baseUrl: options.baseUrl,
|
package/dist/start/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/start/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAA;AAEnD,OAAO,EACL,YAAY,GAGb,MAAM,gBAAgB,CAAA;AACvB,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAE1D,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAA;AAS1D;;;;;;;;;;GAUG;AACH,MAAM,UAAU,gBAAgB,CAC9B,UAAmC,EAAE;IAErC,MAAM,QAAQ,GAAG,qBAAqB,EAAE,CAAA;IACxC,OAAO;QACL,iBAAiB,EAAE,GAAG,EAAE,CACtB,IAAI,WAAW,CAAC;YACd,cAAc,EAAE;gBACd,OAAO,EAAE,EAAE,SAAS,EAAE,OAAO,CAAC,SAAS,IAAI,MAAM,EAAE;aACpD;SACF,CAAC;QACJ,SAAS,EAAE,CAAC,WAAwB,EAA2B,EAAE,CAC/D,YAAY,CAAO;YACjB,WAAW;YACX,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,OAAO,CAAC,OAAO;SACzB,CAAC;KACL,CAAA;AACH,CAAC;AAKD;;;;;;;;GAQG;AACH,MAAM,UAAU,iBAAiB,CAAC,GAEjC;IAMC,MAAM,MAAM,GAAiB,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAA;IAClE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,CAAA;AACrE,CAAC;AAoBD,iFAAiF;AACjF,MAAM,CAAC,KAAK,UAAU,cAAc,CAClC,GAAe,EACf,OAAgB;IAEhB,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAA;IAC3E,OAAO,OAAO,EAAE,IAAI,IAAI,IAAI,CAAA;AAC9B,CAAC","sourcesContent":["import { QueryClient } from '@tanstack/react-query'\n\nimport {\n createClient,\n type AnyBunderstackApp,\n type BunderstackClient,\n} from '../query/index'\nimport { createIsomorphicFetch } from './isomorphic-fetch'\n\nexport { createIsomorphicFetch } from './isomorphic-fetch'\n\nexport type BunderstackStartOptions = {\n /** API mount point. Defaults to '/api'. */\n baseUrl?: string\n /** Default query staleTime (ms). Defaults to 30_000. */\n staleTime?: number\n}\n\n/**\n * One-call client setup for TanStack Start apps: SSR-aware fetch, sensible\n * QueryClient defaults, and a typed query client whose procedures and buckets are\n * inferred from the server app type.\n *\n * @example\n * // src/api.ts — NOT src/client.ts, which is a reserved TanStack Start\n * // entry-point name (it would replace the hydration entry).\n * import type { App } from './bunderstack'\n * export const { createQueryClient, createApi } = bunderstackStart<App>()\n */\nexport function bunderstackStart<TApp extends AnyBunderstackApp>(\n options: BunderstackStartOptions = {},\n) {\n const isoFetch = createIsomorphicFetch()\n return {\n createQueryClient: () =>\n new QueryClient({\n defaultOptions: {\n queries: { staleTime: options.staleTime ?? 30_000 },\n },\n }),\n createApi: (queryClient: QueryClient): BunderstackClient<TApp> =>\n createClient<TApp>({\n queryClient,\n fetch: isoFetch,\n baseUrl: options.baseUrl,\n }),\n }\n}\n\ntype StartRequestContext = { request: Request }\ntype StartHandler = (ctx: StartRequestContext) => Promise<Response>\n\n/**\n * Handlers object for the catch-all API file route.\n *\n * @example\n * // src/routes/api/$.tsx\n * export const Route = createFileRoute('/api/$')({\n * server: { handlers: createApiHandlers(app) },\n * })\n */\nexport function createApiHandlers(app: {\n handler: (req: Request) => Promise<Response>\n}): {\n GET: StartHandler\n POST: StartHandler\n PATCH: StartHandler\n DELETE: StartHandler\n} {\n const handle: StartHandler = ({ request }) => app.handler(request)\n return { GET: handle, POST: handle, PATCH: handle, DELETE: handle }\n}\n\nexport type SessionUser = {\n id: string\n email: string\n name: string\n image?: string | null\n role?: string\n}\n\ntype SessionApp = {\n auth: {\n api: {\n getSession: (opts: {\n headers: Headers\n }) => Promise<{ user: SessionUser | null } | null>\n }\n }\n}\n\n/** Resolve the BetterAuth session user for an incoming request (server-side). */\nexport async function getSessionUser(\n app: SessionApp,\n request: Request,\n): Promise<SessionUser | null> {\n const session = await app.auth.api.getSession({ headers: request.headers })\n return session?.user ?? null\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background.d.ts","sourceRoot":"","sources":["../../src/storage/background.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,sBAAsB,8BAA8B,CAAA;AACjE,eAAO,MAAM,sBAAsB,cAAc,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"background.js","sourceRoot":"","sources":["../../src/storage/background.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,sBAAsB,GAAG,2BAA2B,CAAA;AACjE,MAAM,CAAC,MAAM,sBAAsB,GAAG,WAAW,CAAA","sourcesContent":["export const STORAGE_SWEEP_JOB_NAME = 'bunderstack:storage-sweep'\nexport const STORAGE_SWEEP_SCHEDULE = '0 4 * * *'\n"]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export type TestUser = {
|
|
2
|
+
id: string;
|
|
3
|
+
email: string;
|
|
4
|
+
name: string;
|
|
5
|
+
emailVerified?: boolean;
|
|
6
|
+
image?: string | null;
|
|
7
|
+
createdAt?: string | Date;
|
|
8
|
+
updatedAt?: string | Date;
|
|
9
|
+
};
|
|
10
|
+
export type TestIdentity = {
|
|
11
|
+
user: TestUser;
|
|
12
|
+
headers: Headers;
|
|
13
|
+
};
|
|
14
|
+
export declare class TestAuthError extends Error {
|
|
15
|
+
readonly status: number;
|
|
16
|
+
readonly body: unknown;
|
|
17
|
+
constructor(status: number, body: unknown);
|
|
18
|
+
}
|
|
19
|
+
export type SignUpEmailInput = {
|
|
20
|
+
email: string;
|
|
21
|
+
name: string;
|
|
22
|
+
password?: string;
|
|
23
|
+
};
|
|
24
|
+
export type TestAuth = {
|
|
25
|
+
signUpEmail(input: SignUpEmailInput): Promise<TestIdentity>;
|
|
26
|
+
mockSession<TUser extends TestUser>(user: TUser): TestIdentity & {
|
|
27
|
+
user: TUser;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
export type AuthSessionResolverLike = {
|
|
31
|
+
api: {
|
|
32
|
+
getSession: (opts: {
|
|
33
|
+
headers: Headers;
|
|
34
|
+
}) => Promise<unknown>;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
export type BunderstackAppLike = {
|
|
38
|
+
auth: unknown;
|
|
39
|
+
};
|
|
40
|
+
export declare function mockAuthSession<TUser extends {
|
|
41
|
+
id: string;
|
|
42
|
+
email: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
role?: string;
|
|
45
|
+
}>(app: BunderstackAppLike, resolver: (opts: {
|
|
46
|
+
headers: Headers;
|
|
47
|
+
}) => Promise<{
|
|
48
|
+
user: TUser;
|
|
49
|
+
session?: {
|
|
50
|
+
activeOrganizationId?: string | null;
|
|
51
|
+
} | null;
|
|
52
|
+
} | null>): void;
|
|
53
|
+
export declare function createTestAuth(app: {
|
|
54
|
+
handler(request: Request): Promise<Response>;
|
|
55
|
+
auth: unknown;
|
|
56
|
+
}): TestAuth;
|
|
57
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/testing/auth.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,GAAG;IACrB,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,QAAQ,CAAA;IACd,OAAO,EAAE,OAAO,CAAA;CACjB,CAAA;AAED,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;gBAEV,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO;CAM1C;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAA;CAClB,CAAA;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,WAAW,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IAC3D,WAAW,CAAC,KAAK,SAAS,QAAQ,EAChC,IAAI,EAAE,KAAK,GACV,YAAY,GAAG;QAAE,IAAI,EAAE,KAAK,CAAA;KAAE,CAAA;CAClC,CAAA;AAED,MAAM,MAAM,uBAAuB,GAAG;IACpC,GAAG,EAAE;QACH,UAAU,EAAE,CAAC,IAAI,EAAE;YAAE,OAAO,EAAE,OAAO,CAAA;SAAE,KAAK,OAAO,CAAC,OAAO,CAAC,CAAA;KAC7D,CAAA;CACF,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,OAAO,CAAA;CACd,CAAA;AAED,wBAAgB,eAAe,CAC7B,KAAK,SAAS;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,EAEzE,GAAG,EAAE,kBAAkB,EACvB,QAAQ,EAAE,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,KAAK,OAAO,CAAC;IAChD,IAAI,EAAE,KAAK,CAAA;IACX,OAAO,CAAC,EAAE;QAAE,oBAAoB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAA;CAC1D,GAAG,IAAI,CAAC,GACR,IAAI,CAON;AAWD,wBAAgB,cAAc,CAAC,GAAG,EAAE;IAClC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC5C,IAAI,EAAE,OAAO,CAAA;CACd,GAAG,QAAQ,CAyBX"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export class TestAuthError extends Error {
|
|
2
|
+
status;
|
|
3
|
+
body;
|
|
4
|
+
constructor(status, body) {
|
|
5
|
+
super(`Better Auth request failed (${status})`);
|
|
6
|
+
this.name = 'TestAuthError';
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.body = body;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export function mockAuthSession(app, resolver) {
|
|
12
|
+
const auth = app.auth;
|
|
13
|
+
if (auth?.api) {
|
|
14
|
+
auth.api.getSession = resolver;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function cookieHeaders(response) {
|
|
18
|
+
const values = response.headers.getSetCookie();
|
|
19
|
+
const cookie = values
|
|
20
|
+
.map((value) => value.split(';', 1)[0])
|
|
21
|
+
.filter((value) => Boolean(value))
|
|
22
|
+
.join('; ');
|
|
23
|
+
return new Headers(cookie ? { cookie } : undefined);
|
|
24
|
+
}
|
|
25
|
+
export function createTestAuth(app) {
|
|
26
|
+
return {
|
|
27
|
+
async signUpEmail(input) {
|
|
28
|
+
const response = await app.handler(new Request('http://bunderstack.test/api/auth/sign-up/email', {
|
|
29
|
+
method: 'POST',
|
|
30
|
+
headers: { 'content-type': 'application/json' },
|
|
31
|
+
body: JSON.stringify({
|
|
32
|
+
email: input.email,
|
|
33
|
+
name: input.name,
|
|
34
|
+
password: input.password ?? 'password-123',
|
|
35
|
+
}),
|
|
36
|
+
}));
|
|
37
|
+
const body = await response.json().catch(() => null);
|
|
38
|
+
if (!response.ok)
|
|
39
|
+
throw new TestAuthError(response.status, body);
|
|
40
|
+
const user = body?.user;
|
|
41
|
+
if (!user)
|
|
42
|
+
throw new TestAuthError(response.status, body);
|
|
43
|
+
return { user, headers: cookieHeaders(response) };
|
|
44
|
+
},
|
|
45
|
+
mockSession(user) {
|
|
46
|
+
mockAuthSession(app, async () => ({ user, session: null }));
|
|
47
|
+
return { user, headers: new Headers() };
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/testing/auth.ts"],"names":[],"mappings":"AAeA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,MAAM,CAAQ;IACd,IAAI,CAAS;IAEtB,YAAY,MAAc,EAAE,IAAa;QACvC,KAAK,CAAC,+BAA+B,MAAM,GAAG,CAAC,CAAA;QAC/C,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAyBD,MAAM,UAAU,eAAe,CAG7B,GAAuB,EACvB,QAGS;IAET,MAAM,IAAI,GAAG,GAAG,CAAC,IAA0C,CAAA;IAC3D,IAAI,IAAI,EAAE,GAAG,EAAE,CAAC;QACd,IAAI,CAAC,GAAG,CAAC,UAAU,GAAG,QAEA,CAAA;IACxB,CAAC;AACH,CAAC;AAED,SAAS,aAAa,CAAC,QAAkB;IACvC,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,CAAA;IAC9C,MAAM,MAAM,GAAG,MAAM;SAClB,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;SACtC,MAAM,CAAC,CAAC,KAAK,EAAmB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;SAClD,IAAI,CAAC,IAAI,CAAC,CAAA;IACb,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;AACrD,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAG9B;IACC,OAAO;QACL,KAAK,CAAC,WAAW,CAAC,KAAK;YACrB,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,OAAO,CAChC,IAAI,OAAO,CAAC,gDAAgD,EAAE;gBAC5D,MAAM,EAAE,MAAM;gBACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;gBAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC;oBACnB,KAAK,EAAE,KAAK,CAAC,KAAK;oBAClB,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,QAAQ,EAAE,KAAK,CAAC,QAAQ,IAAI,cAAc;iBAC3C,CAAC;aACH,CAAC,CACH,CAAA;YACD,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAA;YACpD,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YAChE,MAAM,IAAI,GAAI,IAAmC,EAAE,IAAI,CAAA;YACvD,IAAI,CAAC,IAAI;gBAAE,MAAM,IAAI,aAAa,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAA;YACzD,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAA;QACnD,CAAC;QACD,WAAW,CAAC,IAAI;YACd,eAAe,CAAC,GAAG,EAAE,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;YAC3D,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,OAAO,EAAE,EAAE,CAAA;QACzC,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["export type TestUser = {\n id: string\n email: string\n name: string\n emailVerified?: boolean\n image?: string | null\n createdAt?: string | Date\n updatedAt?: string | Date\n}\n\nexport type TestIdentity = {\n user: TestUser\n headers: Headers\n}\n\nexport class TestAuthError extends Error {\n readonly status: number\n readonly body: unknown\n\n constructor(status: number, body: unknown) {\n super(`Better Auth request failed (${status})`)\n this.name = 'TestAuthError'\n this.status = status\n this.body = body\n }\n}\n\nexport type SignUpEmailInput = {\n email: string\n name: string\n password?: string\n}\n\nexport type TestAuth = {\n signUpEmail(input: SignUpEmailInput): Promise<TestIdentity>\n mockSession<TUser extends TestUser>(\n user: TUser,\n ): TestIdentity & { user: TUser }\n}\n\nexport type AuthSessionResolverLike = {\n api: {\n getSession: (opts: { headers: Headers }) => Promise<unknown>\n }\n}\n\nexport type BunderstackAppLike = {\n auth: unknown\n}\n\nexport function mockAuthSession<\n TUser extends { id: string; email: string; name?: string; role?: string },\n>(\n app: BunderstackAppLike,\n resolver: (opts: { headers: Headers }) => Promise<{\n user: TUser\n session?: { activeOrganizationId?: string | null } | null\n } | null>,\n): void {\n const auth = app.auth as unknown as AuthSessionResolverLike\n if (auth?.api) {\n auth.api.getSession = resolver as unknown as (opts: {\n headers: Headers\n }) => Promise<unknown>\n }\n}\n\nfunction cookieHeaders(response: Response): Headers {\n const values = response.headers.getSetCookie()\n const cookie = values\n .map((value) => value.split(';', 1)[0])\n .filter((value): value is string => Boolean(value))\n .join('; ')\n return new Headers(cookie ? { cookie } : undefined)\n}\n\nexport function createTestAuth(app: {\n handler(request: Request): Promise<Response>\n auth: unknown\n}): TestAuth {\n return {\n async signUpEmail(input) {\n const response = await app.handler(\n new Request('http://bunderstack.test/api/auth/sign-up/email', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({\n email: input.email,\n name: input.name,\n password: input.password ?? 'password-123',\n }),\n }),\n )\n const body = await response.json().catch(() => null)\n if (!response.ok) throw new TestAuthError(response.status, body)\n const user = (body as { user?: TestUser } | null)?.user\n if (!user) throw new TestAuthError(response.status, body)\n return { user, headers: cookieHeaders(response) }\n },\n mockSession(user) {\n mockAuthSession(app, async () => ({ user, session: null }))\n return { user, headers: new Headers() }\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { AnyBunderstackApp, BunderstackClient } from '../client/rpc-client.js';
|
|
2
|
+
import type { TestIdentity } from './auth.js';
|
|
3
|
+
export declare function testClient<TApp extends AnyBunderstackApp>(app: TApp & {
|
|
4
|
+
handler(request: Request): Promise<Response>;
|
|
5
|
+
}, identity?: TestIdentity): BunderstackClient<TApp>;
|
|
6
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/testing/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AAI1C,wBAAgB,UAAU,CAAC,IAAI,SAAS,iBAAiB,EACvD,GAAG,EAAE,IAAI,GAAG;IAAE,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAAE,EAC5D,QAAQ,CAAC,EAAE,YAAY,GACtB,iBAAiB,CAAC,IAAI,CAAC,CAMzB"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createClient } from '../client/rpc-client.js';
|
|
2
|
+
export function testClient(app, identity) {
|
|
3
|
+
return createClient({
|
|
4
|
+
baseUrl: 'http://bunderstack.test/api',
|
|
5
|
+
fetch: (input, init) => app.handler(new Request(input, init)),
|
|
6
|
+
headers: identity?.headers,
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/testing/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAA;AAEnD,MAAM,UAAU,UAAU,CACxB,GAA4D,EAC5D,QAAuB;IAEvB,OAAO,YAAY,CAAO;QACxB,OAAO,EAAE,6BAA6B;QACtC,KAAK,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAC7D,OAAO,EAAE,QAAQ,EAAE,OAAO;KAC3B,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type { AnyBunderstackApp, BunderstackClient } from '../client/rpc-client'\nimport type { TestIdentity } from './auth'\n\nimport { createClient } from '../client/rpc-client'\n\nexport function testClient<TApp extends AnyBunderstackApp>(\n app: TApp & { handler(request: Request): Promise<Response> },\n identity?: TestIdentity,\n): BunderstackClient<TApp> {\n return createClient<TApp>({\n baseUrl: 'http://bunderstack.test/api',\n fetch: (input, init) => app.handler(new Request(input, init)),\n headers: identity?.headers,\n })\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DatabaseAdapter, TestDatabaseStrategy, TestDatabaseTarget } from '../database/adapter.js';
|
|
2
|
+
export declare function createTestDatabaseTarget(adapter: DatabaseAdapter, options: {
|
|
3
|
+
mode: 'memory' | 'temporary';
|
|
4
|
+
strategy?: TestDatabaseStrategy;
|
|
5
|
+
}): Promise<TestDatabaseTarget>;
|
|
6
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,oBAAoB,EACpB,kBAAkB,EACnB,MAAM,qBAAqB,CAAA;AAE5B,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,eAAe,EACxB,OAAO,EAAE;IACP,IAAI,EAAE,QAAQ,GAAG,WAAW,CAAA;IAC5B,QAAQ,CAAC,EAAE,oBAAoB,CAAA;CAChC,GACA,OAAO,CAAC,kBAAkB,CAAC,CAQ7B"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export async function createTestDatabaseTarget(adapter, options) {
|
|
2
|
+
const strategy = options.strategy ?? adapter.testing;
|
|
3
|
+
if (!strategy) {
|
|
4
|
+
throw new Error(`[bunderstack] ${adapter.driver} requires an explicit test database strategy`);
|
|
5
|
+
}
|
|
6
|
+
return strategy.createTarget({ mode: options.mode });
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../src/testing/database.ts"],"names":[],"mappings":"AAMA,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,OAAwB,EACxB,OAGC;IAED,MAAM,QAAQ,GAAG,OAAO,CAAC,QAAQ,IAAI,OAAO,CAAC,OAAO,CAAA;IACpD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CACb,iBAAiB,OAAO,CAAC,MAAM,8CAA8C,CAC9E,CAAA;IACH,CAAC;IACD,OAAO,QAAQ,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAA;AACtD,CAAC","sourcesContent":["import type {\n DatabaseAdapter,\n TestDatabaseStrategy,\n TestDatabaseTarget,\n} from '../database/adapter'\n\nexport async function createTestDatabaseTarget(\n adapter: DatabaseAdapter,\n options: {\n mode: 'memory' | 'temporary'\n strategy?: TestDatabaseStrategy\n },\n): Promise<TestDatabaseTarget> {\n const strategy = options.strategy ?? adapter.testing\n if (!strategy) {\n throw new Error(\n `[bunderstack] ${adapter.driver} requires an explicit test database strategy`,\n )\n }\n return strategy.createTarget({ mode: options.mode })\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EmailAdapter, EmailMessage } from '../email.js';
|
|
2
|
+
export type CapturedEmail = Omit<EmailMessage, 'to' | 'cc' | 'bcc'> & {
|
|
3
|
+
from: string;
|
|
4
|
+
to: string[];
|
|
5
|
+
cc?: string[];
|
|
6
|
+
bcc?: string[];
|
|
7
|
+
};
|
|
8
|
+
export type TestEmail = {
|
|
9
|
+
readonly sent: readonly Readonly<CapturedEmail>[];
|
|
10
|
+
};
|
|
11
|
+
export declare function createTestEmail(): {
|
|
12
|
+
adapter: EmailAdapter;
|
|
13
|
+
email: TestEmail;
|
|
14
|
+
};
|
|
15
|
+
//# sourceMappingURL=email.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.d.ts","sourceRoot":"","sources":["../../src/testing/email.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAE1D,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,EAAE,IAAI,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ,EAAE,EAAE,MAAM,EAAE,CAAA;IACZ,EAAE,CAAC,EAAE,MAAM,EAAE,CAAA;IACb,GAAG,CAAC,EAAE,MAAM,EAAE,CAAA;CACf,CAAA;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAA;CAClD,CAAA;AAKD,wBAAgB,eAAe,IAAI;IACjC,OAAO,EAAE,YAAY,CAAA;IACrB,KAAK,EAAE,SAAS,CAAA;CACjB,CAiCA"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const addresses = (value) => value === undefined ? undefined : Array.isArray(value) ? [...value] : [value];
|
|
2
|
+
export function createTestEmail() {
|
|
3
|
+
const messages = [];
|
|
4
|
+
return {
|
|
5
|
+
adapter: {
|
|
6
|
+
async send(message) {
|
|
7
|
+
messages.push({
|
|
8
|
+
...message,
|
|
9
|
+
to: addresses(message.to) ?? [],
|
|
10
|
+
cc: addresses(message.cc),
|
|
11
|
+
bcc: addresses(message.bcc),
|
|
12
|
+
});
|
|
13
|
+
return { id: `test-email-${messages.length}` };
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
email: {
|
|
17
|
+
get sent() {
|
|
18
|
+
return Object.freeze(messages.map((message) => Object.freeze({
|
|
19
|
+
...message,
|
|
20
|
+
to: Object.freeze([...message.to]),
|
|
21
|
+
cc: message.cc
|
|
22
|
+
? Object.freeze([...message.cc])
|
|
23
|
+
: undefined,
|
|
24
|
+
bcc: message.bcc
|
|
25
|
+
? Object.freeze([...message.bcc])
|
|
26
|
+
: undefined,
|
|
27
|
+
})));
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=email.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"email.js","sourceRoot":"","sources":["../../src/testing/email.ts"],"names":[],"mappings":"AAaA,MAAM,SAAS,GAAG,CAAC,KAAoC,EAAE,EAAE,CACzD,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAA;AAE/E,MAAM,UAAU,eAAe;IAI7B,MAAM,QAAQ,GAAoB,EAAE,CAAA;IACpC,OAAO;QACL,OAAO,EAAE;YACP,KAAK,CAAC,IAAI,CAAC,OAAO;gBAChB,QAAQ,CAAC,IAAI,CAAC;oBACZ,GAAG,OAAO;oBACV,EAAE,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE;oBAC/B,EAAE,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE,CAAC;oBACzB,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC;iBAC5B,CAAC,CAAA;gBACF,OAAO,EAAE,EAAE,EAAE,cAAc,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAA;YAChD,CAAC;SACF;QACD,KAAK,EAAE;YACL,IAAI,IAAI;gBACN,OAAO,MAAM,CAAC,MAAM,CAClB,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CACvB,MAAM,CAAC,MAAM,CAAC;oBACZ,GAAG,OAAO;oBACV,EAAE,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAwB;oBACzD,EAAE,EAAE,OAAO,CAAC,EAAE;wBACZ,CAAC,CAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,EAAE,CAAC,CAAyB;wBACzD,CAAC,CAAC,SAAS;oBACb,GAAG,EAAE,OAAO,CAAC,GAAG;wBACd,CAAC,CAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAyB;wBAC1D,CAAC,CAAC,SAAS;iBACd,CAAC,CACH,CACF,CAAA;YACH,CAAC;SACF;KACF,CAAA;AACH,CAAC","sourcesContent":["import type { EmailAdapter, EmailMessage } from '../email'\n\nexport type CapturedEmail = Omit<EmailMessage, 'to' | 'cc' | 'bcc'> & {\n from: string\n to: string[]\n cc?: string[]\n bcc?: string[]\n}\n\nexport type TestEmail = {\n readonly sent: readonly Readonly<CapturedEmail>[]\n}\n\nconst addresses = (value: string | string[] | undefined) =>\n value === undefined ? undefined : Array.isArray(value) ? [...value] : [value]\n\nexport function createTestEmail(): {\n adapter: EmailAdapter\n email: TestEmail\n} {\n const messages: CapturedEmail[] = []\n return {\n adapter: {\n async send(message) {\n messages.push({\n ...message,\n to: addresses(message.to) ?? [],\n cc: addresses(message.cc),\n bcc: addresses(message.bcc),\n })\n return { id: `test-email-${messages.length}` }\n },\n },\n email: {\n get sent() {\n return Object.freeze(\n messages.map((message) =>\n Object.freeze({\n ...message,\n to: Object.freeze([...message.to]) as unknown as string[],\n cc: message.cc\n ? (Object.freeze([...message.cc]) as unknown as string[])\n : undefined,\n bcc: message.bcc\n ? (Object.freeze([...message.bcc]) as unknown as string[])\n : undefined,\n }),\n ),\n )\n },\n },\n }\n}\n"]}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { BunderstackBackend } from '../backend.js';
|
|
2
|
+
import type { AnyBunderstackApp, BunderstackClient } from '../client/rpc-client.js';
|
|
3
|
+
import type { TestDatabaseStrategy } from '../database/adapter.js';
|
|
4
|
+
import type { TestSchemaMode } from '../provision.js';
|
|
5
|
+
import type { TestAuth, TestIdentity } from './auth.js';
|
|
6
|
+
import type { TestEmail } from './email.js';
|
|
7
|
+
import type { TestStorage } from './storage.js';
|
|
8
|
+
import { type TestJobs } from './jobs.js';
|
|
9
|
+
export type TestOptions = {
|
|
10
|
+
env?: Record<string, string | undefined>;
|
|
11
|
+
database?: {
|
|
12
|
+
mode?: 'memory' | 'temporary';
|
|
13
|
+
schema?: TestSchemaMode;
|
|
14
|
+
strategy?: TestDatabaseStrategy;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
export type TestFixture<TApp> = AsyncDisposable & {
|
|
18
|
+
readonly app: TApp;
|
|
19
|
+
readonly auth: TestAuth;
|
|
20
|
+
readonly email: TestEmail;
|
|
21
|
+
readonly jobs: TestJobs;
|
|
22
|
+
readonly storage: TestStorage;
|
|
23
|
+
client(identity?: TestIdentity): TApp extends AnyBunderstackApp ? BunderstackClient<TApp> : never;
|
|
24
|
+
close(): Promise<void>;
|
|
25
|
+
};
|
|
26
|
+
type TestableApp = {
|
|
27
|
+
auth: unknown;
|
|
28
|
+
handler(request: Request): Promise<Response>;
|
|
29
|
+
close(): Promise<void>;
|
|
30
|
+
};
|
|
31
|
+
export declare function createTestApp<TApp extends TestableApp>(backend: BunderstackBackend<TApp>, options?: TestOptions): Promise<TestFixture<TApp>>;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=fixture.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.d.ts","sourceRoot":"","sources":["../../src/testing/fixture.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAA;AACpD,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAChF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAA;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAW5C,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,QAAQ,CAAA;AAGtD,MAAM,MAAM,WAAW,GAAG;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAA;IACxC,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,QAAQ,GAAG,WAAW,CAAA;QAC7B,MAAM,CAAC,EAAE,cAAc,CAAA;QACvB,QAAQ,CAAC,EAAE,oBAAoB,CAAA;KAChC,CAAA;CACF,CAAA;AAED,MAAM,MAAM,WAAW,CAAC,IAAI,IAAI,eAAe,GAAG;IAChD,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAA;IAClB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAA;IACzB,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,WAAW,CAAA;IAC7B,MAAM,CACJ,QAAQ,CAAC,EAAE,YAAY,GACtB,IAAI,SAAS,iBAAiB,GAAG,iBAAiB,CAAC,IAAI,CAAC,GAAG,KAAK,CAAA;IACnE,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAED,KAAK,WAAW,GAAG;IACjB,IAAI,EAAE,OAAO,CAAA;IACb,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;IAC5C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB,CAAA;AAQD,wBAAsB,aAAa,CAAC,IAAI,SAAS,WAAW,EAC1D,OAAO,EAAE,kBAAkB,CAAC,IAAI,CAAC,EACjC,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAmH5B"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { BACKEND_INTERNALS, } from '../backend-internals.js';
|
|
5
|
+
import { provisionForTest } from '../provision.js';
|
|
6
|
+
import { createTestAuth } from './auth.js';
|
|
7
|
+
import { testClient } from './client.js';
|
|
8
|
+
import { createTestDatabaseTarget } from './database.js';
|
|
9
|
+
import { createTestEmail } from './email.js';
|
|
10
|
+
import { createTestJobs } from './jobs.js';
|
|
11
|
+
import { createTestStorage, resolveTestBuckets } from './storage.js';
|
|
12
|
+
const defaultTestEnv = {
|
|
13
|
+
NODE_ENV: 'test',
|
|
14
|
+
AUTH_SECRET: 'bunderstack-test-secret',
|
|
15
|
+
BUNDERSTACK_ROLE: 'web',
|
|
16
|
+
};
|
|
17
|
+
export async function createTestApp(backend, options = {}) {
|
|
18
|
+
const internals = backend[BACKEND_INTERNALS];
|
|
19
|
+
const storageRoot = await mkdtemp(join(tmpdir(), 'bunderstack-storage-'));
|
|
20
|
+
const resolvedStorage = resolveTestBuckets(internals.declaration.config.storage, storageRoot);
|
|
21
|
+
const { adapter: emailAdapter, email } = createTestEmail();
|
|
22
|
+
const storage = createTestStorage(resolvedStorage);
|
|
23
|
+
let target;
|
|
24
|
+
try {
|
|
25
|
+
target = await createTestDatabaseTarget(internals.declaration.config.database.adapter, {
|
|
26
|
+
mode: options.database?.mode ?? 'memory',
|
|
27
|
+
strategy: options.database?.strategy,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (cause) {
|
|
31
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
32
|
+
throw cause;
|
|
33
|
+
}
|
|
34
|
+
let app;
|
|
35
|
+
let testingHandle;
|
|
36
|
+
try {
|
|
37
|
+
app = await internals.start({ ...defaultTestEnv, ...options.env }, {
|
|
38
|
+
database: target.connection,
|
|
39
|
+
resolvedStorage,
|
|
40
|
+
emailAdapter,
|
|
41
|
+
forceMemoryRealtime: true,
|
|
42
|
+
backgroundAutoStart: false,
|
|
43
|
+
captureTestingHandle: (handle) => {
|
|
44
|
+
testingHandle = handle;
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
await provisionForTest(app, options.database?.schema ?? 'auto');
|
|
48
|
+
}
|
|
49
|
+
catch (cause) {
|
|
50
|
+
const cleanupErrors = [];
|
|
51
|
+
if (app) {
|
|
52
|
+
try {
|
|
53
|
+
await app.close();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
cleanupErrors.push(error);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
await target[Symbol.asyncDispose]();
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
cleanupErrors.push(error);
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
cleanupErrors.push(error);
|
|
70
|
+
}
|
|
71
|
+
if (cleanupErrors.length > 0) {
|
|
72
|
+
throw new AggregateError([cause, ...cleanupErrors], '[bunderstack] test fixture setup and cleanup failed');
|
|
73
|
+
}
|
|
74
|
+
throw cause;
|
|
75
|
+
}
|
|
76
|
+
if (!testingHandle) {
|
|
77
|
+
await app.close();
|
|
78
|
+
await target[Symbol.asyncDispose]();
|
|
79
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
80
|
+
throw new Error('[bunderstack] runtime did not provide test controls');
|
|
81
|
+
}
|
|
82
|
+
const auth = createTestAuth(app);
|
|
83
|
+
const jobs = createTestJobs(testingHandle);
|
|
84
|
+
let closePromise;
|
|
85
|
+
const close = () => {
|
|
86
|
+
closePromise ??= (async () => {
|
|
87
|
+
const errors = [];
|
|
88
|
+
try {
|
|
89
|
+
await app.close();
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
errors.push(error);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
await target[Symbol.asyncDispose]();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
errors.push(error);
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
errors.push(error);
|
|
105
|
+
}
|
|
106
|
+
if (errors.length === 1)
|
|
107
|
+
throw errors[0];
|
|
108
|
+
if (errors.length > 1) {
|
|
109
|
+
throw new AggregateError(errors, '[bunderstack] fixture cleanup failed');
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
return closePromise;
|
|
113
|
+
};
|
|
114
|
+
return {
|
|
115
|
+
app,
|
|
116
|
+
auth,
|
|
117
|
+
email,
|
|
118
|
+
jobs,
|
|
119
|
+
storage,
|
|
120
|
+
client: (identity) => testClient(app, identity),
|
|
121
|
+
close,
|
|
122
|
+
[Symbol.asyncDispose]: close,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=fixture.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixture.js","sourceRoot":"","sources":["../../src/testing/fixture.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdtemp, rm } from 'node:fs/promises'\nimport { tmpdir } from 'node:os'\nimport { join } from 'node:path'\n\nimport type { BunderstackBackend } from '../backend'\nimport type { AnyBunderstackApp, BunderstackClient } from '../client/rpc-client'\nimport type { TestDatabaseStrategy } from '../database/adapter'\nimport type { TestSchemaMode } from '../provision'\nimport type { TestAuth, TestIdentity } from './auth'\nimport type { TestEmail } from './email'\nimport type { TestStorage } from './storage'\n\nimport {\n BACKEND_INTERNALS,\n type RuntimeTestingHandle,\n} from '../backend-internals'\nimport { provisionForTest } from '../provision'\nimport { createTestAuth } from './auth'\nimport { testClient } from './client'\nimport { createTestDatabaseTarget } from './database'\nimport { createTestEmail } from './email'\nimport { createTestJobs, type TestJobs } from './jobs'\nimport { createTestStorage, resolveTestBuckets } from './storage'\n\nexport type TestOptions = {\n env?: Record<string, string | undefined>\n database?: {\n mode?: 'memory' | 'temporary'\n schema?: TestSchemaMode\n strategy?: TestDatabaseStrategy\n }\n}\n\nexport type TestFixture<TApp> = AsyncDisposable & {\n readonly app: TApp\n readonly auth: TestAuth\n readonly email: TestEmail\n readonly jobs: TestJobs\n readonly storage: TestStorage\n client(\n identity?: TestIdentity,\n ): TApp extends AnyBunderstackApp ? BunderstackClient<TApp> : never\n close(): Promise<void>\n}\n\ntype TestableApp = {\n auth: unknown\n handler(request: Request): Promise<Response>\n close(): Promise<void>\n}\n\nconst defaultTestEnv = {\n NODE_ENV: 'test',\n AUTH_SECRET: 'bunderstack-test-secret',\n BUNDERSTACK_ROLE: 'web',\n} satisfies Record<string, string | undefined>\n\nexport async function createTestApp<TApp extends TestableApp>(\n backend: BunderstackBackend<TApp>,\n options: TestOptions = {},\n): Promise<TestFixture<TApp>> {\n const internals = backend[BACKEND_INTERNALS]\n const storageRoot = await mkdtemp(join(tmpdir(), 'bunderstack-storage-'))\n const resolvedStorage = resolveTestBuckets(\n internals.declaration.config.storage,\n storageRoot,\n )\n const { adapter: emailAdapter, email } = createTestEmail()\n const storage = createTestStorage(resolvedStorage)\n\n let target\n try {\n target = await createTestDatabaseTarget(\n internals.declaration.config.database.adapter,\n {\n mode: options.database?.mode ?? 'memory',\n strategy: options.database?.strategy,\n },\n )\n } catch (cause) {\n await rm(storageRoot, { recursive: true, force: true })\n throw cause\n }\n\n let app: TApp | undefined\n let testingHandle: RuntimeTestingHandle | undefined\n try {\n app = await internals.start(\n { ...defaultTestEnv, ...options.env },\n {\n database: target.connection,\n resolvedStorage,\n emailAdapter,\n forceMemoryRealtime: true,\n backgroundAutoStart: false,\n captureTestingHandle: (handle) => {\n testingHandle = handle\n },\n },\n )\n await provisionForTest(app as object, options.database?.schema ?? 'auto')\n } catch (cause) {\n const cleanupErrors: unknown[] = []\n if (app) {\n try {\n await app.close()\n } catch (error) {\n cleanupErrors.push(error)\n }\n }\n try {\n await target[Symbol.asyncDispose]()\n } catch (error) {\n cleanupErrors.push(error)\n }\n try {\n await rm(storageRoot, { recursive: true, force: true })\n } catch (error) {\n cleanupErrors.push(error)\n }\n if (cleanupErrors.length > 0) {\n throw new AggregateError(\n [cause, ...cleanupErrors],\n '[bunderstack] test fixture setup and cleanup failed',\n )\n }\n throw cause\n }\n\n if (!testingHandle) {\n await app.close()\n await target[Symbol.asyncDispose]()\n await rm(storageRoot, { recursive: true, force: true })\n throw new Error('[bunderstack] runtime did not provide test controls')\n }\n\n const auth = createTestAuth(app)\n const jobs = createTestJobs(testingHandle)\n let closePromise: Promise<void> | undefined\n const close = () => {\n closePromise ??= (async () => {\n const errors: unknown[] = []\n try {\n await app.close()\n } catch (error) {\n errors.push(error)\n }\n try {\n await target[Symbol.asyncDispose]()\n } catch (error) {\n errors.push(error)\n }\n try {\n await rm(storageRoot, { recursive: true, force: true })\n } catch (error) {\n errors.push(error)\n }\n if (errors.length === 1) throw errors[0]\n if (errors.length > 1) {\n throw new AggregateError(errors, '[bunderstack] fixture cleanup failed')\n }\n })()\n return closePromise\n }\n\n return {\n app,\n auth,\n email,\n jobs,\n storage,\n client: (identity) => testClient(app as never, identity) as never,\n close,\n [Symbol.asyncDispose]: close,\n }\n}\n"]}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { RuntimeJobFailure, RuntimeTestingHandle } from '../backend-internals.js';
|
|
2
|
+
export type JobRunReport = {
|
|
3
|
+
ticks: number;
|
|
4
|
+
claimed: number;
|
|
5
|
+
ran: number;
|
|
6
|
+
failed: number;
|
|
7
|
+
remainingRunnable: number;
|
|
8
|
+
};
|
|
9
|
+
export type RunNextOptions = {
|
|
10
|
+
now?: Date | number;
|
|
11
|
+
};
|
|
12
|
+
export type RunUntilIdleOptions = RunNextOptions & {
|
|
13
|
+
maxTicks?: number;
|
|
14
|
+
failOnJobError?: boolean;
|
|
15
|
+
};
|
|
16
|
+
export declare class TestJobsError extends Error {
|
|
17
|
+
readonly report: JobRunReport;
|
|
18
|
+
readonly failures: RuntimeJobFailure[];
|
|
19
|
+
readonly name = "TestJobsError";
|
|
20
|
+
constructor(report: JobRunReport, failures: RuntimeJobFailure[]);
|
|
21
|
+
}
|
|
22
|
+
export declare class TestJobsConvergenceError extends Error {
|
|
23
|
+
readonly report: JobRunReport;
|
|
24
|
+
readonly name = "TestJobsConvergenceError";
|
|
25
|
+
constructor(report: JobRunReport);
|
|
26
|
+
}
|
|
27
|
+
export type TestJobs = {
|
|
28
|
+
runNext(options?: RunNextOptions): Promise<JobRunReport>;
|
|
29
|
+
runUntilIdle(options?: RunUntilIdleOptions): Promise<JobRunReport>;
|
|
30
|
+
};
|
|
31
|
+
export declare function createTestJobs(handle: RuntimeTestingHandle): TestJobs;
|
|
32
|
+
//# sourceMappingURL=jobs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/testing/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,oBAAoB,EACrB,MAAM,sBAAsB,CAAA;AAE7B,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,iBAAiB,EAAE,MAAM,CAAA;CAC1B,CAAA;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,GAAG,CAAC,EAAE,IAAI,GAAG,MAAM,CAAA;CAAE,CAAA;AAEpD,MAAM,MAAM,mBAAmB,GAAG,cAAc,GAAG;IACjD,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,OAAO,CAAA;CACzB,CAAA;AAED,qBAAa,aAAc,SAAQ,KAAK;IAIpC,QAAQ,CAAC,MAAM,EAAE,YAAY;IAC7B,QAAQ,CAAC,QAAQ,EAAE,iBAAiB,EAAE;IAJxC,SAAkB,IAAI,mBAAkB;gBAG7B,MAAM,EAAE,YAAY,EACpB,QAAQ,EAAE,iBAAiB,EAAE;CAMzC;AAED,qBAAa,wBAAyB,SAAQ,KAAK;IAGrC,QAAQ,CAAC,MAAM,EAAE,YAAY;IAFzC,SAAkB,IAAI,8BAA6B;gBAE9B,MAAM,EAAE,YAAY;CAK1C;AAED,MAAM,MAAM,QAAQ,GAAG;IACrB,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;IACxD,YAAY,CAAC,OAAO,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CAAA;CACnE,CAAA;AAMD,wBAAgB,cAAc,CAAC,MAAM,EAAE,oBAAoB,GAAG,QAAQ,CAiDrE"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
export class TestJobsError extends Error {
|
|
2
|
+
report;
|
|
3
|
+
failures;
|
|
4
|
+
name = 'TestJobsError';
|
|
5
|
+
constructor(report, failures) {
|
|
6
|
+
super(`[bunderstack] ${failures.length} background job${failures.length === 1 ? '' : 's'} failed: ${failures.map((failure) => `${failure.name}: ${failure.lastError ?? 'unknown error'}`).join('; ')}`);
|
|
7
|
+
this.report = report;
|
|
8
|
+
this.failures = failures;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
export class TestJobsConvergenceError extends Error {
|
|
12
|
+
report;
|
|
13
|
+
name = 'TestJobsConvergenceError';
|
|
14
|
+
constructor(report) {
|
|
15
|
+
super(`[bunderstack] background jobs did not become idle after ${report.ticks} ticks (${report.remainingRunnable} still runnable)`);
|
|
16
|
+
this.report = report;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
function timestamp(value) {
|
|
20
|
+
return value === undefined ? Date.now() : new Date(value).getTime();
|
|
21
|
+
}
|
|
22
|
+
export function createTestJobs(handle) {
|
|
23
|
+
return {
|
|
24
|
+
async runNext(options = {}) {
|
|
25
|
+
const now = timestamp(options.now);
|
|
26
|
+
const tick = await handle.tick(now);
|
|
27
|
+
const inspection = await handle.inspect(now);
|
|
28
|
+
return {
|
|
29
|
+
ticks: 1,
|
|
30
|
+
...tick,
|
|
31
|
+
remainingRunnable: inspection.runnable,
|
|
32
|
+
};
|
|
33
|
+
},
|
|
34
|
+
async runUntilIdle(options = {}) {
|
|
35
|
+
const now = timestamp(options.now);
|
|
36
|
+
const maxTicks = options.maxTicks ?? 100;
|
|
37
|
+
if (!Number.isInteger(maxTicks) || maxTicks <= 0) {
|
|
38
|
+
throw new TypeError('[bunderstack] maxTicks must be a positive integer');
|
|
39
|
+
}
|
|
40
|
+
const report = {
|
|
41
|
+
ticks: 0,
|
|
42
|
+
claimed: 0,
|
|
43
|
+
ran: 0,
|
|
44
|
+
failed: 0,
|
|
45
|
+
remainingRunnable: 0,
|
|
46
|
+
};
|
|
47
|
+
let inspection;
|
|
48
|
+
do {
|
|
49
|
+
const tick = await handle.tick(now);
|
|
50
|
+
report.ticks++;
|
|
51
|
+
report.claimed += tick.claimed;
|
|
52
|
+
report.ran += tick.ran;
|
|
53
|
+
report.failed += tick.failed;
|
|
54
|
+
inspection = await handle.inspect(now);
|
|
55
|
+
report.remainingRunnable = inspection.runnable;
|
|
56
|
+
if (inspection.runnable > 0 && report.ticks >= maxTicks) {
|
|
57
|
+
throw new TestJobsConvergenceError(report);
|
|
58
|
+
}
|
|
59
|
+
} while (inspection.runnable > 0);
|
|
60
|
+
if ((options.failOnJobError ?? true) && inspection.failed.length > 0) {
|
|
61
|
+
throw new TestJobsError(report, inspection.failed);
|
|
62
|
+
}
|
|
63
|
+
return report;
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=jobs.js.map
|