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.
Files changed (137) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/README.md +24 -8
  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/start/index.d.ts +3 -3
  92. package/dist/start/index.d.ts.map +1 -1
  93. package/dist/start/index.js +3 -3
  94. package/dist/start/index.js.map +1 -1
  95. package/dist/storage/background.d.ts +3 -0
  96. package/dist/storage/background.d.ts.map +1 -0
  97. package/dist/storage/background.js +3 -0
  98. package/dist/storage/background.js.map +1 -0
  99. package/dist/testing/auth.d.ts +57 -0
  100. package/dist/testing/auth.d.ts.map +1 -0
  101. package/dist/testing/auth.js +51 -0
  102. package/dist/testing/auth.js.map +1 -0
  103. package/dist/testing/client.d.ts +6 -0
  104. package/dist/testing/client.d.ts.map +1 -0
  105. package/dist/testing/client.js +9 -0
  106. package/dist/testing/client.js.map +1 -0
  107. package/dist/testing/database.d.ts +6 -0
  108. package/dist/testing/database.d.ts.map +1 -0
  109. package/dist/testing/database.js +8 -0
  110. package/dist/testing/database.js.map +1 -0
  111. package/dist/testing/email.d.ts +15 -0
  112. package/dist/testing/email.d.ts.map +1 -0
  113. package/dist/testing/email.js +32 -0
  114. package/dist/testing/email.js.map +1 -0
  115. package/dist/testing/fixture.d.ts +33 -0
  116. package/dist/testing/fixture.d.ts.map +1 -0
  117. package/dist/testing/fixture.js +125 -0
  118. package/dist/testing/fixture.js.map +1 -0
  119. package/dist/testing/jobs.d.ts +32 -0
  120. package/dist/testing/jobs.d.ts.map +1 -0
  121. package/dist/testing/jobs.js +67 -0
  122. package/dist/testing/jobs.js.map +1 -0
  123. package/dist/testing/storage.d.ts +7 -0
  124. package/dist/testing/storage.d.ts.map +1 -0
  125. package/dist/testing/storage.js +29 -0
  126. package/dist/testing/storage.js.map +1 -0
  127. package/dist/testing.d.ts +9 -26
  128. package/dist/testing.d.ts.map +1 -1
  129. package/dist/testing.js +3 -9
  130. package/dist/testing.js.map +1 -1
  131. package/llms.txt +43 -13
  132. package/package.json +23 -15
  133. package/skills/creating-bunderstack-apps/references/application-structure.md +16 -12
  134. package/skills/creating-bunderstack-apps/references/runtime-integrations.md +4 -1
  135. package/skills/migrating-to-bunderstack/SKILL.md +7 -6
  136. package/skills/migrating-to-bunderstack/references/audit-checklist.md +4 -4
  137. package/skills/migrating-to-bunderstack/references/runtime-replacements.md +55 -49
@@ -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"]}
package/dist/testing.d.ts CHANGED
@@ -1,27 +1,10 @@
1
- export type AuthSessionResolverLike = {
2
- api: {
3
- getSession: (opts: {
4
- headers: Headers;
5
- }) => Promise<unknown>;
6
- };
7
- };
8
- export type BunderstackAppLike = {
9
- auth: unknown;
10
- };
11
- /**
12
- * Mock the auth session resolver on a Bunderstack app instance for unit testing.
13
- */
14
- export declare function mockAuthSession<TUser extends {
15
- id: string;
16
- email: string;
17
- name?: string;
18
- role?: string;
19
- }>(app: BunderstackAppLike, resolver: (opts: {
20
- headers: Headers;
21
- }) => Promise<{
22
- user: TUser;
23
- session?: {
24
- activeOrganizationId?: string | null;
25
- } | null;
26
- } | null>): void;
1
+ export { createTestApp } from './testing/fixture.js';
2
+ export type { TestFixture, TestOptions } from './testing/fixture.js';
3
+ export { mockAuthSession, TestAuthError } from './testing/auth.js';
4
+ export type { SignUpEmailInput, TestAuth, TestIdentity, TestUser, } from './testing/auth.js';
5
+ export type { CapturedEmail, TestEmail } from './testing/email.js';
6
+ export type { TestStorage } from './testing/storage.js';
7
+ export { TestJobsConvergenceError, TestJobsError } from './testing/jobs.js';
8
+ export type { JobRunReport, RunNextOptions, RunUntilIdleOptions, TestJobs, } from './testing/jobs.js';
9
+ export type { TestDatabaseStrategy, TestDatabaseTarget, TestDatabaseTargetOptions, } from './database/adapter.js';
27
10
  //# sourceMappingURL=testing.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAEA,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;;GAEG;AACH,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"}
