@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.
- package/LICENSE +21 -0
- package/README.md +146 -0
- package/dist/capabilities/analytics.d.ts +3 -0
- package/dist/capabilities/analytics.d.ts.map +1 -0
- package/dist/capabilities/analytics.js +91 -0
- package/dist/capabilities/analytics.js.map +1 -0
- package/dist/capabilities/api-keys.d.ts +3 -0
- package/dist/capabilities/api-keys.d.ts.map +1 -0
- package/dist/capabilities/api-keys.js +90 -0
- package/dist/capabilities/api-keys.js.map +1 -0
- package/dist/capabilities/audit.d.ts +3 -0
- package/dist/capabilities/audit.d.ts.map +1 -0
- package/dist/capabilities/audit.js +74 -0
- package/dist/capabilities/audit.js.map +1 -0
- package/dist/capabilities/entitlements.d.ts +3 -0
- package/dist/capabilities/entitlements.d.ts.map +1 -0
- package/dist/capabilities/entitlements.js +94 -0
- package/dist/capabilities/entitlements.js.map +1 -0
- package/dist/capabilities/events.d.ts +3 -0
- package/dist/capabilities/events.d.ts.map +1 -0
- package/dist/capabilities/events.js +107 -0
- package/dist/capabilities/events.js.map +1 -0
- package/dist/capabilities/files.d.ts +3 -0
- package/dist/capabilities/files.d.ts.map +1 -0
- package/dist/capabilities/files.js +100 -0
- package/dist/capabilities/files.js.map +1 -0
- package/dist/capabilities/helpers.d.ts +27 -0
- package/dist/capabilities/helpers.d.ts.map +1 -0
- package/dist/capabilities/helpers.js +63 -0
- package/dist/capabilities/helpers.js.map +1 -0
- package/dist/capabilities/import-export.d.ts +3 -0
- package/dist/capabilities/import-export.d.ts.map +1 -0
- package/dist/capabilities/import-export.js +267 -0
- package/dist/capabilities/import-export.js.map +1 -0
- package/dist/capabilities/index.d.ts +16 -0
- package/dist/capabilities/index.d.ts.map +1 -0
- package/dist/capabilities/index.js +71 -0
- package/dist/capabilities/index.js.map +1 -0
- package/dist/capabilities/jobs.d.ts +3 -0
- package/dist/capabilities/jobs.d.ts.map +1 -0
- package/dist/capabilities/jobs.js +117 -0
- package/dist/capabilities/jobs.js.map +1 -0
- package/dist/capabilities/notifications.d.ts +3 -0
- package/dist/capabilities/notifications.d.ts.map +1 -0
- package/dist/capabilities/notifications.js +113 -0
- package/dist/capabilities/notifications.js.map +1 -0
- package/dist/capabilities/search.d.ts +3 -0
- package/dist/capabilities/search.d.ts.map +1 -0
- package/dist/capabilities/search.js +121 -0
- package/dist/capabilities/search.js.map +1 -0
- package/dist/capabilities/settings.d.ts +3 -0
- package/dist/capabilities/settings.d.ts.map +1 -0
- package/dist/capabilities/settings.js +75 -0
- package/dist/capabilities/settings.js.map +1 -0
- package/dist/capabilities/types.d.ts +16 -0
- package/dist/capabilities/types.d.ts.map +1 -0
- package/dist/capabilities/types.js +3 -0
- package/dist/capabilities/types.js.map +1 -0
- package/dist/capabilities/webhooks.d.ts +3 -0
- package/dist/capabilities/webhooks.d.ts.map +1 -0
- package/dist/capabilities/webhooks.js +117 -0
- package/dist/capabilities/webhooks.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +60 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/add.d.ts +3 -0
- package/dist/commands/add.d.ts.map +1 -0
- package/dist/commands/add.js +88 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +94 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +41 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/inspect.d.ts +3 -0
- package/dist/commands/inspect.d.ts.map +1 -0
- package/dist/commands/inspect.js +64 -0
- package/dist/commands/inspect.js.map +1 -0
- package/dist/commands/new.d.ts +18 -0
- package/dist/commands/new.d.ts.map +1 -0
- package/dist/commands/new.js +149 -0
- package/dist/commands/new.js.map +1 -0
- package/dist/commands/shared.d.ts +14 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +36 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/skill.d.ts +3 -0
- package/dist/commands/skill.d.ts.map +1 -0
- package/dist/commands/skill.js +59 -0
- package/dist/commands/skill.js.map +1 -0
- package/dist/commands/stack.d.ts +13 -0
- package/dist/commands/stack.d.ts.map +1 -0
- package/dist/commands/stack.js +192 -0
- package/dist/commands/stack.js.map +1 -0
- package/dist/generators/api.d.ts +16 -0
- package/dist/generators/api.d.ts.map +1 -0
- package/dist/generators/api.js +375 -0
- package/dist/generators/api.js.map +1 -0
- package/dist/generators/lwd.d.ts +12 -0
- package/dist/generators/lwd.d.ts.map +1 -0
- package/dist/generators/lwd.js +143 -0
- package/dist/generators/lwd.js.map +1 -0
- package/dist/generators/mobile.d.ts +16 -0
- package/dist/generators/mobile.d.ts.map +1 -0
- package/dist/generators/mobile.js +180 -0
- package/dist/generators/mobile.js.map +1 -0
- package/dist/generators/platform-package.d.ts +18 -0
- package/dist/generators/platform-package.d.ts.map +1 -0
- package/dist/generators/platform-package.js +568 -0
- package/dist/generators/platform-package.js.map +1 -0
- package/dist/generators/root.d.ts +15 -0
- package/dist/generators/root.d.ts.map +1 -0
- package/dist/generators/root.js +249 -0
- package/dist/generators/root.js.map +1 -0
- package/dist/generators/sdk.d.ts +14 -0
- package/dist/generators/sdk.d.ts.map +1 -0
- package/dist/generators/sdk.js +137 -0
- package/dist/generators/sdk.js.map +1 -0
- package/dist/generators/web.d.ts +15 -0
- package/dist/generators/web.d.ts.map +1 -0
- package/dist/generators/web.js +203 -0
- package/dist/generators/web.js.map +1 -0
- package/dist/generators/worker.d.ts +13 -0
- package/dist/generators/worker.d.ts.map +1 -0
- package/dist/generators/worker.js +148 -0
- package/dist/generators/worker.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +44 -0
- package/dist/index.js.map +1 -0
- package/dist/project/context.d.ts +24 -0
- package/dist/project/context.d.ts.map +1 -0
- package/dist/project/context.js +63 -0
- package/dist/project/context.js.map +1 -0
- package/dist/project/manifest.d.ts +26 -0
- package/dist/project/manifest.d.ts.map +1 -0
- package/dist/project/manifest.js +73 -0
- package/dist/project/manifest.js.map +1 -0
- package/dist/project/paths.d.ts +25 -0
- package/dist/project/paths.d.ts.map +1 -0
- package/dist/project/paths.js +51 -0
- package/dist/project/paths.js.map +1 -0
- package/dist/project/plan.d.ts +42 -0
- package/dist/project/plan.d.ts.map +1 -0
- package/dist/project/plan.js +251 -0
- package/dist/project/plan.js.map +1 -0
- package/dist/prompts/index.d.ts +24 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +111 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/recipes/index.d.ts +25 -0
- package/dist/recipes/index.d.ts.map +1 -0
- package/dist/recipes/index.js +77 -0
- package/dist/recipes/index.js.map +1 -0
- package/dist/stack/infer.d.ts +4 -0
- package/dist/stack/infer.d.ts.map +1 -0
- package/dist/stack/infer.js +165 -0
- package/dist/stack/infer.js.map +1 -0
- package/dist/stack/lwd.d.ts +24 -0
- package/dist/stack/lwd.d.ts.map +1 -0
- package/dist/stack/lwd.js +62 -0
- package/dist/stack/lwd.js.map +1 -0
- package/dist/stack/lwdtoml.d.ts +38 -0
- package/dist/stack/lwdtoml.d.ts.map +1 -0
- package/dist/stack/lwdtoml.js +57 -0
- package/dist/stack/lwdtoml.js.map +1 -0
- package/dist/stack/manifest.d.ts +15 -0
- package/dist/stack/manifest.d.ts.map +1 -0
- package/dist/stack/manifest.js +86 -0
- package/dist/stack/manifest.js.map +1 -0
- package/dist/stack/types.d.ts +48 -0
- package/dist/stack/types.d.ts.map +1 -0
- package/dist/stack/types.js +5 -0
- package/dist/stack/types.js.map +1 -0
- package/dist/stack/wire.d.ts +40 -0
- package/dist/stack/wire.d.ts.map +1 -0
- package/dist/stack/wire.js +192 -0
- package/dist/stack/wire.js.map +1 -0
- package/dist/types.d.ts +114 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +13 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +64 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +17 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +49 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +16 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +21 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/shell.d.ts +21 -0
- package/dist/utils/shell.d.ts.map +1 -0
- package/dist/utils/shell.js +40 -0
- package/dist/utils/shell.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- package/package.json +40 -0
- package/skills/obh-add-events/SKILL.md +26 -0
- package/skills/obh-generate-audit-rules/SKILL.md +26 -0
- package/skills/obh-inspect-project/SKILL.md +26 -0
- package/skills/obh-lwd-manifest/SKILL.md +26 -0
- package/skills/obh-retrofit-files/SKILL.md +24 -0
- package/skills/obh-retrofit-jobs/SKILL.md +26 -0
- package/skills/obh-sdk-extraction/SKILL.md +24 -0
- package/skills/obh-settings-migration/SKILL.md +24 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CAPABILITIES = void 0;
|
|
4
|
+
exports.getCapability = getCapability;
|
|
5
|
+
exports.isImplemented = isImplemented;
|
|
6
|
+
exports.resolveOrder = resolveOrder;
|
|
7
|
+
exports.missingPrerequisites = missingPrerequisites;
|
|
8
|
+
const events_1 = require("./events");
|
|
9
|
+
const jobs_1 = require("./jobs");
|
|
10
|
+
const files_1 = require("./files");
|
|
11
|
+
const audit_1 = require("./audit");
|
|
12
|
+
const settings_1 = require("./settings");
|
|
13
|
+
const api_keys_1 = require("./api-keys");
|
|
14
|
+
const webhooks_1 = require("./webhooks");
|
|
15
|
+
const import_export_1 = require("./import-export");
|
|
16
|
+
const entitlements_1 = require("./entitlements");
|
|
17
|
+
const search_1 = require("./search");
|
|
18
|
+
const analytics_1 = require("./analytics");
|
|
19
|
+
const notifications_1 = require("./notifications");
|
|
20
|
+
/** Every OBH platform primitive Forge can install. */
|
|
21
|
+
exports.CAPABILITIES = {
|
|
22
|
+
events: events_1.events,
|
|
23
|
+
jobs: jobs_1.jobs,
|
|
24
|
+
files: files_1.files,
|
|
25
|
+
audit: audit_1.audit,
|
|
26
|
+
settings: settings_1.settings,
|
|
27
|
+
"api-keys": api_keys_1.apiKeys,
|
|
28
|
+
webhooks: webhooks_1.webhooks,
|
|
29
|
+
"import-export": import_export_1.importExport,
|
|
30
|
+
entitlements: entitlements_1.entitlements,
|
|
31
|
+
search: search_1.search,
|
|
32
|
+
analytics: analytics_1.analytics,
|
|
33
|
+
notifications: notifications_1.notifications,
|
|
34
|
+
};
|
|
35
|
+
function getCapability(name) {
|
|
36
|
+
return exports.CAPABILITIES[name];
|
|
37
|
+
}
|
|
38
|
+
function isImplemented(name) {
|
|
39
|
+
return exports.CAPABILITIES[name] !== undefined;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Expand a requested set of capabilities to include their prerequisites, then
|
|
43
|
+
* return them in dependency order (requires-before-dependents). Unknown names
|
|
44
|
+
* are dropped by the caller after reporting.
|
|
45
|
+
*/
|
|
46
|
+
function resolveOrder(requested) {
|
|
47
|
+
const ordered = [];
|
|
48
|
+
const seen = new Set();
|
|
49
|
+
const visit = (name) => {
|
|
50
|
+
if (seen.has(name))
|
|
51
|
+
return;
|
|
52
|
+
seen.add(name);
|
|
53
|
+
const cap = exports.CAPABILITIES[name];
|
|
54
|
+
if (!cap)
|
|
55
|
+
return;
|
|
56
|
+
for (const req of cap.requires ?? [])
|
|
57
|
+
visit(req);
|
|
58
|
+
ordered.push(name);
|
|
59
|
+
};
|
|
60
|
+
for (const name of requested)
|
|
61
|
+
visit(name);
|
|
62
|
+
return ordered;
|
|
63
|
+
}
|
|
64
|
+
/** Prerequisites of a capability that are not already installed/requested. */
|
|
65
|
+
function missingPrerequisites(name, installed) {
|
|
66
|
+
const cap = exports.CAPABILITIES[name];
|
|
67
|
+
if (!cap)
|
|
68
|
+
return [];
|
|
69
|
+
return (cap.requires ?? []).filter((r) => !installed(r));
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/capabilities/index.ts"],"names":[],"mappings":";;;AAiCA,sCAEC;AAED,sCAEC;AAOD,oCAeC;AAGD,oDAOC;AArED,qCAAiC;AACjC,iCAA6B;AAC7B,mCAA+B;AAC/B,mCAA+B;AAC/B,yCAAqC;AACrC,yCAAoC;AACpC,yCAAqC;AACrC,mDAA8C;AAC9C,iDAA6C;AAC7C,qCAAiC;AACjC,2CAAuC;AACvC,mDAA+C;AAI/C,sDAAsD;AACzC,QAAA,YAAY,GAAgD;IACvE,MAAM,EAAN,eAAM;IACN,IAAI,EAAJ,WAAI;IACJ,KAAK,EAAL,aAAK;IACL,KAAK,EAAL,aAAK;IACL,QAAQ,EAAR,mBAAQ;IACR,UAAU,EAAE,kBAAO;IACnB,QAAQ,EAAR,mBAAQ;IACR,eAAe,EAAE,4BAAY;IAC7B,YAAY,EAAZ,2BAAY;IACZ,MAAM,EAAN,eAAM;IACN,SAAS,EAAT,qBAAS;IACT,aAAa,EAAb,6BAAa;CACd,CAAA;AAED,SAAgB,aAAa,CAAC,IAAoB;IAChD,OAAO,oBAAY,CAAC,IAAI,CAAC,CAAA;AAC3B,CAAC;AAED,SAAgB,aAAa,CAAC,IAAoB;IAChD,OAAO,oBAAY,CAAC,IAAI,CAAC,KAAK,SAAS,CAAA;AACzC,CAAC;AAED;;;;GAIG;AACH,SAAgB,YAAY,CAAC,SAA2B;IACtD,MAAM,OAAO,GAAqB,EAAE,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAA;IAEtC,MAAM,KAAK,GAAG,CAAC,IAAoB,EAAQ,EAAE;QAC3C,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,OAAM;QAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAA;QACd,MAAM,GAAG,GAAG,oBAAY,CAAC,IAAI,CAAC,CAAA;QAC9B,IAAI,CAAC,GAAG;YAAE,OAAM;QAChB,KAAK,MAAM,GAAG,IAAI,GAAG,CAAC,QAAQ,IAAI,EAAE;YAAE,KAAK,CAAC,GAAG,CAAC,CAAA;QAChD,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACpB,CAAC,CAAA;IAED,KAAK,MAAM,IAAI,IAAI,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,CAAA;IACzC,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,8EAA8E;AAC9E,SAAgB,oBAAoB,CAClC,IAAoB,EACpB,SAAyC;IAEzC,MAAM,GAAG,GAAG,oBAAY,CAAC,IAAI,CAAC,CAAA;IAC9B,IAAI,CAAC,GAAG;QAAE,OAAO,EAAE,CAAA;IACnB,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;AAC1D,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.d.ts","sourceRoot":"","sources":["../../src/capabilities/jobs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,IAAI,EAAE,UAyBlB,CAAA"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.jobs = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
exports.jobs = {
|
|
6
|
+
name: "jobs",
|
|
7
|
+
describe: "Postgres job queue + worker — background/scheduled commands, idempotent handlers",
|
|
8
|
+
apply(ctx, plan) {
|
|
9
|
+
(0, helpers_1.addPlatformPackage)(plan, ctx, "jobs", { api: true, worker: true });
|
|
10
|
+
// defineJob validates payloads with a zod schema on both the enqueue and run sides.
|
|
11
|
+
if (ctx.hasApp("api"))
|
|
12
|
+
plan.addDependency("apps/api", "zod", "^3.23.8");
|
|
13
|
+
if (ctx.hasWorker())
|
|
14
|
+
plan.addDependency("apps/worker", "zod", "^3.23.8");
|
|
15
|
+
plan.create("scripts/migrations.d/jobs.ts", (0, helpers_1.migrationModule)("jobs"), "jobs migrations wiring");
|
|
16
|
+
const notes = (0, helpers_1.hasNotesExample)(ctx);
|
|
17
|
+
if (ctx.hasApp("api")) {
|
|
18
|
+
plan.create("apps/api/src/platform/jobs.ts", apiClient(notes), "jobs client + registry");
|
|
19
|
+
if (notes) {
|
|
20
|
+
// Enqueue a job whenever a note is created — inside the write transaction.
|
|
21
|
+
plan.create("apps/api/src/bus.d/jobs-index.ts", ENQUEUE, "enqueue a job on note.created");
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (ctx.hasWorker()) {
|
|
25
|
+
plan.create("apps/worker/src/consumers.d/jobs.ts", worker(notes), "jobs worker (claims + runs jobs)");
|
|
26
|
+
}
|
|
27
|
+
plan.patchManifest({ platform: { jobs: true } });
|
|
28
|
+
plan.nextStep("Run `pnpm migrate`. Enqueue with jobs.enqueue(tx, { name, workspaceId, payload }); the worker runs handlers.");
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
function jobExample(notes) {
|
|
32
|
+
return notes
|
|
33
|
+
? { name: "send_welcome_email", export: "sendWelcomeEmail", schema: "z.object({ noteId: z.string() })", field: "note_id: payload.noteId" }
|
|
34
|
+
: { name: "example_job", export: "exampleJob", schema: "z.object({ id: z.string() })", field: "id: payload.id" };
|
|
35
|
+
}
|
|
36
|
+
function apiClient(notes) {
|
|
37
|
+
const d = jobExample(notes);
|
|
38
|
+
return `import { createJobClient, createJobRegistry, defineJob, type JobDb, type JobDefinition } from "@obh/jobs"
|
|
39
|
+
import { z } from "zod"
|
|
40
|
+
|
|
41
|
+
// Every platform row is scoped to a workspace. Single-tenant apps can leave this.
|
|
42
|
+
export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
|
|
43
|
+
|
|
44
|
+
// Jobs are commands (snake_case imperatives), not events. The handler runs in the
|
|
45
|
+
// worker (apps/worker/src/consumers.d/jobs.ts); here the schema validates payloads
|
|
46
|
+
// at enqueue time. Keep the name + schema in sync with the worker's copy.
|
|
47
|
+
export const ${d.export} = defineJob({
|
|
48
|
+
name: "${d.name}",
|
|
49
|
+
version: 1,
|
|
50
|
+
schema: ${d.schema},
|
|
51
|
+
handler: async () => {
|
|
52
|
+
throw new Error("${d.name} runs in the worker, not the API")
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
export const registry = createJobRegistry([${d.export} as JobDefinition])
|
|
57
|
+
|
|
58
|
+
export const jobs = createJobClient({ source: "app", registry })
|
|
59
|
+
|
|
60
|
+
/** Adapt a raw pg transaction to the structural JobDb @obh/jobs expects. */
|
|
61
|
+
export function asJobDb(tx: { query: (sql: string, params?: unknown[]) => Promise<{ rows: unknown[] }> }): JobDb {
|
|
62
|
+
return { query: (sql: string, params?: unknown[]) => tx.query(sql, params) as never }
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
const ENQUEUE = `import { onEmit } from "../bus"
|
|
67
|
+
import { asJobDb, jobs, sendWelcomeEmail, WORKSPACE } from "../platform/jobs"
|
|
68
|
+
|
|
69
|
+
// Enqueue a job on the SAME transaction as the note write, so the job is durable
|
|
70
|
+
// exactly when the note is (and never queued if the write rolls back). The
|
|
71
|
+
// idempotency key makes a re-emit a no-op rather than a duplicate job.
|
|
72
|
+
onEmit(async (tx, name, payload) => {
|
|
73
|
+
if (name !== "note.created") return
|
|
74
|
+
const noteId = String((payload as { id?: string }).id ?? "")
|
|
75
|
+
await jobs.enqueue(asJobDb(tx), {
|
|
76
|
+
name: sendWelcomeEmail.name,
|
|
77
|
+
workspaceId: WORKSPACE,
|
|
78
|
+
payload: { noteId },
|
|
79
|
+
idempotencyKey: \`welcome:\${noteId}\`,
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
`;
|
|
83
|
+
function worker(notes) {
|
|
84
|
+
const d = jobExample(notes);
|
|
85
|
+
return `// Runs queued jobs: each tick claims a batch and executes the matching handler.
|
|
86
|
+
// Handlers live here so the worker never imports across app boundaries; keep the
|
|
87
|
+
// name + schema in sync with apps/api/src/platform/jobs.ts.
|
|
88
|
+
import { createJobRegistry, createWorker, defineJob, pgAdapter, type JobDefinition } from "@obh/jobs"
|
|
89
|
+
import { z } from "zod"
|
|
90
|
+
import type { WorkerContext } from "../context"
|
|
91
|
+
|
|
92
|
+
const ${d.export} = defineJob({
|
|
93
|
+
name: "${d.name}",
|
|
94
|
+
version: 1,
|
|
95
|
+
schema: ${d.schema},
|
|
96
|
+
handler: async (ctx, payload) => {
|
|
97
|
+
ctx.log.info("running ${d.name}", { ${d.field} })
|
|
98
|
+
// Real work goes here (call your mailer, render a PDF, …). Handlers must be
|
|
99
|
+
// idempotent: at-least-once execution means a job can run more than once.
|
|
100
|
+
console.log(JSON.stringify({ msg: "${d.name} done", ${d.field}, job_id: ctx.jobId }))
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const registry = createJobRegistry([${d.export} as JobDefinition])
|
|
105
|
+
|
|
106
|
+
let worker: ReturnType<typeof createWorker>
|
|
107
|
+
|
|
108
|
+
export function init(ctx: WorkerContext): void {
|
|
109
|
+
worker = createWorker({ db: pgAdapter(ctx.pool), registry, instanceId: "worker" })
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function tick(): Promise<void> {
|
|
113
|
+
await worker.tick()
|
|
114
|
+
}
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=jobs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.js","sourceRoot":"","sources":["../../src/capabilities/jobs.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,IAAI,GAAe;IAC9B,IAAI,EAAE,MAAM;IACZ,QAAQ,EAAE,kFAAkF;IAC5F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAClE,oFAAoF;QACpF,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACvE,IAAI,GAAG,CAAC,SAAS,EAAE;YAAE,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACxE,IAAI,CAAC,MAAM,CAAC,8BAA8B,EAAE,IAAA,yBAAe,EAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC,CAAA;QAE9F,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,+BAA+B,EAAE,SAAS,CAAC,KAAK,CAAC,EAAE,wBAAwB,CAAC,CAAA;YACxF,IAAI,KAAK,EAAE,CAAC;gBACV,2EAA2E;gBAC3E,IAAI,CAAC,MAAM,CAAC,kCAAkC,EAAE,OAAO,EAAE,+BAA+B,CAAC,CAAA;YAC3F,CAAC;QACH,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,qCAAqC,EAAE,MAAM,CAAC,KAAK,CAAC,EAAE,kCAAkC,CAAC,CAAA;QACvG,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAChD,IAAI,CAAC,QAAQ,CAAC,8GAA8G,CAAC,CAAA;IAC/H,CAAC;CACF,CAAA;AASD,SAAS,UAAU,CAAC,KAAc;IAChC,OAAO,KAAK;QACV,CAAC,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,kBAAkB,EAAE,MAAM,EAAE,kCAAkC,EAAE,KAAK,EAAE,yBAAyB,EAAE;QAC1I,CAAC,CAAC,EAAE,IAAI,EAAE,aAAa,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,8BAA8B,EAAE,KAAK,EAAE,gBAAgB,EAAE,CAAA;AACpH,CAAC;AAED,SAAS,SAAS,CAAC,KAAc;IAC/B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO;;;;;;;;;eASM,CAAC,CAAC,MAAM;WACZ,CAAC,CAAC,IAAI;;YAEL,CAAC,CAAC,MAAM;;uBAEG,CAAC,CAAC,IAAI;;;;6CAIgB,CAAC,CAAC,MAAM;;;;;;;;CAQpD,CAAA;AACD,CAAC;AAED,MAAM,OAAO,GAAG;;;;;;;;;;;;;;;;CAgBf,CAAA;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,MAAM,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,CAAA;IAC3B,OAAO;;;;;;;QAOD,CAAC,CAAC,MAAM;WACL,CAAC,CAAC,IAAI;;YAEL,CAAC,CAAC,MAAM;;4BAEQ,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC,KAAK;;;yCAGR,CAAC,CAAC,IAAI,WAAW,CAAC,CAAC,KAAK;;;;sCAI3B,CAAC,CAAC,MAAM;;;;;;;;;;;CAW7C,CAAA;AACD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.d.ts","sourceRoot":"","sources":["../../src/capabilities/notifications.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,aAAa,EAAE,UA+B3B,CAAA"}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.notifications = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
exports.notifications = {
|
|
6
|
+
name: "notifications",
|
|
7
|
+
requires: ["events"],
|
|
8
|
+
describe: "Email notification engine — templates + rules reacting to events, delivered over SMTP",
|
|
9
|
+
apply(ctx, plan) {
|
|
10
|
+
(0, helpers_1.addPlatformPackage)(plan, ctx, "notifications", { api: true, worker: true });
|
|
11
|
+
plan.create("scripts/migrations.d/notifications.ts", (0, helpers_1.migrationModule)("notifications"), "notifications migrations wiring");
|
|
12
|
+
const notes = (0, helpers_1.hasNotesExample)(ctx);
|
|
13
|
+
if (ctx.hasApp("api")) {
|
|
14
|
+
plan.create("apps/api/src/platform/notifications.ts", CLIENT, "direct-send notification client");
|
|
15
|
+
}
|
|
16
|
+
if (ctx.hasWorker()) {
|
|
17
|
+
plan.create("apps/worker/src/notification-defs.ts", defsFile(notes), "worker-side templates + rules");
|
|
18
|
+
plan.create("apps/worker/src/dispatch.d/notifications.ts", `export const consumer = { name: "notifications", events: ["*"] }\n`, "register notifications as an event consumer");
|
|
19
|
+
plan.create("apps/worker/src/consumers.d/notifications.ts", WORKER, "notification intake + send tick");
|
|
20
|
+
}
|
|
21
|
+
plan.addEnvVar({
|
|
22
|
+
name: "SMTP_URL",
|
|
23
|
+
example: "smtp://user:pass@localhost:1025",
|
|
24
|
+
comment: "smtp://user:pass@host:port (smtps:// for implicit TLS)",
|
|
25
|
+
});
|
|
26
|
+
plan.patchManifest({ platform: { notifications: true } });
|
|
27
|
+
plan.nextStep("Run `pnpm migrate` and set SMTP_URL. Rules turn events into intents the worker renders and sends.");
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
// The event-driven path (rules) is preferred; this client is the escape hatch
|
|
31
|
+
// for notifications that aren't domain events (invites, password resets).
|
|
32
|
+
const CLIENT = `import { createNotificationClient, pgAdapter } from "@obh/notifications"
|
|
33
|
+
import { pool } from "../db"
|
|
34
|
+
|
|
35
|
+
export const notifications = createNotificationClient({ db: pgAdapter(pool) })
|
|
36
|
+
`;
|
|
37
|
+
function defsFile(notes) {
|
|
38
|
+
const templates = notes
|
|
39
|
+
? ` defineTemplate({
|
|
40
|
+
key: "note_created_email",
|
|
41
|
+
version: 1,
|
|
42
|
+
channel: "email",
|
|
43
|
+
name: "Note created",
|
|
44
|
+
subject: "New note: {{title}}",
|
|
45
|
+
bodyText: "A note titled {{title}} was just created.",
|
|
46
|
+
}),`
|
|
47
|
+
: ` // defineTemplate({ key: "thing_created_email", channel: "email", name: "Thing created", subject: "New thing", bodyText: "…" }),`;
|
|
48
|
+
const rules = notes
|
|
49
|
+
? ` defineNotificationRule({
|
|
50
|
+
id: "rule_note_created",
|
|
51
|
+
eventName: "note.created",
|
|
52
|
+
templateKey: "note_created_email",
|
|
53
|
+
recipientStrategy: "static_email",
|
|
54
|
+
recipientConfig: { email: process.env.NOTIFY_TO ?? "ops@example.com" },
|
|
55
|
+
variableMapping: { title: "$.payload.title" },
|
|
56
|
+
}),`
|
|
57
|
+
: ` // defineNotificationRule({ id: "rule_thing_created", eventName: "thing.created", templateKey: "thing_created_email", recipientStrategy: "static_email", recipientConfig: { email: "ops@example.com" } }),`;
|
|
58
|
+
return `import {
|
|
59
|
+
defineNotificationRule,
|
|
60
|
+
defineTemplate,
|
|
61
|
+
type NotificationRule,
|
|
62
|
+
type NotificationTemplate,
|
|
63
|
+
} from "@obh/notifications"
|
|
64
|
+
|
|
65
|
+
// Worker-side template + rule definitions, seeded into the DB on startup.
|
|
66
|
+
export const templates: NotificationTemplate[] = [
|
|
67
|
+
${templates}
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
export const rules: NotificationRule[] = [
|
|
71
|
+
${rules}
|
|
72
|
+
]
|
|
73
|
+
`;
|
|
74
|
+
}
|
|
75
|
+
const WORKER = `// Seeds templates/rules, then each tick: claim the "notifications" event
|
|
76
|
+
// deliveries into intents, then render and send pending intents over SMTP
|
|
77
|
+
// (retried, dead-lettered). intake and send both run in the default tick().
|
|
78
|
+
import { createNotificationWorker, pgAdapter, smtpProvider, syncRules, syncTemplates } from "@obh/notifications"
|
|
79
|
+
import { rules, templates } from "../notification-defs"
|
|
80
|
+
import type { WorkerContext } from "../context"
|
|
81
|
+
|
|
82
|
+
let worker: ReturnType<typeof createNotificationWorker>
|
|
83
|
+
|
|
84
|
+
function providerFromEnv() {
|
|
85
|
+
const url = new URL(process.env.SMTP_URL ?? "smtp://localhost:1025")
|
|
86
|
+
return smtpProvider({
|
|
87
|
+
host: url.hostname,
|
|
88
|
+
port: Number(url.port) || 25,
|
|
89
|
+
secure: url.protocol === "smtps:",
|
|
90
|
+
user: url.username ? decodeURIComponent(url.username) : undefined,
|
|
91
|
+
pass: url.password ? decodeURIComponent(url.password) : undefined,
|
|
92
|
+
from: process.env.SMTP_FROM ?? "no-reply@example.com",
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function init(ctx: WorkerContext): Promise<void> {
|
|
97
|
+
const db = pgAdapter(ctx.pool)
|
|
98
|
+
// Keep the DB in sync with the code-authored templates/rules (idempotent).
|
|
99
|
+
await syncTemplates(db, templates)
|
|
100
|
+
await syncRules(db, rules)
|
|
101
|
+
worker = createNotificationWorker({
|
|
102
|
+
db,
|
|
103
|
+
provider: providerFromEnv(),
|
|
104
|
+
instanceId: "worker",
|
|
105
|
+
defaultFrom: process.env.SMTP_FROM ?? "no-reply@example.com",
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function tick(): Promise<void> {
|
|
110
|
+
await worker.tick()
|
|
111
|
+
}
|
|
112
|
+
`;
|
|
113
|
+
//# sourceMappingURL=notifications.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"notifications.js","sourceRoot":"","sources":["../../src/capabilities/notifications.ts"],"names":[],"mappings":";;;AACA,uCAAgF;AAEnE,QAAA,aAAa,GAAe;IACvC,IAAI,EAAE,eAAe;IACrB,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,uFAAuF;IACjG,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,eAAe,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3E,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,IAAA,yBAAe,EAAC,eAAe,CAAC,EAAE,iCAAiC,CAAC,CAAA;QAEzH,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,wCAAwC,EAAE,MAAM,EAAE,iCAAiC,CAAC,CAAA;QAClG,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,sCAAsC,EAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,+BAA+B,CAAC,CAAA;YACrG,IAAI,CAAC,MAAM,CACT,6CAA6C,EAC7C,oEAAoE,EACpE,6CAA6C,CAC9C,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,8CAA8C,EAAE,MAAM,EAAE,iCAAiC,CAAC,CAAA;QACxG,CAAC;QAED,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,UAAU;YAChB,OAAO,EAAE,iCAAiC;YAC1C,OAAO,EAAE,wDAAwD;SAClE,CAAC,CAAA;QAEF,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACzD,IAAI,CAAC,QAAQ,CAAC,mGAAmG,CAAC,CAAA;IACpH,CAAC;CACF,CAAA;AAED,8EAA8E;AAC9E,0EAA0E;AAC1E,MAAM,MAAM,GAAG;;;;CAId,CAAA;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,MAAM,SAAS,GAAG,KAAK;QACrB,CAAC,CAAC;;;;;;;MAOA;QACF,CAAC,CAAC,oIAAoI,CAAA;IACxI,MAAM,KAAK,GAAG,KAAK;QACjB,CAAC,CAAC;;;;;;;MAOA;QACF,CAAC,CAAC,8MAA8M,CAAA;IAClN,OAAO;;;;;;;;;EASP,SAAS;;;;EAIT,KAAK;;CAEN,CAAA;AACD,CAAC;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqCd,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../src/capabilities/search.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,MAAM,EAAE,UA8BpB,CAAA"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.search = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
exports.search = {
|
|
6
|
+
name: "search",
|
|
7
|
+
requires: ["events"],
|
|
8
|
+
describe: "Workspace search + indexing — Postgres full-text/trigram, kept fresh from events",
|
|
9
|
+
apply(ctx, plan) {
|
|
10
|
+
(0, helpers_1.addPlatformPackage)(plan, ctx, "search", { api: true, worker: true });
|
|
11
|
+
plan.create("scripts/migrations.d/search.ts", (0, helpers_1.migrationModule)("search"), "search migrations wiring");
|
|
12
|
+
const notes = (0, helpers_1.hasNotesExample)(ctx);
|
|
13
|
+
if (ctx.hasApp("api")) {
|
|
14
|
+
plan.create("apps/api/src/platform/search.ts", CLIENT, "search client + query provider");
|
|
15
|
+
plan.create("apps/api/src/routes/search.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "search query route");
|
|
16
|
+
}
|
|
17
|
+
if (ctx.hasWorker()) {
|
|
18
|
+
plan.create("apps/worker/src/search-entities.ts", entitiesFile(notes), "worker-side search entity definitions");
|
|
19
|
+
plan.create("apps/worker/src/dispatch.d/search.ts", `export const consumer = { name: "search", events: ["*"] }\n`, "register search as an event consumer");
|
|
20
|
+
plan.create("apps/worker/src/consumers.d/search.ts", WORKER, "search indexer tick");
|
|
21
|
+
}
|
|
22
|
+
plan.patchManifest({ platform: { search: true } });
|
|
23
|
+
plan.nextStep("Run `pnpm migrate` (the @obh/search migration enables pg_trgm). The worker keeps the index fresh from events; query at GET /api/search?q=.");
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
// The client only needs the query provider; entity mappers live worker-side.
|
|
27
|
+
const CLIENT = `import { createPostgresSearchProvider, createSearchClient, pgAdapter } from "@obh/search"
|
|
28
|
+
import { pool } from "../db"
|
|
29
|
+
|
|
30
|
+
// Postgres full-text + trigram provider. pg_trgm is enabled by the migration.
|
|
31
|
+
const provider = createPostgresSearchProvider({ db: pgAdapter(pool) })
|
|
32
|
+
|
|
33
|
+
export const search = createSearchClient({ provider })
|
|
34
|
+
`;
|
|
35
|
+
function entitiesFile(notes) {
|
|
36
|
+
const defs = notes
|
|
37
|
+
? ` defineSearchEntity({
|
|
38
|
+
type: "note",
|
|
39
|
+
events: ["note.created", "note.updated"],
|
|
40
|
+
deleteEvents: ["note.deleted"],
|
|
41
|
+
buildDocument: (_ctx, event) => {
|
|
42
|
+
const p = event.payload as { id: string; title?: string; body?: string }
|
|
43
|
+
return {
|
|
44
|
+
workspaceId: event.workspaceId,
|
|
45
|
+
entityType: "note",
|
|
46
|
+
entityId: p.id,
|
|
47
|
+
title: p.title ?? "",
|
|
48
|
+
content: p.body ?? "",
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
buildDeleteRef: (_ctx, event) => {
|
|
52
|
+
const p = event.payload as { id: string }
|
|
53
|
+
return { workspaceId: event.workspaceId, entityType: "note", entityId: p.id }
|
|
54
|
+
},
|
|
55
|
+
}),`
|
|
56
|
+
: ` // defineSearchEntity({
|
|
57
|
+
// type: "thing",
|
|
58
|
+
// events: ["thing.created", "thing.updated"],
|
|
59
|
+
// deleteEvents: ["thing.deleted"],
|
|
60
|
+
// buildDocument: (_ctx, event) => ({ workspaceId: event.workspaceId, entityType: "thing", entityId: String((event.payload as { id: string }).id), title: "…", content: "…" }),
|
|
61
|
+
// buildDeleteRef: (_ctx, event) => ({ workspaceId: event.workspaceId, entityType: "thing", entityId: String((event.payload as { id: string }).id) }),
|
|
62
|
+
// }),`;
|
|
63
|
+
return `import { defineSearchEntity, type SearchEntityDefinition } from "@obh/search"
|
|
64
|
+
|
|
65
|
+
// Worker-side entity definitions: how domain events become search documents.
|
|
66
|
+
// Kept here so the worker never imports across app boundaries.
|
|
67
|
+
export const entities: SearchEntityDefinition[] = [
|
|
68
|
+
${defs}
|
|
69
|
+
]
|
|
70
|
+
`;
|
|
71
|
+
}
|
|
72
|
+
const ROUTE_HONO = `import type { Hono } from "hono"
|
|
73
|
+
import { search } from "../platform/search"
|
|
74
|
+
import { WORKSPACE } from "../platform/events"
|
|
75
|
+
|
|
76
|
+
export function register(app: Hono): void {
|
|
77
|
+
app.get("/api/search", async (c) => {
|
|
78
|
+
const q = c.req.query("q") ?? ""
|
|
79
|
+
return c.json(await search.query({ workspaceId: WORKSPACE, query: q }))
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
`;
|
|
83
|
+
const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
|
|
84
|
+
import { search } from "../platform/search"
|
|
85
|
+
import { WORKSPACE } from "../platform/events"
|
|
86
|
+
|
|
87
|
+
export function register(app: Express): void {
|
|
88
|
+
app.get("/api/search", async (req: Request, res: Response) => {
|
|
89
|
+
const q = (req.query.q as string) ?? ""
|
|
90
|
+
res.json(await search.query({ workspaceId: WORKSPACE, query: q }))
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
`;
|
|
94
|
+
const WORKER = `// Claims the "search" event deliveries and keeps the index in sync. The search
|
|
95
|
+
// worker exposes an events consumer (index on create/update, unindex on delete);
|
|
96
|
+
// createConsumerRunner drives it against this consumer's deliveries.
|
|
97
|
+
import { createConsumerRunner, pgAdapter } from "@obh/events"
|
|
98
|
+
import { createPostgresSearchProvider, createSearchClient, createSearchWorker, pgAdapter as searchPgAdapter } from "@obh/search"
|
|
99
|
+
import { entities } from "../search-entities"
|
|
100
|
+
import type { WorkerContext } from "../context"
|
|
101
|
+
|
|
102
|
+
let runner: ReturnType<typeof createConsumerRunner>
|
|
103
|
+
|
|
104
|
+
export function init(ctx: WorkerContext): void {
|
|
105
|
+
const provider = createPostgresSearchProvider({ db: searchPgAdapter(ctx.pool) })
|
|
106
|
+
const client = createSearchClient({ provider })
|
|
107
|
+
// deps are passed to mappers as ctx.db; the note mappers read straight from
|
|
108
|
+
// the event payload, so nothing product-specific is needed here.
|
|
109
|
+
const worker = createSearchWorker<unknown>({ client, entities, deps: undefined })
|
|
110
|
+
runner = createConsumerRunner({
|
|
111
|
+
db: pgAdapter(ctx.pool),
|
|
112
|
+
consumers: [worker.consumer],
|
|
113
|
+
instanceId: "worker",
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function tick(): Promise<void> {
|
|
118
|
+
await runner.tick()
|
|
119
|
+
}
|
|
120
|
+
`;
|
|
121
|
+
//# sourceMappingURL=search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"search.js","sourceRoot":"","sources":["../../src/capabilities/search.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,MAAM,GAAe;IAChC,IAAI,EAAE,QAAQ;IACd,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpB,QAAQ,EAAE,kFAAkF;IAC5F,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,IAAI,CAAC,MAAM,CAAC,gCAAgC,EAAE,IAAA,yBAAe,EAAC,QAAQ,CAAC,EAAE,0BAA0B,CAAC,CAAA;QAEpG,MAAM,KAAK,GAAG,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAA;QAClC,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,iCAAiC,EAAE,MAAM,EAAE,gCAAgC,CAAC,CAAA;YACxF,IAAI,CAAC,MAAM,CACT,+BAA+B,EAC/B,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EACzD,oBAAoB,CACrB,CAAA;QACH,CAAC;QACD,IAAI,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,CAAC,oCAAoC,EAAE,YAAY,CAAC,KAAK,CAAC,EAAE,uCAAuC,CAAC,CAAA;YAC/G,IAAI,CAAC,MAAM,CACT,sCAAsC,EACtC,6DAA6D,EAC7D,sCAAsC,CACvC,CAAA;YACD,IAAI,CAAC,MAAM,CAAC,uCAAuC,EAAE,MAAM,EAAE,qBAAqB,CAAC,CAAA;QACrF,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QAClD,IAAI,CAAC,QAAQ,CAAC,4IAA4I,CAAC,CAAA;IAC7J,CAAC;CACF,CAAA;AAED,6EAA6E;AAC7E,MAAM,MAAM,GAAG;;;;;;;CAOd,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,IAAI,GAAG,KAAK;QAChB,CAAC,CAAC;;;;;;;;;;;;;;;;;;MAkBA;QACF,CAAC,CAAC;;;;;;SAMG,CAAA;IACP,OAAO;;;;;EAKP,IAAI;;CAEL,CAAA;AACD,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;CAUlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;;;CAUrB,CAAA;AAED,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;CA0Bd,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.d.ts","sourceRoot":"","sources":["../../src/capabilities/settings.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,QAAQ,EAAE,UAiBtB,CAAA"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.settings = void 0;
|
|
4
|
+
const helpers_1 = require("./helpers");
|
|
5
|
+
exports.settings = {
|
|
6
|
+
name: "settings",
|
|
7
|
+
describe: "Typed, scoped configuration — defaults in code, overrides per workspace in Postgres",
|
|
8
|
+
apply(ctx, plan) {
|
|
9
|
+
(0, helpers_1.addPlatformPackage)(plan, ctx, "settings", { api: true });
|
|
10
|
+
// defineSetting validates values with a zod schema.
|
|
11
|
+
if (ctx.hasApp("api"))
|
|
12
|
+
plan.addDependency("apps/api", "zod", "^3.23.8");
|
|
13
|
+
plan.create("scripts/migrations.d/settings.ts", (0, helpers_1.migrationModule)("settings"), "settings migrations wiring");
|
|
14
|
+
if (ctx.hasApp("api")) {
|
|
15
|
+
plan.create("apps/api/src/platform/settings.ts", registryFile((0, helpers_1.hasNotesExample)(ctx)), "settings registry + client");
|
|
16
|
+
plan.create("apps/api/src/routes/settings.ts", (0, helpers_1.apiFramework)(ctx) === "hono" ? ROUTE_HONO : ROUTE_EXPRESS, "read a resolved setting");
|
|
17
|
+
}
|
|
18
|
+
plan.patchManifest({ platform: { settings: true } });
|
|
19
|
+
plan.nextStep("Run `pnpm migrate`. Defaults live in code; overrides resolve per scope from platform.settings.");
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
function registryFile(notes) {
|
|
23
|
+
const def = notes
|
|
24
|
+
? `export const notesRetentionDays = defineSetting({
|
|
25
|
+
key: "notes.retention_days",
|
|
26
|
+
schema: z.number().int().positive(),
|
|
27
|
+
defaultValue: 30,
|
|
28
|
+
scopes: ["platform", "workspace"],
|
|
29
|
+
})`
|
|
30
|
+
: `export const exampleFlag = defineSetting({
|
|
31
|
+
key: "app.example_flag",
|
|
32
|
+
schema: z.boolean(),
|
|
33
|
+
defaultValue: false,
|
|
34
|
+
scopes: ["platform", "workspace"],
|
|
35
|
+
})`;
|
|
36
|
+
const exportName = notes ? "notesRetentionDays" : "exampleFlag";
|
|
37
|
+
return `import { createSettingsClient, createSettingsRegistry, defineSetting, pgAdapter } from "@obh/settings"
|
|
38
|
+
import { z } from "zod"
|
|
39
|
+
import { pool } from "../db"
|
|
40
|
+
|
|
41
|
+
// Every platform row is scoped to a workspace. Single-tenant apps can leave this.
|
|
42
|
+
export const WORKSPACE = process.env.WORKSPACE_ID ?? "default"
|
|
43
|
+
|
|
44
|
+
// A typed setting: schema + default in code, overridable per scope (most specific
|
|
45
|
+
// wins: user > group > workspace > platform > default).
|
|
46
|
+
${def}
|
|
47
|
+
|
|
48
|
+
export const registry = createSettingsRegistry([${exportName}])
|
|
49
|
+
|
|
50
|
+
// The client is stateless; each call takes a db/tx handle and a resolution context.
|
|
51
|
+
export const settings = createSettingsClient({ registry })
|
|
52
|
+
export const settingsDb = pgAdapter(pool)
|
|
53
|
+
`;
|
|
54
|
+
}
|
|
55
|
+
const ROUTE_HONO = `import type { Hono } from "hono"
|
|
56
|
+
import { settings, settingsDb, WORKSPACE } from "../platform/settings"
|
|
57
|
+
|
|
58
|
+
export function register(app: Hono): void {
|
|
59
|
+
// Resolve a setting's effective value for the current workspace, falling back
|
|
60
|
+
// through platform default to the definition default.
|
|
61
|
+
app.get("/settings/:key", async (c) => {
|
|
62
|
+
return c.json(await settings.resolve(settingsDb, c.req.param("key"), { workspaceId: WORKSPACE }))
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
const ROUTE_EXPRESS = `import type { Express, Request, Response } from "express"
|
|
67
|
+
import { settings, settingsDb, WORKSPACE } from "../platform/settings"
|
|
68
|
+
|
|
69
|
+
export function register(app: Express): void {
|
|
70
|
+
app.get("/settings/:key", async (req: Request, res: Response) => {
|
|
71
|
+
res.json(await settings.resolve(settingsDb, req.params.key, { workspaceId: WORKSPACE }))
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
`;
|
|
75
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/capabilities/settings.ts"],"names":[],"mappings":";;;AACA,uCAA8F;AAEjF,QAAA,QAAQ,GAAe;IAClC,IAAI,EAAE,UAAU;IAChB,QAAQ,EAAE,qFAAqF;IAC/F,KAAK,CAAC,GAAG,EAAE,IAAI;QACb,IAAA,4BAAkB,EAAC,IAAI,EAAE,GAAG,EAAE,UAAU,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,CAAA;QACxD,oDAAoD;QACpD,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC;YAAE,IAAI,CAAC,aAAa,CAAC,UAAU,EAAE,KAAK,EAAE,SAAS,CAAC,CAAA;QACvE,IAAI,CAAC,MAAM,CAAC,kCAAkC,EAAE,IAAA,yBAAe,EAAC,UAAU,CAAC,EAAE,4BAA4B,CAAC,CAAA;QAE1G,IAAI,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,CAAC,mCAAmC,EAAE,YAAY,CAAC,IAAA,yBAAe,EAAC,GAAG,CAAC,CAAC,EAAE,4BAA4B,CAAC,CAAA;YAClH,IAAI,CAAC,MAAM,CAAC,iCAAiC,EAAE,IAAA,sBAAY,EAAC,GAAG,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,aAAa,EAAE,yBAAyB,CAAC,CAAA;QACtI,CAAC;QAED,IAAI,CAAC,aAAa,CAAC,EAAE,QAAQ,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,CAAC,CAAA;QACpD,IAAI,CAAC,QAAQ,CAAC,gGAAgG,CAAC,CAAA;IACjH,CAAC;CACF,CAAA;AAED,SAAS,YAAY,CAAC,KAAc;IAClC,MAAM,GAAG,GAAG,KAAK;QACf,CAAC,CAAC;;;;;GAKH;QACC,CAAC,CAAC;;;;;GAKH,CAAA;IACD,MAAM,UAAU,GAAG,KAAK,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,aAAa,CAAA;IAC/D,OAAO;;;;;;;;;EASP,GAAG;;kDAE6C,UAAU;;;;;CAK3D,CAAA;AACD,CAAC;AAED,MAAM,UAAU,GAAG;;;;;;;;;;CAUlB,CAAA;AAED,MAAM,aAAa,GAAG;;;;;;;;CAQrB,CAAA"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CapabilityName } from "../types";
|
|
2
|
+
import type { ProjectContext } from "../project/context";
|
|
3
|
+
import type { Plan } from "../project/plan";
|
|
4
|
+
/**
|
|
5
|
+
* A capability installs one OBH platform primitive into a project. It only ever
|
|
6
|
+
* builds up a Plan (which only ever creates files) — never patches shared code.
|
|
7
|
+
*/
|
|
8
|
+
export interface Capability {
|
|
9
|
+
name: CapabilityName;
|
|
10
|
+
/** Other capabilities that must be present first. */
|
|
11
|
+
requires?: CapabilityName[];
|
|
12
|
+
/** One-line description for prompts and docs. */
|
|
13
|
+
describe: string;
|
|
14
|
+
apply(ctx: ProjectContext, plan: Plan): void;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/capabilities/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA;AAC9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAA;AAE3C;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,cAAc,CAAA;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,cAAc,EAAE,CAAA;IAC3B,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAA;IAChB,KAAK,CAAC,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,GAAG,IAAI,CAAA;CAC7C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/capabilities/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../../src/capabilities/webhooks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAGzC,eAAO,MAAM,QAAQ,EAAE,UAmCtB,CAAA"}
|