@obh/forge 0.2.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 (215) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +146 -0
  3. package/dist/capabilities/analytics.d.ts +3 -0
  4. package/dist/capabilities/analytics.d.ts.map +1 -0
  5. package/dist/capabilities/analytics.js +91 -0
  6. package/dist/capabilities/analytics.js.map +1 -0
  7. package/dist/capabilities/api-keys.d.ts +3 -0
  8. package/dist/capabilities/api-keys.d.ts.map +1 -0
  9. package/dist/capabilities/api-keys.js +90 -0
  10. package/dist/capabilities/api-keys.js.map +1 -0
  11. package/dist/capabilities/audit.d.ts +3 -0
  12. package/dist/capabilities/audit.d.ts.map +1 -0
  13. package/dist/capabilities/audit.js +74 -0
  14. package/dist/capabilities/audit.js.map +1 -0
  15. package/dist/capabilities/entitlements.d.ts +3 -0
  16. package/dist/capabilities/entitlements.d.ts.map +1 -0
  17. package/dist/capabilities/entitlements.js +94 -0
  18. package/dist/capabilities/entitlements.js.map +1 -0
  19. package/dist/capabilities/events.d.ts +3 -0
  20. package/dist/capabilities/events.d.ts.map +1 -0
  21. package/dist/capabilities/events.js +107 -0
  22. package/dist/capabilities/events.js.map +1 -0
  23. package/dist/capabilities/files.d.ts +3 -0
  24. package/dist/capabilities/files.d.ts.map +1 -0
  25. package/dist/capabilities/files.js +100 -0
  26. package/dist/capabilities/files.js.map +1 -0
  27. package/dist/capabilities/helpers.d.ts +27 -0
  28. package/dist/capabilities/helpers.d.ts.map +1 -0
  29. package/dist/capabilities/helpers.js +63 -0
  30. package/dist/capabilities/helpers.js.map +1 -0
  31. package/dist/capabilities/import-export.d.ts +3 -0
  32. package/dist/capabilities/import-export.d.ts.map +1 -0
  33. package/dist/capabilities/import-export.js +267 -0
  34. package/dist/capabilities/import-export.js.map +1 -0
  35. package/dist/capabilities/index.d.ts +16 -0
  36. package/dist/capabilities/index.d.ts.map +1 -0
  37. package/dist/capabilities/index.js +71 -0
  38. package/dist/capabilities/index.js.map +1 -0
  39. package/dist/capabilities/jobs.d.ts +3 -0
  40. package/dist/capabilities/jobs.d.ts.map +1 -0
  41. package/dist/capabilities/jobs.js +117 -0
  42. package/dist/capabilities/jobs.js.map +1 -0
  43. package/dist/capabilities/notifications.d.ts +3 -0
  44. package/dist/capabilities/notifications.d.ts.map +1 -0
  45. package/dist/capabilities/notifications.js +113 -0
  46. package/dist/capabilities/notifications.js.map +1 -0
  47. package/dist/capabilities/search.d.ts +3 -0
  48. package/dist/capabilities/search.d.ts.map +1 -0
  49. package/dist/capabilities/search.js +121 -0
  50. package/dist/capabilities/search.js.map +1 -0
  51. package/dist/capabilities/settings.d.ts +3 -0
  52. package/dist/capabilities/settings.d.ts.map +1 -0
  53. package/dist/capabilities/settings.js +75 -0
  54. package/dist/capabilities/settings.js.map +1 -0
  55. package/dist/capabilities/types.d.ts +16 -0
  56. package/dist/capabilities/types.d.ts.map +1 -0
  57. package/dist/capabilities/types.js +3 -0
  58. package/dist/capabilities/types.js.map +1 -0
  59. package/dist/capabilities/webhooks.d.ts +3 -0
  60. package/dist/capabilities/webhooks.d.ts.map +1 -0
  61. package/dist/capabilities/webhooks.js +117 -0
  62. package/dist/capabilities/webhooks.js.map +1 -0
  63. package/dist/cli.d.ts +3 -0
  64. package/dist/cli.d.ts.map +1 -0
  65. package/dist/cli.js +60 -0
  66. package/dist/cli.js.map +1 -0
  67. package/dist/commands/add.d.ts +3 -0
  68. package/dist/commands/add.d.ts.map +1 -0
  69. package/dist/commands/add.js +88 -0
  70. package/dist/commands/add.js.map +1 -0
  71. package/dist/commands/doctor.d.ts +3 -0
  72. package/dist/commands/doctor.d.ts.map +1 -0
  73. package/dist/commands/doctor.js +94 -0
  74. package/dist/commands/doctor.js.map +1 -0
  75. package/dist/commands/generate.d.ts +3 -0
  76. package/dist/commands/generate.d.ts.map +1 -0
  77. package/dist/commands/generate.js +41 -0
  78. package/dist/commands/generate.js.map +1 -0
  79. package/dist/commands/inspect.d.ts +3 -0
  80. package/dist/commands/inspect.d.ts.map +1 -0
  81. package/dist/commands/inspect.js +64 -0
  82. package/dist/commands/inspect.js.map +1 -0
  83. package/dist/commands/new.d.ts +18 -0
  84. package/dist/commands/new.d.ts.map +1 -0
  85. package/dist/commands/new.js +149 -0
  86. package/dist/commands/new.js.map +1 -0
  87. package/dist/commands/shared.d.ts +14 -0
  88. package/dist/commands/shared.d.ts.map +1 -0
  89. package/dist/commands/shared.js +36 -0
  90. package/dist/commands/shared.js.map +1 -0
  91. package/dist/commands/skill.d.ts +3 -0
  92. package/dist/commands/skill.d.ts.map +1 -0
  93. package/dist/commands/skill.js +59 -0
  94. package/dist/commands/skill.js.map +1 -0
  95. package/dist/commands/stack.d.ts +13 -0
  96. package/dist/commands/stack.d.ts.map +1 -0
  97. package/dist/commands/stack.js +192 -0
  98. package/dist/commands/stack.js.map +1 -0
  99. package/dist/generators/api.d.ts +16 -0
  100. package/dist/generators/api.d.ts.map +1 -0
  101. package/dist/generators/api.js +375 -0
  102. package/dist/generators/api.js.map +1 -0
  103. package/dist/generators/lwd.d.ts +12 -0
  104. package/dist/generators/lwd.d.ts.map +1 -0
  105. package/dist/generators/lwd.js +143 -0
  106. package/dist/generators/lwd.js.map +1 -0
  107. package/dist/generators/mobile.d.ts +16 -0
  108. package/dist/generators/mobile.d.ts.map +1 -0
  109. package/dist/generators/mobile.js +180 -0
  110. package/dist/generators/mobile.js.map +1 -0
  111. package/dist/generators/platform-package.d.ts +18 -0
  112. package/dist/generators/platform-package.d.ts.map +1 -0
  113. package/dist/generators/platform-package.js +568 -0
  114. package/dist/generators/platform-package.js.map +1 -0
  115. package/dist/generators/root.d.ts +15 -0
  116. package/dist/generators/root.d.ts.map +1 -0
  117. package/dist/generators/root.js +249 -0
  118. package/dist/generators/root.js.map +1 -0
  119. package/dist/generators/sdk.d.ts +14 -0
  120. package/dist/generators/sdk.d.ts.map +1 -0
  121. package/dist/generators/sdk.js +137 -0
  122. package/dist/generators/sdk.js.map +1 -0
  123. package/dist/generators/web.d.ts +15 -0
  124. package/dist/generators/web.d.ts.map +1 -0
  125. package/dist/generators/web.js +203 -0
  126. package/dist/generators/web.js.map +1 -0
  127. package/dist/generators/worker.d.ts +13 -0
  128. package/dist/generators/worker.d.ts.map +1 -0
  129. package/dist/generators/worker.js +148 -0
  130. package/dist/generators/worker.js.map +1 -0
  131. package/dist/index.d.ts +10 -0
  132. package/dist/index.d.ts.map +1 -0
  133. package/dist/index.js +44 -0
  134. package/dist/index.js.map +1 -0
  135. package/dist/project/context.d.ts +24 -0
  136. package/dist/project/context.d.ts.map +1 -0
  137. package/dist/project/context.js +63 -0
  138. package/dist/project/context.js.map +1 -0
  139. package/dist/project/manifest.d.ts +26 -0
  140. package/dist/project/manifest.d.ts.map +1 -0
  141. package/dist/project/manifest.js +73 -0
  142. package/dist/project/manifest.js.map +1 -0
  143. package/dist/project/paths.d.ts +25 -0
  144. package/dist/project/paths.d.ts.map +1 -0
  145. package/dist/project/paths.js +51 -0
  146. package/dist/project/paths.js.map +1 -0
  147. package/dist/project/plan.d.ts +42 -0
  148. package/dist/project/plan.d.ts.map +1 -0
  149. package/dist/project/plan.js +251 -0
  150. package/dist/project/plan.js.map +1 -0
  151. package/dist/prompts/index.d.ts +24 -0
  152. package/dist/prompts/index.d.ts.map +1 -0
  153. package/dist/prompts/index.js +111 -0
  154. package/dist/prompts/index.js.map +1 -0
  155. package/dist/recipes/index.d.ts +25 -0
  156. package/dist/recipes/index.d.ts.map +1 -0
  157. package/dist/recipes/index.js +77 -0
  158. package/dist/recipes/index.js.map +1 -0
  159. package/dist/stack/infer.d.ts +4 -0
  160. package/dist/stack/infer.d.ts.map +1 -0
  161. package/dist/stack/infer.js +165 -0
  162. package/dist/stack/infer.js.map +1 -0
  163. package/dist/stack/lwd.d.ts +24 -0
  164. package/dist/stack/lwd.d.ts.map +1 -0
  165. package/dist/stack/lwd.js +62 -0
  166. package/dist/stack/lwd.js.map +1 -0
  167. package/dist/stack/lwdtoml.d.ts +38 -0
  168. package/dist/stack/lwdtoml.d.ts.map +1 -0
  169. package/dist/stack/lwdtoml.js +57 -0
  170. package/dist/stack/lwdtoml.js.map +1 -0
  171. package/dist/stack/manifest.d.ts +15 -0
  172. package/dist/stack/manifest.d.ts.map +1 -0
  173. package/dist/stack/manifest.js +86 -0
  174. package/dist/stack/manifest.js.map +1 -0
  175. package/dist/stack/types.d.ts +48 -0
  176. package/dist/stack/types.d.ts.map +1 -0
  177. package/dist/stack/types.js +5 -0
  178. package/dist/stack/types.js.map +1 -0
  179. package/dist/stack/wire.d.ts +40 -0
  180. package/dist/stack/wire.d.ts.map +1 -0
  181. package/dist/stack/wire.js +192 -0
  182. package/dist/stack/wire.js.map +1 -0
  183. package/dist/types.d.ts +114 -0
  184. package/dist/types.d.ts.map +1 -0
  185. package/dist/types.js +5 -0
  186. package/dist/types.js.map +1 -0
  187. package/dist/utils/fs.d.ts +13 -0
  188. package/dist/utils/fs.d.ts.map +1 -0
  189. package/dist/utils/fs.js +64 -0
  190. package/dist/utils/fs.js.map +1 -0
  191. package/dist/utils/git.d.ts +17 -0
  192. package/dist/utils/git.d.ts.map +1 -0
  193. package/dist/utils/git.js +49 -0
  194. package/dist/utils/git.js.map +1 -0
  195. package/dist/utils/logger.d.ts +16 -0
  196. package/dist/utils/logger.d.ts.map +1 -0
  197. package/dist/utils/logger.js +21 -0
  198. package/dist/utils/logger.js.map +1 -0
  199. package/dist/utils/shell.d.ts +21 -0
  200. package/dist/utils/shell.d.ts.map +1 -0
  201. package/dist/utils/shell.js +40 -0
  202. package/dist/utils/shell.js.map +1 -0
  203. package/dist/version.d.ts +2 -0
  204. package/dist/version.d.ts.map +1 -0
  205. package/dist/version.js +6 -0
  206. package/dist/version.js.map +1 -0
  207. package/package.json +40 -0
  208. package/skills/obh-add-events/SKILL.md +26 -0
  209. package/skills/obh-generate-audit-rules/SKILL.md +26 -0
  210. package/skills/obh-inspect-project/SKILL.md +26 -0
  211. package/skills/obh-lwd-manifest/SKILL.md +26 -0
  212. package/skills/obh-retrofit-files/SKILL.md +24 -0
  213. package/skills/obh-retrofit-jobs/SKILL.md +26 -0
  214. package/skills/obh-sdk-extraction/SKILL.md +24 -0
  215. package/skills/obh-settings-migration/SKILL.md +24 -0
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.events = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.events = {
6
+ name: "events",
7
+ describe: "Postgres event outbox + dispatcher — durable facts emitted inside your transactions",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "events", { api: true, worker: true });
10
+ // defineEvent requires a zod schema; events peer-depends on zod.
11
+ if (ctx.hasApp("api"))
12
+ plan.addDependency("apps/api", "zod", "^3.23.8");
13
+ plan.create("scripts/migrations.d/events.ts", (0, helpers_1.migrationModule)("events"), "events migrations wiring");
14
+ if (ctx.hasApp("api")) {
15
+ plan.create("apps/api/src/platform/events.ts", registryFile((0, helpers_1.hasNotesExample)(ctx)), "events registry + client");
16
+ // Bridge the in-app bus to the durable outbox — same transaction as the write.
17
+ plan.create("apps/api/src/bus.d/events-outbox.ts", OUTBOX, "forward domain facts to the outbox");
18
+ }
19
+ if (ctx.hasWorker()) {
20
+ plan.create("apps/worker/src/dispatch.d/.gitkeep", "", "event-consumer registration directory");
21
+ plan.create("apps/worker/src/consumers.d/events.ts", WORKER, "events dispatcher (fans facts to registered consumers)");
22
+ }
23
+ plan.patchManifest({ platform: { events: true } });
24
+ plan.nextStep("Run `pnpm migrate` — domain facts land in platform.events and fan out to consumers.");
25
+ },
26
+ };
27
+ function registryFile(notes) {
28
+ const defs = notes
29
+ ? `const notePayload = z.object({ id: z.string() }).passthrough()
30
+
31
+ export const noteCreated = defineEvent({ name: "note.created", version: 1, schema: notePayload })
32
+ export const noteUpdated = defineEvent({ name: "note.updated", version: 1, schema: notePayload })
33
+ export const noteDeleted = defineEvent({ name: "note.deleted", version: 1, schema: notePayload })
34
+
35
+ export const registry = createEventRegistry([noteCreated, noteUpdated, noteDeleted])`
36
+ : `// Declare your domain facts, e.g.:
37
+ // export const thingCreated = defineEvent({ name: "thing.created", version: 1, schema: z.object({ id: z.string() }) })
38
+ export const registry = createEventRegistry([])`;
39
+ return `import { z } from "zod"
40
+ import { createEventClient, createEventRegistry, defineEvent, type EventDb } from "@obh/events"
41
+
42
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
43
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
44
+
45
+ ${defs}
46
+
47
+ export const events = createEventClient({ source: "app", registry })
48
+
49
+ /** Adapt a raw pg transaction to the structural EventDb @obh/events expects. */
50
+ export function asEventDb(tx: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> }): EventDb {
51
+ return { query: (sql: string, params?: unknown[]) => tx.query(sql, params) as never }
52
+ }
53
+ `;
54
+ }
55
+ const OUTBOX = `import { onEmit } from "../bus"
56
+ import { asEventDb, events, WORKSPACE } from "../platform/events"
57
+
58
+ // Every domain fact emitted on the bus is written to the durable @obh/events
59
+ // outbox on the SAME transaction, so nothing is lost after a committed write.
60
+ onEmit(async (tx, name, payload) => {
61
+ await events.emit(asEventDb(tx), name, { workspaceId: WORKSPACE, payload })
62
+ })
63
+ `;
64
+ const WORKER = `// Fans new events into per-consumer deliveries. Each event-consuming capability
65
+ // (analytics, audit, search, …) drops a { name, events } registration into
66
+ // dispatch.d/; this loads them so their deliveries get created.
67
+ import { createEventDispatcher, pgAdapter } from "@obh/events"
68
+ import { readdirSync } from "node:fs"
69
+ import path from "node:path"
70
+ import type { WorkerContext } from "../context"
71
+
72
+ interface DispatchConsumer {
73
+ name: string
74
+ events: string[]
75
+ }
76
+
77
+ function loadDispatchConsumers(): DispatchConsumer[] {
78
+ const dir = path.join(__dirname, "..", "dispatch.d")
79
+ const out: DispatchConsumer[] = []
80
+ try {
81
+ for (const file of readdirSync(dir).sort()) {
82
+ if (!/\\.(ts|js)$/.test(file) || file.endsWith(".d.ts")) continue
83
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
84
+ const mod = require(path.join(dir, file))
85
+ if (mod.consumer) out.push(mod.consumer as DispatchConsumer)
86
+ }
87
+ } catch {
88
+ // no dispatch.d yet
89
+ }
90
+ return out
91
+ }
92
+
93
+ let dispatcher: ReturnType<typeof createEventDispatcher>
94
+
95
+ export function init(ctx: WorkerContext): void {
96
+ dispatcher = createEventDispatcher({
97
+ db: pgAdapter(ctx.pool),
98
+ instanceId: "worker",
99
+ consumers: loadDispatchConsumers(),
100
+ })
101
+ }
102
+
103
+ export async function tick(): Promise<void> {
104
+ await dispatcher.tick()
105
+ }
106
+ `;
107
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../../src/capabilities/events.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,MAAM,GAAe;IAChC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,qFAAqF;IAC/F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACpE,iEAAiE;QACjE,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAA,yBAAe,EAAC,QAAQ,CAAC,EAAE,0BAA0B,CAAC,CAAA;QAEpG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,iCAAiC,EAAE,YAAY,CAAC,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC,EAAE,0BAA0B,CAAC,CAAA;YAC9G,+EAA+E;YAC/E,IAAI,CAAC,MAAM,CAAC,qCAAqC,EAAE,MAAM,EAAE,oCAAoC,CAAC,CAAA;QAClG,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,qCAAqC,EAAE,EAAE,EAAE,uCAAuC,CAAC,CAAA;YAC/F,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,MAAM,EAAE,wDAAwD,CAAC,CAAA;QACxH,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAClD,IAAI,CAAC,QAAQ,CAAC,qFAAqF,CAAC,CAAA;IACtG,CAAC;CACF,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC;;;;;;qFAM+E;QACjF,CAAC,CAAC;;gDAE0C,CAAA;IAC9C,OAAO;;;;;;EAMP,IAAI;;;;;;;;CAQL,CAAA;AACD,CAAC;AAED,MAAM,MAAM,GAAG;;;;;;;;CAQd,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Cd,CAAA"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const files: Capability;
3
+ //# sourceMappingURL=files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../src/capabilities/files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,KAAK,EAAE,UAqBnB,CAAA"}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.files = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.files = {
6
+ name: "files",
7
+ describe: "File metadata + signed URLs over S3-compatible storage (product handles file_id only)",
8
+ apply(ctx, plan) {
9
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "files", { api: true });
10
+ plan.create("scripts/migrations.d/files.ts", (0, helpers_1.migrationModule)("files"), "files migrations wiring");
11
+ if (ctx.hasApp("api")) {
12
+ plan.create("apps/api/src/platform/files.ts", CLIENT, "files client + S3 storage provider");
13
+ plan.create("apps/api/src/routes/files.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "signed upload/download routes");
14
+ }
15
+ plan.addEnvVar({ name: "S3_ENDPOINT", example: "http://localhost:9000", comment: "S3-compatible endpoint (MinIO in dev); omit for real AWS S3" });
16
+ plan.addEnvVar({ name: "S3_REGION", example: "us-east-1" });
17
+ plan.addEnvVar({ name: "S3_BUCKET", example: `${ctx.manifest?.name ?? "app"}-files` });
18
+ plan.addEnvVar({ name: "S3_ACCESS_KEY_ID", example: "minioadmin", comment: "secret in prod — set via lwd secret set", secret: true });
19
+ plan.addEnvVar({ name: "S3_SECRET_ACCESS_KEY", example: "minioadmin", comment: "secret in prod — set via lwd secret set", secret: true });
20
+ plan.patchManifest({ platform: { files: true } });
21
+ plan.nextStep("Run `pnpm migrate`. For local dev run MinIO and create the S3_BUCKET.");
22
+ },
23
+ };
24
+ const CLIENT = `import { createFilesClient, createS3StorageProvider, pgAdapter } from "@obh/files"
25
+ import { pool } from "../db"
26
+
27
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
28
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
29
+
30
+ // S3-compatible provider: AWS S3, MinIO, Cloudflare R2, Backblaze B2, etc.
31
+ // forcePathStyle defaults to true whenever an endpoint is set (MinIO needs it).
32
+ const storage = createS3StorageProvider({
33
+ endpoint: process.env.S3_ENDPOINT,
34
+ region: process.env.S3_REGION ?? "us-east-1",
35
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
36
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
37
+ })
38
+
39
+ // The bucket lives on the client; the storage provider is bucket-agnostic.
40
+ export const files = createFilesClient({ storage, bucket: process.env.S3_BUCKET! })
41
+
42
+ // Client methods take a db/tx handle as their first argument so metadata writes
43
+ // can join a transaction. Here we hand them a pool-backed adapter.
44
+ export const filesDb = pgAdapter(pool)
45
+ `;
46
+ const ROUTE_HONO = `import type { Hono } from "hono"
47
+ import { files, filesDb, WORKSPACE } from "../platform/files"
48
+
49
+ export function register(app: Hono): void {
50
+ // 1. Ask for a signed upload URL, then PUT the bytes straight to storage.
51
+ app.post("/files/upload-url", async (c) => {
52
+ const body = await c.req.json<{ originalName?: string; contentType?: string; sizeBytes?: number }>()
53
+ if (!body.originalName) return c.json({ error: "originalName is required" }, 400)
54
+ return c.json(
55
+ await files.createUpload(filesDb, {
56
+ workspaceId: WORKSPACE,
57
+ originalName: body.originalName,
58
+ contentType: body.contentType ?? null,
59
+ sizeBytes: body.sizeBytes ?? null,
60
+ }),
61
+ )
62
+ })
63
+
64
+ // 2. Confirm the upload completed (the server verifies via a HEAD request).
65
+ app.post("/files/:id/complete", async (c) => {
66
+ return c.json(await files.completeUpload(filesDb, { workspaceId: WORKSPACE, fileId: c.req.param("id") }))
67
+ })
68
+
69
+ // 3. Hand back a short-lived signed download URL.
70
+ app.get("/files/:id/download-url", async (c) => {
71
+ return c.json(await files.createDownloadUrl(filesDb, { workspaceId: WORKSPACE, fileId: c.req.param("id") }))
72
+ })
73
+ }
74
+ `;
75
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
76
+ import { files, filesDb, WORKSPACE } from "../platform/files"
77
+
78
+ export function register(app: Express): void {
79
+ app.post("/files/upload-url", async (req: Request, res: Response) => {
80
+ if (!req.body.originalName) return res.status(400).json({ error: "originalName is required" })
81
+ res.json(
82
+ await files.createUpload(filesDb, {
83
+ workspaceId: WORKSPACE,
84
+ originalName: req.body.originalName,
85
+ contentType: req.body.contentType ?? null,
86
+ sizeBytes: req.body.sizeBytes ?? null,
87
+ }),
88
+ )
89
+ })
90
+
91
+ app.post("/files/:id/complete", async (req: Request, res: Response) => {
92
+ res.json(await files.completeUpload(filesDb, { workspaceId: WORKSPACE, fileId: req.params.id }))
93
+ })
94
+
95
+ app.get("/files/:id/download-url", async (req: Request, res: Response) => {
96
+ res.json(await files.createDownloadUrl(filesDb, { workspaceId: WORKSPACE, fileId: req.params.id }))
97
+ })
98
+ }
99
+ `;
100
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../../src/capabilities/files.ts"],"names":[],"mappings":";;;AACA,uCAA6E;AAEhE,QAAA,KAAK,GAAe;IAC/B,IAAI,EAAE,OAAO;IACb,QAAQ,EAAE,uFAAuF;IACjG,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QACrD,IAAI,CAAC,MAAM,CAAC,+BAA+B,EAAE,IAAA,yBAAe,EAAC,OAAO,CAAC,EAAE,yBAAyB,CAAC,CAAA;QAEjG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,MAAM,EAAE,oCAAoC,CAAC,CAAA;YAC3F,IAAI,CAAC,MAAM,CAAC,8BAA8B,EAAE,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EAAE,+BAA+B,CAAC,CAAA;QACzI,CAAC;QAED,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,uBAAuB,EAAE,OAAO,EAAE,6DAA6D,EAAE,CAAC,CAAA;QACjJ,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAA;QAC3D,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,QAAQ,EAAE,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC,CAAA;QACtF,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,kBAAkB,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,yCAAyC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QACrI,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,yCAAyC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAEzI,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACjD,IAAI,CAAC,QAAQ,CAAC,uEAAuE,CAAC,CAAA;IACxF,CAAC;CACF,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;CAqBd,CAAA;AAED,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA4BlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;;;;;;;;;;;;;;;CAwBrB,CAAA"}
@@ -0,0 +1,27 @@
1
+ import type { ApiFramework } from "../types";
2
+ import type { ProjectContext } from "../project/context";
3
+ import type { Plan } from "../project/plan";
4
+ /** The API framework the project chose (defaults to hono). */
5
+ export declare function apiFramework(ctx: ProjectContext): ApiFramework;
6
+ /** Whether the project was scaffolded with the notes example domain. */
7
+ export declare function hasNotesExample(ctx: ProjectContext): boolean;
8
+ /** Map a capability's short name to its published @obh package name. */
9
+ export declare const OBH_PACKAGE: Record<string, string>;
10
+ export interface PackageTargets {
11
+ /** Add to apps/api (client/emit side). */
12
+ api?: boolean;
13
+ /** Add to apps/worker (consumer side). */
14
+ worker?: boolean;
15
+ }
16
+ /**
17
+ * Add an @obh platform package to the right package.json files. It is always
18
+ * added to the root (so scripts/migrate.ts can resolve it) plus any requested
19
+ * app that actually exists.
20
+ */
21
+ export declare function addPlatformPackage(plan: Plan, ctx: ProjectContext, cap: string, targets: PackageTargets): void;
22
+ /**
23
+ * Standard migrations.d module. Every @obh package exports `pgAdapter` and
24
+ * `runMigrations`, so the wiring is uniform.
25
+ */
26
+ export declare function migrationModule(cap: string): string;
27
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../src/capabilities/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAA;AAC5C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAI3C,8DAA8D;AAC9D,wBAAgB,YAAY,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY,CAE9D;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,GAAG,EAAE,cAAc,GAAG,OAAO,CAE5D;AAED,wEAAwE;AACxE,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAa9C,CAAA;AAED,MAAM,WAAW,cAAc;IAC7B,0CAA0C;IAC1C,GAAG,CAAC,EAAE,OAAO,CAAA;IACb,0CAA0C;IAC1C,MAAM,CAAC,EAAE,OAAO,CAAA;CACjB;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,cAAc,EACnB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,cAAc,GACtB,IAAI,CAMN;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAWnD"}
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OBH_PACKAGE = void 0;
4
+ exports.apiFramework = apiFramework;
5
+ exports.hasNotesExample = hasNotesExample;
6
+ exports.addPlatformPackage = addPlatformPackage;
7
+ exports.migrationModule = migrationModule;
8
+ const OBH_VERSION = "^0.1.0";
9
+ /** The API framework the project chose (defaults to hono). */
10
+ function apiFramework(ctx) {
11
+ return ctx.manifest?.config?.apiFramework ?? "hono";
12
+ }
13
+ /** Whether the project was scaffolded with the notes example domain. */
14
+ function hasNotesExample(ctx) {
15
+ return ctx.manifest?.config?.example === "notes";
16
+ }
17
+ /** Map a capability's short name to its published @obh package name. */
18
+ exports.OBH_PACKAGE = {
19
+ events: "@obh/events",
20
+ jobs: "@obh/jobs",
21
+ files: "@obh/files",
22
+ audit: "@obh/audit",
23
+ settings: "@obh/settings",
24
+ "api-keys": "@obh/api-keys",
25
+ webhooks: "@obh/webhooks",
26
+ "import-export": "@obh/import-export",
27
+ entitlements: "@obh/entitlements",
28
+ search: "@obh/search",
29
+ analytics: "@obh/analytics",
30
+ notifications: "@obh/notifications",
31
+ };
32
+ /**
33
+ * Add an @obh platform package to the right package.json files. It is always
34
+ * added to the root (so scripts/migrate.ts can resolve it) plus any requested
35
+ * app that actually exists.
36
+ */
37
+ function addPlatformPackage(plan, ctx, cap, targets) {
38
+ const pkg = exports.OBH_PACKAGE[cap];
39
+ if (!pkg)
40
+ return;
41
+ plan.addDependency(".", pkg, OBH_VERSION);
42
+ if (targets.api && ctx.hasApp("api"))
43
+ plan.addDependency("apps/api", pkg, OBH_VERSION);
44
+ if (targets.worker && ctx.hasWorker())
45
+ plan.addDependency("apps/worker", pkg, OBH_VERSION);
46
+ }
47
+ /**
48
+ * Standard migrations.d module. Every @obh package exports `pgAdapter` and
49
+ * `runMigrations`, so the wiring is uniform.
50
+ */
51
+ function migrationModule(cap) {
52
+ const pkg = exports.OBH_PACKAGE[cap];
53
+ return `import { pgAdapter, runMigrations } from "${pkg}"
54
+ import type { Pool } from "pg"
55
+
56
+ // Registered automatically by scripts/migrate.ts. Creates this primitive's
57
+ // tables under the "platform" schema.
58
+ export async function migrate(pool: Pool): Promise<void> {
59
+ await runMigrations(pgAdapter(pool))
60
+ }
61
+ `;
62
+ }
63
+ //# sourceMappingURL=helpers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.js","sourceRoot":"","sources":["../../src/capabilities/helpers.ts"],"names":[],"mappings":";;;AAOA,oCAEC;AAGD,0CAEC;AA8BD,gDAWC;AAMD,0CAWC;AApED,MAAM,WAAW,GAAG,QAAQ,CAAA;AAE5B,8DAA8D;AAC9D,SAAgB,YAAY,CAAC,GAAmB;IAC9C,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,IAAI,MAAM,CAAA;AACrD,CAAC;AAED,wEAAwE;AACxE,SAAgB,eAAe,CAAC,GAAmB;IACjD,OAAO,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,KAAK,OAAO,CAAA;AAClD,CAAC;AAED,wEAAwE;AAC3D,QAAA,WAAW,GAA2B;IACjD,MAAM,EAAE,aAAa;IACrB,IAAI,EAAE,WAAW;IACjB,KAAK,EAAE,YAAY;IACnB,KAAK,EAAE,YAAY;IACnB,QAAQ,EAAE,eAAe;IACzB,UAAU,EAAE,eAAe;IAC3B,QAAQ,EAAE,eAAe;IACzB,eAAe,EAAE,oBAAoB;IACrC,YAAY,EAAE,mBAAmB;IACjC,MAAM,EAAE,aAAa;IACrB,SAAS,EAAE,gBAAgB;IAC3B,aAAa,EAAE,oBAAoB;CACpC,CAAA;AASD;;;;GAIG;AACH,SAAgB,kBAAkB,CAChC,IAAU,EACV,GAAmB,EACnB,GAAW,EACX,OAAuB;IAEvB,MAAM,GAAG,GAAG,mBAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,IAAI,CAAC,GAAG;QAAE,OAAM;IAChB,IAAI,CAAC,aAAa,CAAC,GAAG,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IACzC,IAAI,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;IACtF,IAAI,OAAO,CAAC,MAAM,IAAI,GAAG,CAAC,SAAS,EAAE;QAAE,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,GAAG,EAAE,WAAW,CAAC,CAAA;AAC5F,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe,CAAC,GAAW;IACzC,MAAM,GAAG,GAAG,mBAAW,CAAC,GAAG,CAAC,CAAA;IAC5B,OAAO,6CAA6C,GAAG;;;;;;;;CAQxD,CAAA;AACD,CAAC"}
@@ -0,0 +1,3 @@
1
+ import type { Capability } from "./types";
2
+ export declare const importExport: Capability;
3
+ //# sourceMappingURL=import-export.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-export.d.ts","sourceRoot":"","sources":["../../src/capabilities/import-export.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,YAAY,EAAE,UAkC1B,CAAA"}
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.importExport = void 0;
4
+ const helpers_1 = require("./helpers");
5
+ exports.importExport = {
6
+ name: "import-export",
7
+ requires: ["files", "jobs"],
8
+ describe: "CSV import/export engine — files in/out, run as background jobs",
9
+ apply(ctx, plan) {
10
+ (0, helpers_1.addPlatformPackage)(plan, ctx, "import-export", { api: true, worker: true });
11
+ plan.create("scripts/migrations.d/import-export.ts", (0, helpers_1.migrationModule)("import-export"), "import-export migrations wiring");
12
+ // defineImport fields carry Zod schemas; the FileStore port wires to @obh/files.
13
+ if (ctx.hasApp("api")) {
14
+ plan.addDependency("apps/api", "zod", "^3.23.8");
15
+ plan.addDependency("apps/api", "@obh/files", "^0.1.0");
16
+ }
17
+ if (ctx.hasWorker()) {
18
+ plan.addDependency("apps/worker", "zod", "^3.23.8");
19
+ plan.addDependency("apps/worker", "@obh/files", "^0.1.0");
20
+ }
21
+ const notes = (0, helpers_1.hasNotesExample)(ctx);
22
+ if (ctx.hasApp("api")) {
23
+ plan.create("apps/api/src/platform/import-export.ts", apiPlatform(notes), "import/export registry + client");
24
+ plan.create("apps/api/src/routes/import-export.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "start CSV import/export routes");
25
+ }
26
+ if (ctx.hasWorker()) {
27
+ plan.create("apps/worker/src/consumers.d/import-export.ts", worker(notes), "import/export job worker tick");
28
+ }
29
+ plan.patchManifest({ platform: { "import-export": true } });
30
+ plan.nextStep("Run `pnpm migrate`. Start imports/exports via the API; the worker runs them as jobs, reading/writing CSV through the files service.");
31
+ },
32
+ };
33
+ // A FileStore port over @obh/files plus the import/export registry, shared by the
34
+ // API client and the worker (each builds its own — the worker never imports from
35
+ // apps/api). `pool` is passed in so this works at module scope or inside init().
36
+ function portBuilder(notes) {
37
+ const imports = notes
38
+ ? ` defineImport({
39
+ type: "note",
40
+ fields: [
41
+ { key: "title", label: "Title", required: true, schema: z.string().min(1) },
42
+ { key: "body", label: "Body", schema: z.string().optional() },
43
+ ],
44
+ commitRow: async (_ctx, row) => {
45
+ // Replace with your own domain write. Must be idempotent.
46
+ const id = randomUUID()
47
+ await pool.query("insert into notes (id, title, body) values ($1, $2, $3)", [
48
+ id,
49
+ String(row.mapped.title ?? ""),
50
+ String(row.mapped.body ?? ""),
51
+ ])
52
+ return { entityType: "note", entityId: id }
53
+ },
54
+ }),`
55
+ : ` // defineImport({ type: "thing", fields: [{ key: "name", label: "Name", required: true, schema: z.string().min(1) }], commitRow: async (ctx, row) => ({ entityType: "thing", entityId: "…" }) }),`;
56
+ const exports = notes
57
+ ? ` defineExport({
58
+ type: "note",
59
+ columns: [
60
+ { key: "title", label: "Title" },
61
+ { key: "body", label: "Body" },
62
+ ],
63
+ loadRows: async () => {
64
+ const res = await pool.query("select title, body from notes order by created_at desc")
65
+ return res.rows as Array<Record<string, unknown>>
66
+ },
67
+ }),`
68
+ : ` // defineExport({ type: "thing", columns: [{ key: "name", label: "Name" }], loadRows: async (ctx, filters) => [] }),`;
69
+ return `function buildClient(pool: Pool): ImportExportClient {
70
+ const filesDb = filesPgAdapter(pool)
71
+ const filesClient = createFilesClient({
72
+ bucket: process.env.S3_BUCKET!,
73
+ storage: createS3StorageProvider({
74
+ endpoint: process.env.S3_ENDPOINT,
75
+ region: process.env.S3_REGION ?? "us-east-1",
76
+ accessKeyId: process.env.S3_ACCESS_KEY_ID!,
77
+ secretAccessKey: process.env.S3_SECRET_ACCESS_KEY!,
78
+ }),
79
+ })
80
+
81
+ // read via a signed download URL; write by creating a file and PUTting the
82
+ // bytes to its signed upload URL, then confirming the upload.
83
+ const files: FileStore = {
84
+ async read({ workspaceId, fileId }) {
85
+ const { url } = await filesClient.createDownloadUrl(filesDb, { workspaceId, fileId })
86
+ const res = await fetch(url)
87
+ if (!res.ok) throw new Error(\`file download failed (\${res.status})\`)
88
+ return Buffer.from(await res.arrayBuffer())
89
+ },
90
+ async create({ workspaceId, filename, contentType, content, createdBy }) {
91
+ const upload = await filesClient.createUpload(filesDb, {
92
+ workspaceId,
93
+ originalName: filename,
94
+ contentType,
95
+ createdBy: createdBy ?? null,
96
+ })
97
+ const put = await fetch(upload.uploadUrl, {
98
+ method: "PUT",
99
+ headers: upload.headers,
100
+ body: typeof content === "string" ? content : content.toString("utf8"),
101
+ })
102
+ if (!put.ok) throw new Error(\`file upload failed (\${put.status})\`)
103
+ await filesClient.completeUpload(filesDb, { workspaceId, fileId: upload.fileId })
104
+ return { fileId: upload.fileId }
105
+ },
106
+ }
107
+
108
+ const registry = createRegistry({
109
+ imports: [
110
+ ${imports}
111
+ ],
112
+ exports: [
113
+ ${exports}
114
+ ],
115
+ })
116
+
117
+ return createImportExportClient({ db: pgAdapter(pool), files, registry })
118
+ }`;
119
+ }
120
+ function apiPlatform(notes) {
121
+ return `import { createFilesClient, createS3StorageProvider, pgAdapter as filesPgAdapter } from "@obh/files"
122
+ import {
123
+ createImportExportClient,
124
+ createRegistry,
125
+ defineExport,
126
+ defineImport,
127
+ pgAdapter,
128
+ type FileStore,
129
+ type ImportExportClient,
130
+ } from "@obh/import-export"
131
+ import { randomUUID } from "node:crypto"
132
+ import type { Pool } from "pg"
133
+ import { z } from "zod"
134
+ import { pool } from "../db"
135
+
136
+ // Every platform row is scoped to a workspace. Single-tenant apps can leave this.
137
+ export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
138
+
139
+ ${portBuilder(notes)}
140
+
141
+ export const importExport = buildClient(pool)
142
+ `;
143
+ }
144
+ const ROUTE_HONO = `import type { Hono } from "hono"
145
+ import { createJobClient, pgAdapter as jobsPgAdapter } from "@obh/jobs"
146
+ import { importExport, WORKSPACE } from "../platform/import-export"
147
+ import { pool } from "../db"
148
+
149
+ // Processing runs in the background via @obh/jobs; the worker registers the
150
+ // handlers under these snake_case command names.
151
+ const jobs = createJobClient({ source: "api" })
152
+ const jobsDb = jobsPgAdapter(pool)
153
+
154
+ export function register(app: Hono): void {
155
+ // Start an import over an already-uploaded CSV (sourceFileId from the files service).
156
+ app.post("/api/imports", async (c) => {
157
+ const body = await c.req.json<{ sourceFileId: string }>()
158
+ const batch = await importExport.imports.createBatch({
159
+ workspaceId: WORKSPACE,
160
+ importType: "note",
161
+ sourceFileId: body.sourceFileId,
162
+ })
163
+ await jobs.enqueue(jobsDb, "import_parse_csv", {
164
+ workspaceId: WORKSPACE,
165
+ payload: { workspaceId: WORKSPACE, batchId: batch.id },
166
+ })
167
+ return c.json(batch, 202)
168
+ })
169
+
170
+ // Start an export; the worker generates the CSV and stores it via files.
171
+ app.post("/api/exports", async (c) => {
172
+ const exp = await importExport.exports.createExport({
173
+ workspaceId: WORKSPACE,
174
+ exportType: "note",
175
+ })
176
+ await jobs.enqueue(jobsDb, "export_generate_csv", {
177
+ workspaceId: WORKSPACE,
178
+ payload: { workspaceId: WORKSPACE, exportId: exp.id },
179
+ })
180
+ return c.json(exp, 202)
181
+ })
182
+ }
183
+ `;
184
+ const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
185
+ import { createJobClient, pgAdapter as jobsPgAdapter } from "@obh/jobs"
186
+ import { importExport, WORKSPACE } from "../platform/import-export"
187
+ import { pool } from "../db"
188
+
189
+ const jobs = createJobClient({ source: "api" })
190
+ const jobsDb = jobsPgAdapter(pool)
191
+
192
+ export function register(app: Express): void {
193
+ app.post("/api/imports", async (req: Request, res: Response) => {
194
+ const batch = await importExport.imports.createBatch({
195
+ workspaceId: WORKSPACE,
196
+ importType: "note",
197
+ sourceFileId: req.body.sourceFileId,
198
+ })
199
+ await jobs.enqueue(jobsDb, "import_parse_csv", {
200
+ workspaceId: WORKSPACE,
201
+ payload: { workspaceId: WORKSPACE, batchId: batch.id },
202
+ })
203
+ res.status(202).json(batch)
204
+ })
205
+
206
+ app.post("/api/exports", async (_req: Request, res: Response) => {
207
+ const exp = await importExport.exports.createExport({
208
+ workspaceId: WORKSPACE,
209
+ exportType: "note",
210
+ })
211
+ await jobs.enqueue(jobsDb, "export_generate_csv", {
212
+ workspaceId: WORKSPACE,
213
+ payload: { workspaceId: WORKSPACE, exportId: exp.id },
214
+ })
215
+ res.status(202).json(exp)
216
+ })
217
+ }
218
+ `;
219
+ function worker(notes) {
220
+ return `// Job-driven CSV pipeline. createImportWorker/createExportWorker expose handlers
221
+ // keyed by the recommended (dotted) job names; we register them with @obh/jobs
222
+ // under snake_case commands the API enqueues, then drain the queue each tick.
223
+ import { createFilesClient, createS3StorageProvider, pgAdapter as filesPgAdapter } from "@obh/files"
224
+ import {
225
+ createExportWorker,
226
+ createImportExportClient,
227
+ createImportWorker,
228
+ createRegistry,
229
+ defineExport,
230
+ defineImport,
231
+ pgAdapter,
232
+ type FileStore,
233
+ type ImportExportClient,
234
+ } from "@obh/import-export"
235
+ import { createJobRegistry, createWorker, defineJob, pgAdapter as jobsPgAdapter } from "@obh/jobs"
236
+ import { randomUUID } from "node:crypto"
237
+ import type { Pool } from "pg"
238
+ import { z } from "zod"
239
+ import type { WorkerContext } from "../context"
240
+
241
+ let worker: ReturnType<typeof createWorker>
242
+
243
+ ${portBuilder(notes)}
244
+
245
+ export function init(ctx: WorkerContext): void {
246
+ const client = buildClient(ctx.pool)
247
+ const imp = createImportWorker(client)
248
+ const exp = createExportWorker(client)
249
+
250
+ // Each qport handler validates its own payload (workspaceId + batch/export id),
251
+ // so the job schema stays permissive.
252
+ const registry = createJobRegistry([
253
+ defineJob({ name: "import_parse_csv", version: 1, schema: z.any(), handler: (_c, p) => imp.handlers["import.parse_csv"](p) }),
254
+ defineJob({ name: "import_validate_batch", version: 1, schema: z.any(), handler: (_c, p) => imp.handlers["import.validate_batch"](p) }),
255
+ defineJob({ name: "import_commit_batch", version: 1, schema: z.any(), handler: (_c, p) => imp.handlers["import.commit_batch"](p) }),
256
+ defineJob({ name: "export_generate_csv", version: 1, schema: z.any(), handler: (_c, p) => exp.handlers["export.generate_csv"](p) }),
257
+ ])
258
+
259
+ worker = createWorker({ db: jobsPgAdapter(ctx.pool), registry, instanceId: "worker" })
260
+ }
261
+
262
+ export async function tick(): Promise<void> {
263
+ await worker.tick()
264
+ }
265
+ `;
266
+ }
267
+ //# sourceMappingURL=import-export.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import-export.js","sourceRoot":"","sources":["../../src/capabilities/import-export.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import type { CapabilityName } from "../types";
2
+ import type { Capability } from "./types";
3
+ export type { Capability } from "./types";
4
+ /** Every OBH platform primitive Forge can install. */
5
+ export declare const CAPABILITIES: Partial<Record<CapabilityName, Capability>>;
6
+ export declare function getCapability(name: CapabilityName): Capability | undefined;
7
+ export declare function isImplemented(name: CapabilityName): boolean;
8
+ /**
9
+ * Expand a requested set of capabilities to include their prerequisites, then
10
+ * return them in dependency order (requires-before-dependents). Unknown names
11
+ * are dropped by the caller after reporting.
12
+ */
13
+ export declare function resolveOrder(requested: CapabilityName[]): CapabilityName[];
14
+ /** Prerequisites of a capability that are not already installed/requested. */
15
+ export declare function missingPrerequisites(name: CapabilityName, installed: (n: CapabilityName) => boolean): CapabilityName[];
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAczC,YAAY,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAEzC,sDAAsD;AACtD,eAAO,MAAM,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC,CAapE,CAAA;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,UAAU,GAAG,SAAS,CAE1E;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAE3D;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,cAAc,EAAE,GAAG,cAAc,EAAE,CAe1E;AAED,8EAA8E;AAC9E,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,CAAC,CAAC,EAAE,cAAc,KAAK,OAAO,GACxC,cAAc,EAAE,CAIlB"}