1
+ {"version":3,"file":"testing.d.ts","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AACjD,YAAY,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACjE,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAC/D,YAAY,EACV,gBAAgB,EAChB,QAAQ,EACR,YAAY,EACZ,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAC/D,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AACpD,OAAO,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AACxE,YAAY,EACV,YAAY,EACZ,cAAc,EACd,mBAAmB,EACnB,QAAQ,GACT,MAAM,gBAAgB,CAAA;AACvB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,yBAAyB,GAC1B,MAAM,oBAAoB,CAAA"}
package/dist/testing.js CHANGED
@@ -1,11 +1,5 @@
1
1
  // src/testing.ts — test utilities for Bunderstack applications.
2
- /**
3
- * Mock the auth session resolver on a Bunderstack app instance for unit testing.
4
- */
5
- export function mockAuthSession(app, resolver) {
6
- const auth = app.auth;
7
- if (auth?.api) {
8
- auth.api.getSession = resolver;
9
- }
10
- }
2
+ export { createTestApp } from './testing/fixture.js';
3
+ export { mockAuthSession, TestAuthError } from './testing/auth.js';
4
+ export { TestJobsConvergenceError, TestJobsError } from './testing/jobs.js';
11
5
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAYhE;;GAEG;AACH,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","sourcesContent":["// src/testing.ts — test utilities for Bunderstack applications.\n\nexport type AuthSessionResolverLike = {\n api: {\n getSession: (opts: { headers: Headers }) => Promise<unknown>\n }\n}\n\nexport type BunderstackAppLike = {\n auth: unknown\n}\n\n/**\n * Mock the auth session resolver on a Bunderstack app instance for unit testing.\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"]}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"AAAA,gEAAgE;AAEhE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAEjD,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA;AAS/D,OAAO,EAAE,wBAAwB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAA","sourcesContent":["// src/testing.ts — test utilities for Bunderstack applications.\n\nexport { createTestApp } from './testing/fixture'\nexport type { TestFixture, TestOptions } from './testing/fixture'\nexport { mockAuthSession, TestAuthError } from './testing/auth'\nexport type {\n SignUpEmailInput,\n TestAuth,\n TestIdentity,\n TestUser,\n} from './testing/auth'\nexport type { CapturedEmail, TestEmail } from './testing/email'\nexport type { TestStorage } from './testing/storage'\nexport { TestJobsConvergenceError, TestJobsError } from './testing/jobs'\nexport type {\n JobRunReport,\n RunNextOptions,\n RunUntilIdleOptions,\n TestJobs,\n} from './testing/jobs'\nexport type {\n TestDatabaseStrategy,\n TestDatabaseTarget,\n TestDatabaseTargetOptions,\n} from './database/adapter'\n"]}
package/llms.txt CHANGED
@@ -6,11 +6,13 @@ This file is written for coding agents. It is dense on purpose.
6
6
 
7
7
  WHAT IT IS
8
8
 
9
- createBunderstack() takes a Drizzle schema and returns an app whose handler is
10
- a single Web-Standard Request -> Response function. From the schema it
11
- generates secured CRUD procedures. Your own procedures, file storage, realtime
12
- subscriptions, and a health check live in the same oRPC graph, reachable both
13
- as typed RPC and as ordinary HTTP.
9
+ bunderstack() takes a Drizzle schema and returns a synchronous, side-effect-free
10
+ backend declaration. backend.start() materializes the production runtime whose
11
+ handler is a single Web-Standard Request -> Response function. backend.test()
12
+ materializes an isolated, lexically owned test fixture. From the schema the
13
+ runtime generates secured CRUD procedures. Your own procedures, file storage,
14
+ realtime subscriptions, and a health check live in the same oRPC graph,
15
+ reachable both as typed RPC and as ordinary HTTP.
14
16
 
15
17
  Stack: Bun, Drizzle (+ drizzle-kit), Better Auth, oRPC v2, libSQL or Postgres,
