bunderstack 0.21.1 → 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.
Files changed (133) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +14 -10
  3. package/dist/api/builder.d.ts +2 -2
  4. package/dist/api/builder.js +1 -1
  5. package/dist/api/builder.js.map +1 -1
  6. package/dist/api/context.d.ts +1 -1
  7. package/dist/api/context.d.ts.map +1 -1
  8. package/dist/api/context.js.map +1 -1
  9. package/dist/auth.d.ts +8 -0
  10. package/dist/auth.d.ts.map +1 -1
  11. package/dist/auth.js +14 -0
  12. package/dist/auth.js.map +1 -1
  13. package/dist/backend-internals.d.ts +32 -0
  14. package/dist/backend-internals.d.ts.map +1 -0
  15. package/dist/backend-internals.js +2 -0
  16. package/dist/backend-internals.js.map +1 -0
  17. package/dist/backend.d.ts +26 -0
  18. package/dist/backend.d.ts.map +1 -0
  19. package/dist/backend.js +53 -0
  20. package/dist/backend.js.map +1 -0
  21. package/dist/blueprint-generator.d.ts.map +1 -1
  22. package/dist/blueprint-generator.js +41 -51
  23. package/dist/blueprint-generator.js.map +1 -1
  24. package/dist/config.d.ts +0 -6
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js.map +1 -1
  27. package/dist/database/adapter.d.ts +10 -3
  28. package/dist/database/adapter.d.ts.map +1 -1
  29. package/dist/database/adapter.js.map +1 -1
  30. package/dist/database/bun-sql.d.ts.map +1 -1
  31. package/dist/database/bun-sql.js +1 -3
  32. package/dist/database/bun-sql.js.map +1 -1
  33. package/dist/database/bun-sqlite.d.ts +8 -0
  34. package/dist/database/bun-sqlite.d.ts.map +1 -0
  35. package/dist/database/bun-sqlite.js +56 -0
  36. package/dist/database/bun-sqlite.js.map +1 -0
  37. package/dist/database/libsql.d.ts.map +1 -1
  38. package/dist/database/libsql.js +25 -3
  39. package/dist/database/libsql.js.map +1 -1
  40. package/dist/database/pglite.d.ts.map +1 -1
  41. package/dist/database/pglite.js +25 -4
  42. package/dist/database/pglite.js.map +1 -1
  43. package/dist/database/postgres-js.d.ts.map +1 -1
  44. package/dist/database/postgres-js.js +1 -3
  45. package/dist/database/postgres-js.js.map +1 -1
  46. package/dist/db.d.ts +1 -2
  47. package/dist/db.d.ts.map +1 -1
  48. package/dist/db.js +5 -3
  49. package/dist/db.js.map +1 -1
  50. package/dist/email.d.ts +3 -1
  51. package/dist/email.d.ts.map +1 -1
  52. package/dist/email.js +9 -2
  53. package/dist/email.js.map +1 -1
  54. package/dist/env.d.ts +1 -1
  55. package/dist/env.d.ts.map +1 -1
  56. package/dist/env.js +1 -4
  57. package/dist/env.js.map +1 -1
  58. package/dist/index.d.ts +3 -108
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +1 -484
  61. package/dist/index.js.map +1 -1
  62. package/dist/jobs/define.d.ts +1 -1
  63. package/dist/jobs/define.d.ts.map +1 -1
  64. package/dist/jobs/define.js.map +1 -1
  65. package/dist/jobs/index.js +1 -1
  66. package/dist/jobs/index.js.map +1 -1
  67. package/dist/jobs/queue.d.ts +1 -1
  68. package/dist/jobs/queue.d.ts.map +1 -1
  69. package/dist/jobs/queue.js +1 -2
  70. package/dist/jobs/queue.js.map +1 -1
  71. package/dist/jobs/worker.d.ts +9 -0
  72. package/dist/jobs/worker.d.ts.map +1 -1
  73. package/dist/jobs/worker.js +32 -3
  74. package/dist/jobs/worker.js.map +1 -1
  75. package/dist/provision-internals.d.ts +1 -1
  76. package/dist/provision-internals.js +1 -1
  77. package/dist/provision-internals.js.map +1 -1
  78. package/dist/provision.d.ts +2 -0
  79. package/dist/provision.d.ts.map +1 -1
  80. package/dist/provision.js +24 -3
  81. package/dist/provision.js.map +1 -1
  82. package/dist/query/infer.d.ts +1 -1
  83. package/dist/query/infer.d.ts.map +1 -1
  84. package/dist/query/infer.js.map +1 -1
  85. package/dist/runtime.d.ts +149 -0
  86. package/dist/runtime.d.ts.map +1 -0
  87. package/dist/runtime.js +522 -0
  88. package/dist/runtime.js.map +1 -0
  89. package/dist/schema-export-pg.js +1 -1
  90. package/dist/schema-export-pg.js.map +1 -1
  91. package/dist/storage/background.d.ts +3 -0
  92. package/dist/storage/background.d.ts.map +1 -0
  93. package/dist/storage/background.js +3 -0
  94. package/dist/storage/background.js.map +1 -0
  95. package/dist/testing/auth.d.ts +57 -0
  96. package/dist/testing/auth.d.ts.map +1 -0
  97. package/dist/testing/auth.js +51 -0
  98. package/dist/testing/auth.js.map +1 -0
  99. package/dist/testing/client.d.ts +6 -0
  100. package/dist/testing/client.d.ts.map +1 -0
  101. package/dist/testing/client.js +9 -0
  102. package/dist/testing/client.js.map +1 -0
  103. package/dist/testing/database.d.ts +6 -0
  104. package/dist/testing/database.d.ts.map +1 -0
  105. package/dist/testing/database.js +8 -0
  106. package/dist/testing/database.js.map +1 -0
  107. package/dist/testing/email.d.ts +15 -0
  108. package/dist/testing/email.d.ts.map +1 -0
  109. package/dist/testing/email.js +32 -0
  110. package/dist/testing/email.js.map +1 -0
  111. package/dist/testing/fixture.d.ts +33 -0
  112. package/dist/testing/fixture.d.ts.map +1 -0
  113. package/dist/testing/fixture.js +125 -0
  114. package/dist/testing/fixture.js.map +1 -0
  115. package/dist/testing/jobs.d.ts +32 -0
  116. package/dist/testing/jobs.d.ts.map +1 -0
  117. package/dist/testing/jobs.js +67 -0
  118. package/dist/testing/jobs.js.map +1 -0
  119. package/dist/testing/storage.d.ts +7 -0
  120. package/dist/testing/storage.d.ts.map +1 -0
  121. package/dist/testing/storage.js +29 -0
  122. package/dist/testing/storage.js.map +1 -0
  123. package/dist/testing.d.ts +9 -26
  124. package/dist/testing.d.ts.map +1 -1
  125. package/dist/testing.js +3 -9
  126. package/dist/testing.js.map +1 -1
  127. package/llms.txt +43 -13
  128. package/package.json +23 -15
  129. package/skills/creating-bunderstack-apps/references/application-structure.md +16 -12
  130. package/skills/creating-bunderstack-apps/references/runtime-integrations.md +4 -1
  131. package/skills/migrating-to-bunderstack/SKILL.md +7 -6
  132. package/skills/migrating-to-bunderstack/references/audit-checklist.md +4 -4
  133. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +55 -49