16
18
  Bun.Image. Validation accepts any Standard Schema library; generated schemas use
@@ -22,19 +24,22 @@ integration).
22
24
 
23
25
  MINIMAL APP
24
26
 
25
- import { createBunderstack } from 'bunderstack'
26
- import { libsql } from 'bunderstack/database/libsql'
27
+ import { bunderstack } from 'bunderstack'
28
+ import { libsql } from 'bunderstack/libsql'
27
29
  import * as schema from './schema'
28
30
 
29
- export const app = await createBunderstack({
31
+ export const backend = bunderstack({
30
32
  schema,
31
33
  database: { adapter: libsql(), url: 'file:./data.db' },
32
34
  access: { posts: { list: 'public', get: 'public' } },
33
35
  })
34
36
 
35
- export type App = typeof app
37
+ export type App = Awaited<ReturnType<typeof backend.start>>
38
+ export const app = await backend.start()
39
+
36
40
  Bun.serve({ fetch: app.handler })
37
41
 
42
+ The blueprint imports the backend declaration and never starts the runtime.
38
43
  Database adapters are imported from their own entry points: libsql(),
39
44
  pglite(), bunSql(), postgresJs(). Provisioning: `await provision(app)` pushes
40
45
  the schema in development and applies committed migrations once a migrations/
@@ -71,13 +76,38 @@ Router modules are plain objects that import the base they need:
71
76
  export const api = { boards: boardsRouter }
72
77
 
73
78
  // config
74
- createBunderstack({ schema, database, api })
79
+ bunderstack({ schema, database, api })
75
80
 
76
81
  Do NOT write a factory that receives a bag of procedures. That pattern exists
77
82
  only because the api option used to be a callback. The callback form still
78
83
  works — api: (o) => ({ ... }) — for a router that must be built from the
79
84
  framework builder at configuration time, but the object form is the default.
80
85
 
86
+ TESTING
87
+
88
+ Use lexical fixture ownership; do not use a process-wide app registry or a
89
+ global afterEach hook:
90
+
91
+ import { backend } from './backend'
92
+
93
+ test('creates a post', async () => {
94
+ await using t = await backend.test({ database: { schema: 'push' } })
95
+ const identity = t.auth.mockSession({
96
+ id: 'user-1',
97
+ email: 'dev@example.com',
98
+ name: 'Developer',
99
+ })
100
+ const client = t.client(identity)
101
+ await client.posts.create({ title: 'Hello', userId: identity.user.id })
102
+ })
103
+
104
+ The fixture owns isolated database, email, storage, realtime, and job state.
105
+ Its auth helper creates user/session identities only; application-specific
106
+ organization setup belongs in a typed application helper. t.jobs.tick() claims
107
+ one snapshot of runnable jobs. t.jobs.settle() repeats snapshots until the
108
+ queue converges, a terminal job fails, or its limits are reached; it does not
109
+ advance the clock to delayed jobs automatically.
110
+
81
111
  BASES
82
112
 
83
113
  o.public session resolved only if the handler calls context.getSession()
@@ -118,7 +148,7 @@ Two placements, and the difference is the thing people get wrong.
118
148
  .use() on a base reaches only procedures built from that base. Use it for
119
149
  rules about a group of procedures: role checks, organization scope, quotas.
120
150
 
121
- middleware: [...] in createBunderstack reaches EVERY procedure, including the
151
+ middleware: [...] in bunderstack reaches EVERY procedure, including the
122
152
  generated CRUD, storage, realtime, and health. Use it for observability. A
123
153
  tracing middleware attached to a base leaves generated CRUD unmeasured,
124
154
  because the framework builds those procedures itself and they never pass
@@ -137,7 +167,7 @@ through an application base.
137
167
  }
138
168
  })
139
169
 
140
- createBunderstack({ schema, database, middleware: [instrumentation], api })
170
+ bunderstack({ schema, database, middleware: [instrumentation], api })
141
171
 
142
172
  Rules for graph-wide middleware:
143
173
  - It runs before authentication. context.user does not exist there.
@@ -262,7 +292,7 @@ ENV
262
292
 
263
293
  Server variables must not start with PUBLIC_; client variables must. Validated
264
294
  at boot; app.env and context.env are typed from the schema. Declare the schema
265
- in its own module so both createBunderstack and defineApi can use it.
295
+ in its own module so both bunderstack and defineApi can use it.
266
296
 
267
297
  AUTH
268
298
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunderstack",
3
- "version": "0.21.0",
3
+ "version": "0.22.0",
4
4
  "description": "Batteries-included backend framework for Bun: type-safe oRPC APIs, auth, storage, realtime, jobs, email, and validated env from one config.",
5
5
  "keywords": [
6
6
  "backend",
@@ -48,19 +48,23 @@
48
48
  "types": "./dist/codegen.d.ts",
49
49
  "default": "./dist/codegen.js"
50
50
  },
51
- "./database/bun-sql": {
51
+ "./bun-sql": {
52
52
  "types": "./dist/database/bun-sql.d.ts",
53
53
  "default": "./dist/database/bun-sql.js"
54
54
  },
55
- "./database/libsql": {
55
+ "./bun-sqlite": {
56
+ "types": "./dist/database/bun-sqlite.d.ts",
57
+ "default": "./dist/database/bun-sqlite.js"
58
+ },
59
+ "./libsql": {
56
60
  "types": "./dist/database/libsql.d.ts",
57
61
  "default": "./dist/database/libsql.js"
58
62
  },
59
- "./database/pglite": {
63
+ "./pglite": {
60
64
  "types": "./dist/database/pglite.d.ts",
61
65
  "default": "./dist/database/pglite.js"
62
66
  },
63
- "./database/postgres-js": {
67
+ "./postgres-js": {
64
68
  "types": "./dist/database/postgres-js.d.ts",
65
69
  "default": "./dist/database/postgres-js.js"
66
70
  },
@@ -72,11 +76,15 @@
72
76
  "types": "./dist/provision.d.ts",
73
77
  "default": "./dist/provision.js"
74
78
  },
79
+ "./testing": {
80
+ "types": "./dist/testing.d.ts",
81
+ "default": "./dist/testing.js"
82
+ },
75
83
  "./schema": {
76
84
  "types": "./dist/schema-export.d.ts",
77
85
  "default": "./dist/schema-export.js"
78
86
  },
79
- "./schema/pg": {
87
+ "./schema-pg": {
80
88
  "types": "./dist/schema-export-pg.d.ts",
81
89
  "default": "./dist/schema-export-pg.js"
82
90
  },
@@ -84,7 +92,7 @@
84
92
  "types": "./dist/typeid.d.ts",
85
93
  "default": "./dist/typeid.js"
86
94
  },
87
- "./typeid/pg": {
95
+ "./typeid-pg": {
88
96
  "types": "./dist/typeid-pg.d.ts",
89
97
  "default": "./dist/typeid-pg.js"
90
98
  },
@@ -100,7 +108,7 @@
100
108
  "types": "./dist/cron.d.ts",
101
109
  "default": "./dist/cron.js"
102
110
  },
103
- "./email/smtp": {
111
+ "./email-smtp": {
104
112
  "types": "./dist/email/smtp.d.ts",
105
113
  "default": "./dist/email/smtp.js"
106
114
  },
@@ -112,23 +120,23 @@
112
120
  "types": "./dist/client/index.d.ts",
113
121
  "default": "./dist/client/index.js"
114
122
  },
115
- "./client/rest": {
123
+ "./client-rest": {
116
124
  "types": "./dist/client/rest.d.ts",
117
125
  "default": "./dist/client/rest.js"
118
126
  },
119
- "./client/react": {
127
+ "./client-react": {
120
128
  "types": "./dist/client/react.d.ts",
121
129
  "default": "./dist/client/react.js"
122
130
  },
123
- "./client/solid": {
131
+ "./client-solid": {
124
132
  "types": "./dist/client/solid.d.ts",
125
133
  "default": "./dist/client/solid.js"
126
134
  },
127
- "./client/svelte": {
135
+ "./client-svelte": {
128
136
  "types": "./dist/client/svelte.d.ts",
129
137
  "default": "./dist/client/svelte.js"
130
138
  },
131
- "./client/vue": {
139
+ "./client-vue": {
132
140
  "types": "./dist/client/vue.d.ts",
133
141
  "default": "./dist/client/vue.js"
134
142
  },
@@ -136,7 +144,7 @@
136
144
  "types": "./dist/query/index.d.ts",
137
145
  "default": "./dist/query/index.js"
138
146
  },
139
- "./query/react": {
147
+ "./query-react": {
140
148
  "types": "./dist/query/react.d.ts",
141
149
  "default": "./dist/query/react.js"
142
150
  },
@@ -148,7 +156,7 @@
148
156
  "types": "./dist/start/index.d.ts",
149
157
  "default": "./dist/start/index.js"
150
158
  },
151
- "./start/auth": {
159
+ "./start-auth": {
152
160
  "types": "./dist/start/auth-client.d.ts",
153
161
  "default": "./dist/start/auth-client.js"
154
162
  }
@@ -1,23 +1,27 @@
1
1
  # Application structure
2
2
 
3
- ## Keep the entry declarative
3
+ ## Separate declaration from runtime
4
4
 
5
- The Bunderstack entry constructs and exports `app` (and usually `type App =
6
- typeof app`), configures the declared capabilities, and calls `provision(app)`
7
- when the application owns provisioning. Keep unrelated external side effects
8
- out of its import graph: the blueprint command imports this entry with
9
- `BUNDERSTACK_INTROSPECT=1`.
5
+ `src/bunderstack/backend.ts` synchronously constructs and exports `backend =
6
+ bunderstack({...})`. It is pure: it validates the declaration and exposes
7
+ `backend.manifest`, but it does not connect to infrastructure. The blueprint
8
+ imports this declaration without starting the application.
9
+
10
+ `src/bunderstack/index.ts` owns the production runtime: it imports `backend`,
11
+ calls `await backend.start()`, exports `app`, and calls `provision(app)` when the
12
+ application owns provisioning. Keep unrelated external side effects out of the
13
+ backend import graph.
10
14
 
11
15
  Start a small API in `src/bunderstack.ts`. Split a meaningful configuration
12
- into `src/bunderstack/` with `index.ts`, `schema/`, `access.ts`, `auth.ts`,
16
+ into `src/bunderstack/` with `backend.ts`, `index.ts`, `schema/`, `access.ts`, `auth.ts`,
13
17
  `env.ts`, `jobs/`, and `api/` as needed. The entry remains the one place that
14
- assembles those modules into `createBunderstack()`; do not create parallel app,
18
+ starts the declared backend; do not create parallel app,
15
19
  database, or auth instances.
16
20
 
17
21
  ## Aggregate the schema
18
22
 
19
23
  Export every domain, Better Auth, plugin, and Bunderstack internal table from
20
- the schema object passed to `createBunderstack()`. Include
24
+ the schema object passed to `bunderstack()`. Include
21
25
  `export * from 'bunderstack/schema'` so migrations include the internal tables.
22
26
  Define Better Auth tables required by the selected auth flows and plugins; do
23
27
  not assume a minimal auth configuration needs every optional provider table.
@@ -61,8 +65,8 @@ export const projectsRouter = {
61
65
  // src/bunderstack/api/index.ts
62
66
  export const api = { projects: projectsRouter }
63
67
 
64
- // entry
65
- createBunderstack({ schema, database, api })
68
+ // backend.ts
69
+ bunderstack({ schema, database, api })
66
70
  ```
67
71
 
68
72
  Do not write a router factory that receives a bag of procedures. That shape
@@ -118,7 +122,7 @@ const instrumentation = o.middleware(async ({ context, next, path }) => {
118
122
  }
119
123
  })
120
124
 
121
- createBunderstack({ schema, database, middleware: [instrumentation], api })
125
+ bunderstack({ schema, database, middleware: [instrumentation], api })
122
126
  ```
123
127
 
124
128
  Three rules apply to a graph-wide middleware. It runs before authentication, so
@@ -39,8 +39,11 @@ Declare queue jobs with `jobs: (j) => j.define(...)`, then run them in a
39
39
  separate production process:
40
40
 
41
41
  ```ts
42
- import { app } from './bunderstack'
42
+ import { backend } from './bunderstack/backend'
43
43
 
44
+ const app = await backend.start({
45
+ env: { ...process.env, BUNDERSTACK_ROLE: 'web' },
46
+ })
44
47
  await app.runWorker()
45
48
  ```
46
49
 
@@ -16,7 +16,7 @@ infrastructure, use `creating-bunderstack-apps` instead.
16
16
  capability today and which call sites depend on it.
17
17
  2. Add a migration contract test before removing legacy paths, so every
18
18
  deletion has a gate that fails when behaviour is lost.
19
- 3. Establish one Bunderstack app and one schema aggregate.
19
+ 3. Establish one Bunderstack backend declaration, one runtime, and one schema aggregate.
20
20
  4. Move auth and access without creating duplicate instances.
21
21
  5. Replace infrastructure capability by capability.
22
22
  6. Mount one handler and separate the production worker.
@@ -31,8 +31,9 @@ database client sits outside provisioning, migration state, and request
31
31
  transactions. A second env schema drifts from the validated one and passes
32
32
  locally while failing at boot.
33
33
 
34
- Pass `authConfig` into `createBunderstack()`, re-export `app.auth` and `app.db`
35
- from the entry, and let the `env` passed to `createBunderstack()` be the only
34
+ Pass `authConfig` into `bunderstack()`, start that declaration once in the web
35
+ entry, and re-export `app.auth` and `app.db` from the runtime entry. Let the
36
+ declared `env` schema plus the source passed to `backend.start()` be the only
36
37
  validated source. A more specific file route also shadows the catch-all, so a
37
38
  surviving `/api/auth/$` silently keeps serving the instance you meant to delete.
38
39
 
@@ -53,7 +54,7 @@ surviving `/api/auth/$` silently keeps serving the instance you meant to delete.
53
54
  | Channel-and-payload realtime publishing | `ctx.realtime.publish(schema.tasks, 'update', row)` after the write commits |
54
55
  | AWS or Tigris SDK wrapper | `app.storage` buckets |
55
56
  | Resend SDK wrapper | `app.email.send(...)` |
56
- | `createEnv()` beside the app | `env` passed to `createBunderstack()` |
57
+ | `createEnv()` beside the app | `env` schema in `bunderstack()` and source in `backend.start()` |
57
58
  | Implicit database driver | Explicit adapter, `database: { adapter: libsql(), url }` |
58
59
  | Schema push in production | Committed Drizzle `migrations/`, applied by `provision(app)` |
59
60
  | Undeclared deployment | `package.json#bunderstack.entry` and a checked blueprint |
@@ -73,8 +74,8 @@ shim is acceptable when call sites are numerous; the shim is deleted under this
73
74
  same gate. Uninstall the replaced SDK package in the commit that removes its
74
75
  last importer, so a stale wrapper cannot be reintroduced silently.
75
76
 
76
- Tests and scripts that construct their own app instance must call
77
- `app.close()`.
77
+ Tests should use lexically scoped `await using` fixtures from `backend.test()`;
78
+ scripts that explicitly start a runtime must close the runtime they own.
78
79
 
79
80
  ## Production gate
80
81
 
@@ -6,26 +6,26 @@ output or a file reference, not an assertion.
6
6
 
7
7
  | Capability | Legacy shape to find | Authoritative replacement | Evidence that the move is done | Deletion gate |
8
8
  | ---------------------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
9
- | Auth instance | A second `betterAuth({...})` call, a custom session resolver, a patched `getSession` | `authConfig` passed to `createBunderstack()`; consumers import `app.auth` | One `betterAuth` construction in the repository; a protected route rejects an unauthenticated request | No importer of the legacy auth module remains |
9
+ | Auth instance | A second `betterAuth({...})` call, a custom session resolver, a patched `getSession` | `authConfig` passed to `bunderstack()`; consumers import `app.auth` | One `betterAuth` construction in the repository; a protected route rejects an unauthenticated request | No importer of the legacy auth module remains |
10
10
  | Auth schema | Auth tables generated into a legacy schema directory | Auth tables in the one schema aggregate | Generated migration includes `user`, `session`, `account`, `verification` | Legacy schema directory has no importers |
11
11
  | Database client | A module constructing its own libSQL/Postgres client | `app.db`, re-exported from the entry | The entry is the only place calling the adapter factory | Legacy `db` module deleted or reduced to a re-export, then deleted |
12
12
  | API mounting | Hand-written handler maps; separate `/api/auth/$`, `/api/trpc/$` | `createApiHandlers(app)` on one `/api/$` | Auth and oRPC requests succeed with only the catch-all present | Shadowing route files deleted |
13
13
  | Custom API routes | Route files doing CRUD the framework can generate | Generated CRUD plus `defineAccess`, or an `o.protected` procedure | Access rules cover each exposed table; a cross-owner request is denied | Route file has no client callers |
14
14
  | Access control | Per-endpoint session checks and hand-written SQL filters | `defineAccess(schema, rules)` with `scope.read` / `scope.write` | A test asserts a second user cannot read or write the first user's rows | Manual filter helpers unused |
15
- | Jobs | BullMQ or a bespoke queue module | `jobs.define({ ... })` and `app.jobs.enqueue(...)` | Job appears in `app.manifest.background.jobs` | No queue library importer; package uninstalled |
15
+ | Jobs | BullMQ or a bespoke queue module | `jobs.define({ ... })` and `app.jobs.enqueue(...)` | Job appears in `backend.manifest.background.jobs` | No queue library importer; package uninstalled |
16
16
  | Cron | `/api/cron/*` guarded by a shared secret | `jobs.cron({ schedule, handler })` | Cron task appears in the blueprint | Cron route file and its secret removed from env |
17
17
  | Worker topology | `startWorker()` or a queue bootstrap in the web entry | `src/worker.ts` calling `app.runWorker()`, run as its own process | Web entry starts no worker; the worker command exists in deployment config | Worker process is deployed before the embedded call is removed |
18
18
  | Realtime | Custom WebSocket server, manual pub/sub, channel-and-payload publishing | `realtime` config plus `ctx.realtime.publish(table, event, row)` after commit | A direct write reaches a subscriber with the complete row | Custom transport deleted; shared Redis configured for multi-process |
19
19
  | Storage | AWS or Tigris SDK wrapper, custom multipart upload route | Declared buckets and `app.storage` | Upload, signed URL, and delete work through the facade | Wrapper deleted and SDK uninstalled |
20
20
  | Email | Resend or SMTP SDK wrapper | `email` config and `app.email.send(...)` | A send succeeds through the configured provider | Wrapper deleted and SDK uninstalled |
21
- | Env | `createEnv()` beside the app, `dotenv`, unchecked `process.env` reads | `env` passed to `createBunderstack()`; `app.env` / `ctx.env` | Boot fails with a clear message when a required variable is missing | Legacy env module unused; `.env.example` lists names only |
21
+ | Env | `createEnv()` beside the app, `dotenv`, unchecked `process.env` reads | `env` schema in `bunderstack()`; source in `backend.start()`; `app.env` / `ctx.env` | Boot fails with a clear message when a required variable is missing | Legacy env module unused; `.env.example` lists names only |
22
22
  | API declaration | Router factories taking a bag of procedures; hand-written builder generics | `defineApi({ schema, env })` bases in one module, plain router objects, `api` object | A router module imports its base and exports an object; no factory remains | `BunderstackApiBuilder<...>` and `os.$context<...>()` deleted |
23
23
  | Observability | Tracing or logging attached to an application procedure base | `middleware: [...]` in the config, which also reaches the generated CRUD | A generated CRUD request produces a span or log line | Per-base instrumentation removed |
24
24
  | Errors | `new ORPCError(...)` at call sites, or a second error model | `errors.CODE({ message })`; `BunderstackError` outside a handler | A failing request answers the declared status, not 500 | `ORPCError` import gone from the api layer |
25
25
  | List endpoints | Hand-rolled limit/offset/filter/count blocks repeated per table | `listSpec(table, options)` applied to your own base | The endpoint accepts cursor and `count: true` and answers `ListResult` | Duplicated paging helpers deleted |
26
26
  | Migrations | Schema push against production | Committed Drizzle `migrations/`, applied by `provision(app)` | `migrations/` is under version control and applies cleanly to an empty database | Push command removed from deployment |
27
27
  | Deployment declaration | No `bunderstack.entry`, no blueprint | `package.json#bunderstack.entry` and a committed blueprint | `bun run blueprint:check` passes in CI | Deployment reads the blueprint rather than ad-hoc process config |
28
- | App lifetime | Tests and scripts leaking app instances | `app.close()` in a `finally` or `afterEach` | The test run exits without hanging | — |
28
+ | App lifetime | Tests and scripts leaking app instances | Lexical `await using` fixture from `backend.test()` | Concurrent tests own independent fixtures and the run exits without hanging | — |
29
29
 
30
30
  ## Reading a partially migrated application
31
31