@@ -0,0 +1,3 @@
1
+ export declare const STORAGE_SWEEP_JOB_NAME = "bunderstack:storage-sweep";
2
+ export declare const STORAGE_SWEEP_SCHEDULE = "0 4 * * *";
3
+ //# sourceMappingURL=background.d.ts.map
@@ -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,3 @@
1
+ export const STORAGE_SWEEP_JOB_NAME = 'bunderstack:storage-sweep';
2
+ export const STORAGE_SWEEP_SCHEDULE = '0 4 * * *';
3
+ //# sourceMappingURL=background.js.map
@@ -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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/testing/jobs.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n RuntimeJobFailure,\n RuntimeTestingHandle,\n} from '../backend-internals'\n\nexport type JobRunReport = {\n ticks: number\n claimed: number\n ran: number\n failed: number\n remainingRunnable: number\n}\n\nexport type RunNextOptions = { now?: Date | number }\n\nexport type RunUntilIdleOptions = RunNextOptions & {\n maxTicks?: number\n failOnJobError?: boolean\n}\n\nexport class TestJobsError extends Error {\n override readonly name = 'TestJobsError'\n\n constructor(\n readonly report: JobRunReport,\n readonly failures: RuntimeJobFailure[],\n ) {\n super(\n `[bunderstack] ${failures.length} background job${failures.length === 1 ? '' : 's'} failed: ${failures.map((failure) => `${failure.name}: ${failure.lastError ?? 'unknown error'}`).join('; ')}`,\n )\n }\n}\n\nexport class TestJobsConvergenceError extends Error {\n override readonly name = 'TestJobsConvergenceError'\n\n constructor(readonly report: JobRunReport) {\n super(\n `[bunderstack] background jobs did not become idle after ${report.ticks} ticks (${report.remainingRunnable} still runnable)`,\n )\n }\n}\n\nexport type TestJobs = {\n runNext(options?: RunNextOptions): Promise<JobRunReport>\n runUntilIdle(options?: RunUntilIdleOptions): Promise<JobRunReport>\n}\n\nfunction timestamp(value: Date | number | undefined): number {\n return value === undefined ? Date.now() : new Date(value).getTime()\n}\n\nexport function createTestJobs(handle: RuntimeTestingHandle): TestJobs {\n return {\n async runNext(options = {}) {\n const now = timestamp(options.now)\n const tick = await handle.tick(now)\n const inspection = await handle.inspect(now)\n return {\n ticks: 1,\n ...tick,\n remainingRunnable: inspection.runnable,\n }\n },\n\n async runUntilIdle(options = {}) {\n const now = timestamp(options.now)\n const maxTicks = options.maxTicks ?? 100\n if (!Number.isInteger(maxTicks) || maxTicks <= 0) {\n throw new TypeError('[bunderstack] maxTicks must be a positive integer')\n }\n\n const report: JobRunReport = {\n ticks: 0,\n claimed: 0,\n ran: 0,\n failed: 0,\n remainingRunnable: 0,\n }\n let inspection: Awaited<ReturnType<RuntimeTestingHandle['inspect']>>\n\n do {\n const tick = await handle.tick(now)\n report.ticks++\n report.claimed += tick.claimed\n report.ran += tick.ran\n report.failed += tick.failed\n inspection = await handle.inspect(now)\n report.remainingRunnable = inspection.runnable\n\n if (inspection.runnable > 0 && report.ticks >= maxTicks) {\n throw new TestJobsConvergenceError(report)\n }\n } while (inspection.runnable > 0)\n\n if ((options.failOnJobError ?? true) && inspection.failed.length > 0) {\n throw new TestJobsError(report, inspection.failed)\n }\n return report\n },\n }\n}\n"]}
@@ -0,0 +1,7 @@
1
+ import type { ResolvedStorageBuckets, StorageConfigInput } from '../storage/buckets.js';
2
+ export type TestStorage = {
3
+ read(key: string): Promise<Uint8Array>;
4
+ };
5
+ export declare function resolveTestBuckets(input: StorageConfigInput | undefined, root: string): ResolvedStorageBuckets;
6
+ export declare function createTestStorage(resolved: ResolvedStorageBuckets): TestStorage;
7
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/testing/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,kBAAkB,EACnB,MAAM,oBAAoB,CAAA;AAK3B,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;CACvC,CAAA;AAED,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,kBAAkB,GAAG,SAAS,EACrC,IAAI,EAAE,MAAM,GACX,sBAAsB,CAWxB;AAED,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,sBAAsB,GAC/B,WAAW,CAcb"}
@@ -0,0 +1,29 @@
1
+ import { resolveBuckets } from '../storage/buckets.js';
2
+ import { createBucketStorages } from '../storage/registry.js';
3
+ export function resolveTestBuckets(input, root) {
4
+ const resolved = resolveBuckets(input, {});
5
+ return {
6
+ defaultBucket: resolved.defaultBucket,
7
+ buckets: new Map([...resolved.buckets].map(([name, bucket]) => [
8
+ name,
9
+ { ...bucket, backend: { type: 'local', path: root } },
10
+ ])),
11
+ };
12
+ }
13
+ export function createTestStorage(resolved) {
14
+ const registry = createBucketStorages(resolved);
15
+ return {
16
+ async read(key) {
17
+ const bucketName = key.split('/')[0] || resolved.defaultBucket;
18
+ const adapter = registry.get(bucketName)?.adapter;
19
+ if (!adapter)
20
+ throw new Error(`Unknown bucket: ${bucketName}`);
21
+ const response = await adapter.get(key);
22
+ if (!response.ok) {
23
+ throw new Error(`Storage object not found: ${key}`);
24
+ }
25
+ return new Uint8Array(await response.arrayBuffer());
26
+ },
27
+ };
28
+ }
29
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/testing/storage.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAM1D,MAAM,UAAU,kBAAkB,CAChC,KAAqC,EACrC,IAAY;IAEZ,MAAM,QAAQ,GAAG,cAAc,CAAC,KAAK,EAAE,EAAE,CAAC,CAAA;IAC1C,OAAO;QACL,aAAa,EAAE,QAAQ,CAAC,aAAa;QACrC,OAAO,EAAE,IAAI,GAAG,CACd,CAAC,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC;YAC5C,IAAI;YACJ,EAAE,GAAG,MAAM,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAgB,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE;SAC/D,CAAC,CACH;KACF,CAAA;AACH,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,QAAgC;IAEhC,MAAM,QAAQ,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAA;IAC/C,OAAO;QACL,KAAK,CAAC,IAAI,CAAC,GAAG;YACZ,MAAM,UAAU,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,aAAa,CAAA;YAC9D,MAAM,OAAO,GAAG,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;YACjD,IAAI,CAAC,OAAO;gBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,UAAU,EAAE,CAAC,CAAA;YAC9D,MAAM,QAAQ,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACvC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,IAAI,KAAK,CAAC,6BAA6B,GAAG,EAAE,CAAC,CAAA;YACrD,CAAC;YACD,OAAO,IAAI,UAAU,CAAC,MAAM,QAAQ,CAAC,WAAW,EAAE,CAAC,CAAA;QACrD,CAAC;KACF,CAAA;AACH,CAAC","sourcesContent":["import type {\n ResolvedStorageBuckets,\n StorageConfigInput,\n} from '../storage/buckets'\n\nimport { resolveBuckets } from '../storage/buckets'\nimport { createBucketStorages } from '../storage/registry'\n\nexport type TestStorage = {\n read(key: string): Promise<Uint8Array>\n}\n\nexport function resolveTestBuckets(\n input: StorageConfigInput | undefined,\n root: string,\n): ResolvedStorageBuckets {\n const resolved = resolveBuckets(input, {})\n return {\n defaultBucket: resolved.defaultBucket,\n buckets: new Map(\n [...resolved.buckets].map(([name, bucket]) => [\n name,\n { ...bucket, backend: { type: 'local' as const, path: root } },\n ]),\n ),\n }\n}\n\nexport function createTestStorage(\n resolved: ResolvedStorageBuckets,\n): TestStorage {\n const registry = createBucketStorages(resolved)\n return {\n async read(key) {\n const bucketName = key.split('/')[0] || resolved.defaultBucket\n const adapter = registry.get(bucketName)?.adapter\n if (!adapter) throw new Error(`Unknown bucket: ${bucketName}`)\n const response = await adapter.get(key)\n if (!response.ok) {\n throw new Error(`Storage object not found: ${key}`)\n }\n return new Uint8Array(await response.arrayBuffer())\n },\n }\n}\n"]}