@tekki-forge/runtime-api 0.1.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 (74) hide show
  1. package/README.md +5 -0
  2. package/dist/access.d.ts +15 -0
  3. package/dist/access.js +51 -0
  4. package/dist/access.js.map +1 -0
  5. package/dist/api-keys.d.ts +140 -0
  6. package/dist/api-keys.js +361 -0
  7. package/dist/api-keys.js.map +1 -0
  8. package/dist/backup.d.ts +142 -0
  9. package/dist/backup.js +280 -0
  10. package/dist/backup.js.map +1 -0
  11. package/dist/bootstrap.d.ts +57 -0
  12. package/dist/bootstrap.js +191 -0
  13. package/dist/bootstrap.js.map +1 -0
  14. package/dist/comments.d.ts +139 -0
  15. package/dist/comments.js +370 -0
  16. package/dist/comments.js.map +1 -0
  17. package/dist/computed.d.ts +27 -0
  18. package/dist/computed.js +174 -0
  19. package/dist/computed.js.map +1 -0
  20. package/dist/controllers.d.ts +309 -0
  21. package/dist/controllers.js +634 -0
  22. package/dist/controllers.js.map +1 -0
  23. package/dist/dashboard.d.ts +58 -0
  24. package/dist/dashboard.js +258 -0
  25. package/dist/dashboard.js.map +1 -0
  26. package/dist/errors.d.ts +6 -0
  27. package/dist/errors.js +55 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/events.d.ts +39 -0
  30. package/dist/events.js +36 -0
  31. package/dist/events.js.map +1 -0
  32. package/dist/extension.d.ts +103 -0
  33. package/dist/extension.js +8 -0
  34. package/dist/extension.js.map +1 -0
  35. package/dist/import.d.ts +69 -0
  36. package/dist/import.js +347 -0
  37. package/dist/import.js.map +1 -0
  38. package/dist/index.d.ts +22 -0
  39. package/dist/index.js +23 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/jobs.d.ts +41 -0
  42. package/dist/jobs.js +10 -0
  43. package/dist/jobs.js.map +1 -0
  44. package/dist/notifications.d.ts +61 -0
  45. package/dist/notifications.js +195 -0
  46. package/dist/notifications.js.map +1 -0
  47. package/dist/plugin.d.ts +43 -0
  48. package/dist/plugin.js +4 -0
  49. package/dist/plugin.js.map +1 -0
  50. package/dist/query.d.ts +29 -0
  51. package/dist/query.js +192 -0
  52. package/dist/query.js.map +1 -0
  53. package/dist/saved-views.d.ts +42 -0
  54. package/dist/saved-views.js +130 -0
  55. package/dist/saved-views.js.map +1 -0
  56. package/dist/search.d.ts +17 -0
  57. package/dist/search.js +42 -0
  58. package/dist/search.js.map +1 -0
  59. package/dist/seed.d.ts +12 -0
  60. package/dist/seed.js +132 -0
  61. package/dist/seed.js.map +1 -0
  62. package/dist/serialize.d.ts +37 -0
  63. package/dist/serialize.js +165 -0
  64. package/dist/serialize.js.map +1 -0
  65. package/dist/service.d.ts +168 -0
  66. package/dist/service.js +757 -0
  67. package/dist/service.js.map +1 -0
  68. package/dist/teams.d.ts +63 -0
  69. package/dist/teams.js +131 -0
  70. package/dist/teams.js.map +1 -0
  71. package/dist/testing.d.ts +63 -0
  72. package/dist/testing.js +420 -0
  73. package/dist/testing.js.map +1 -0
  74. package/package.json +61 -0
@@ -0,0 +1,58 @@
1
+ import type { ForgeRequestContext } from "./extension.js";
2
+ import type { ForgeService } from "./service.js";
3
+ type Row = Record<string, any>;
4
+ /** "$today", "$today-30d", "$now", "$startOfMonth", "$me" → concrete values. */
5
+ export declare function resolveTokens(where: Record<string, string>, userId: string | null, now?: Date): Record<string, string>;
6
+ export declare function bucketKey(d: Date, by: "day" | "week" | "month" | "year"): string;
7
+ export type ComparePeriod = "week" | "month" | "quarter" | "year";
8
+ /** Periods in a metric's sparkline (the last one is the current period). */
9
+ export declare const TREND_PERIODS = 12;
10
+ /** `d` moved by `n` periods; months clamp to the month's last day (31 Mar − 1 month = 28/29 Feb). */
11
+ export declare function shiftPeriod(d: Date, period: ComparePeriod, n: number): Date;
12
+ /**
13
+ * Rolling windows ending with today (whole UTC days, so numbers are stable through the day and date-only fields
14
+ * compare cleanly), oldest first: with period "month" on 1 Oct the last window is [2 Sep, 2 Oct).
15
+ */
16
+ export declare function trendWindows(now: Date, period: ComparePeriod, count?: number): {
17
+ start: Date;
18
+ end: Date;
19
+ asOf: Date;
20
+ }[];
21
+ export interface MetricTrend {
22
+ period: ComparePeriod;
23
+ trend: "up-is-good" | "down-is-good";
24
+ current: number | null;
25
+ previous: number | null;
26
+ /** (current − previous) / |previous|; null when there is nothing to compare with. */
27
+ change: number | null;
28
+ direction: "up" | "down" | "flat";
29
+ /** Whether the change is good news (null when flat). */
30
+ good: boolean | null;
31
+ /** One value per period, oldest first; `start`/`end` are ISO dates (end exclusive). */
32
+ series: {
33
+ start: string;
34
+ end: string;
35
+ value: number | null;
36
+ }[];
37
+ }
38
+ export declare class DashboardService {
39
+ private readonly service;
40
+ constructor(service: ForgeService);
41
+ private readable;
42
+ private fieldReadable;
43
+ /** `now` is injectable for tests; relative dates in filters ($today…) are resolved against it. */
44
+ compute(ctx: ForgeRequestContext, now?: Date): Promise<{
45
+ widgets: null;
46
+ } | {
47
+ widgets: Row[];
48
+ }>;
49
+ private aggregate;
50
+ /**
51
+ * The metric over each of the last TREND_PERIODS rolling periods, within exactly the same row-level, tenant and
52
+ * owner rules as the value itself; relative dates in the filter ($today…) are resolved as of each period.
53
+ */
54
+ private trend;
55
+ /** `label` is ready to show (default language); `key` is the raw group value so clients can localize it ("__other" = the rest). */
56
+ private series;
57
+ }
58
+ export {};
@@ -0,0 +1,258 @@
1
+ import { authorize, evaluate } from "@tekki-forge/runtime-permissions";
2
+ import { QueryError, parseListQuery, parseSort } from "./query.js";
3
+ import { relationInclude } from "./serialize.js";
4
+ const MAX_BUCKET_ROWS = 50_000;
5
+ /** "$today", "$today-30d", "$now", "$startOfMonth", "$me" → concrete values. */
6
+ export function resolveTokens(where, userId, now = new Date()) {
7
+ const today = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()));
8
+ const out = {};
9
+ for (const [k, v] of Object.entries(where)) {
10
+ out[k] = v.replace(/\$(today|now|startOfMonth|startOfYear|me)([+-]\d+[dwmy])?/g, (_, token, offset) => {
11
+ if (token === "me")
12
+ return userId ?? "__nobody__";
13
+ const d = token === "now" ? new Date(now) : token === "startOfMonth" ? new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1)) : token === "startOfYear" ? new Date(Date.UTC(now.getUTCFullYear(), 0, 1)) : new Date(today);
14
+ if (offset) {
15
+ const n = Number(offset.slice(0, -1));
16
+ const unit = offset.slice(-1);
17
+ if (unit === "d")
18
+ d.setUTCDate(d.getUTCDate() + n);
19
+ if (unit === "w")
20
+ d.setUTCDate(d.getUTCDate() + 7 * n);
21
+ if (unit === "m")
22
+ d.setUTCMonth(d.getUTCMonth() + n);
23
+ if (unit === "y")
24
+ d.setUTCFullYear(d.getUTCFullYear() + n);
25
+ }
26
+ return token === "now" ? d.toISOString() : d.toISOString().slice(0, 10);
27
+ });
28
+ }
29
+ return out;
30
+ }
31
+ export function bucketKey(d, by) {
32
+ const y = d.getUTCFullYear();
33
+ const m = String(d.getUTCMonth() + 1).padStart(2, "0");
34
+ if (by === "year")
35
+ return String(y);
36
+ if (by === "month")
37
+ return `${y}-${m}`;
38
+ if (by === "week") {
39
+ // ISO week start (Monday).
40
+ const monday = new Date(Date.UTC(y, d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7)));
41
+ return monday.toISOString().slice(0, 10);
42
+ }
43
+ return d.toISOString().slice(0, 10);
44
+ }
45
+ function nextBucket(key, by) {
46
+ if (by === "year")
47
+ return String(Number(key) + 1);
48
+ if (by === "month") {
49
+ const [y, m] = key.split("-").map(Number);
50
+ return m === 12 ? `${y + 1}-01` : `${y}-${String(m + 1).padStart(2, "0")}`;
51
+ }
52
+ const d = new Date(`${key}T00:00:00Z`);
53
+ d.setUTCDate(d.getUTCDate() + (by === "week" ? 7 : 1));
54
+ return d.toISOString().slice(0, 10);
55
+ }
56
+ /** Periods in a metric's sparkline (the last one is the current period). */
57
+ export const TREND_PERIODS = 12;
58
+ /** `d` moved by `n` periods; months clamp to the month's last day (31 Mar − 1 month = 28/29 Feb). */
59
+ export function shiftPeriod(d, period, n) {
60
+ if (period === "week")
61
+ return new Date(d.getTime() + n * 7 * 86_400_000);
62
+ const months = n * (period === "month" ? 1 : period === "quarter" ? 3 : 12);
63
+ const first = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + months, 1, d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), d.getUTCMilliseconds()));
64
+ const last = new Date(Date.UTC(first.getUTCFullYear(), first.getUTCMonth() + 1, 0)).getUTCDate();
65
+ first.setUTCDate(Math.min(d.getUTCDate(), last));
66
+ return first;
67
+ }
68
+ /**
69
+ * Rolling windows ending with today (whole UTC days, so numbers are stable through the day and date-only fields
70
+ * compare cleanly), oldest first: with period "month" on 1 Oct the last window is [2 Sep, 2 Oct).
71
+ */
72
+ export function trendWindows(now, period, count = TREND_PERIODS) {
73
+ const anchor = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
74
+ return Array.from({ length: count }, (_, k) => {
75
+ const i = count - 1 - k;
76
+ return { start: shiftPeriod(anchor, period, -(i + 1)), end: shiftPeriod(anchor, period, -i), asOf: shiftPeriod(now, period, -i) };
77
+ });
78
+ }
79
+ const toNumber = (v) => (v === null || v === undefined ? 0 : Number(typeof v === "object" ? String(v) : v));
80
+ export class DashboardService {
81
+ service;
82
+ constructor(service) {
83
+ this.service = service;
84
+ }
85
+ readable(entity, w, ctx) {
86
+ if (!authorize(entity, "read", ctx.principal).allowed)
87
+ return false;
88
+ if (entity.features.tenant && !ctx.principal?.organizationId)
89
+ return false;
90
+ for (const name of [w.field, w.groupBy?.field, ...Object.keys(w.where).map((k) => k.split(".")[0])]) {
91
+ if (!this.fieldReadable(entity, name, ctx))
92
+ return false;
93
+ }
94
+ return true;
95
+ }
96
+ fieldReadable(entity, name, ctx) {
97
+ const f = entity.fields.find((x) => x.name === name || x.apiName === name);
98
+ if (!f?.access?.read)
99
+ return true;
100
+ const d = evaluate(f.access.read, ctx.principal);
101
+ return d.allowed && d.scope === "all";
102
+ }
103
+ /** `now` is injectable for tests; relative dates in filters ($today…) are resolved against it. */
104
+ async compute(ctx, now = new Date()) {
105
+ const dashboard = this.service.app.dashboard;
106
+ if (!dashboard)
107
+ return { widgets: null };
108
+ const out = [];
109
+ for (const w of dashboard.widgets) {
110
+ const entity = this.service.entityByName(w.entity);
111
+ if (!this.readable(entity, w, ctx))
112
+ continue;
113
+ const base = { id: w.id, type: w.type, title: w.title, span: w.span, entity: entity.name, path: entity.path, ...(w.aggregate !== "count" && w.field ? { field: w.field } : {}) };
114
+ try {
115
+ const access = { ...this.service.queryAccess(entity, ctx), anyField: true };
116
+ const filters = parseListQuery(entity, resolveTokens(w.where, ctx.principal?.id ?? null, now), access).where;
117
+ const where = { AND: [...this.service.readWhere(entity, ctx), ...filters] };
118
+ if (w.type === "metric") {
119
+ // The trend is dropped (not the widget) when the viewer can't read the date field it is based on.
120
+ const withTrend = w.compare && this.fieldReadable(entity, w.compare.dateField, ctx);
121
+ const [value, trend] = await Promise.all([this.aggregate(entity, where, w), withTrend ? this.trend(entity, w, ctx, now) : undefined]);
122
+ out.push({ ...base, value, ...(trend ? { trend } : {}) });
123
+ }
124
+ else if (w.type === "chart")
125
+ out.push({ ...base, chart: w.chart, groupBy: w.groupBy, data: await this.series(entity, where, w) });
126
+ else {
127
+ const rows = await this.service.delegate(entity).findMany({
128
+ where,
129
+ orderBy: parseSort(entity, w.sort, access),
130
+ take: w.limit,
131
+ include: relationInclude(entity),
132
+ });
133
+ out.push({ ...base, columns: w.columns ?? [entity.display, ...entity.list.columns.filter((c) => c !== entity.display).slice(0, 2)], rows: await Promise.all(rows.map((r) => this.service.present(entity, r, ctx))) });
134
+ }
135
+ }
136
+ catch (e) {
137
+ // One broken widget (e.g. a filter on a renamed field) must not take the whole dashboard down.
138
+ if (!(e instanceof QueryError))
139
+ console.error(`[forge] dashboard widget "${w.title}" failed`, e);
140
+ out.push({ ...base, error: e instanceof QueryError ? e.message : "This widget could not be loaded" });
141
+ }
142
+ }
143
+ return { widgets: out };
144
+ }
145
+ async aggregate(entity, where, w) {
146
+ const d = this.service.delegate(entity);
147
+ if (w.aggregate === "count")
148
+ return d.count({ where });
149
+ const op = `_${w.aggregate}`;
150
+ const r = await d.aggregate({ where, [op]: { [w.field]: true } });
151
+ const v = r[op]?.[w.field];
152
+ return v === null || v === undefined ? (w.aggregate === "sum" ? 0 : null) : typeof v === "object" ? String(v) : v;
153
+ }
154
+ /**
155
+ * The metric over each of the last TREND_PERIODS rolling periods, within exactly the same row-level, tenant and
156
+ * owner rules as the value itself; relative dates in the filter ($today…) are resolved as of each period.
157
+ */
158
+ async trend(entity, w, ctx, now) {
159
+ const { period, dateField, trend } = w.compare;
160
+ const access = { ...this.service.queryAccess(entity, ctx), anyField: true };
161
+ const scope = this.service.readWhere(entity, ctx);
162
+ const windows = trendWindows(now, period);
163
+ const values = await Promise.all(windows.map(async ({ start, end, asOf }) => {
164
+ const filters = parseListQuery(entity, resolveTokens(w.where, ctx.principal?.id ?? null, asOf), access).where;
165
+ const v = await this.aggregate(entity, { AND: [...scope, ...filters, { [dateField]: { gte: start, lt: end } }] }, w);
166
+ return v === null ? null : toNumber(v);
167
+ }));
168
+ const current = values.at(-1) ?? null;
169
+ const previous = values.at(-2) ?? null;
170
+ const delta = current !== null && previous !== null ? current - previous : null;
171
+ const direction = !delta ? "flat" : delta > 0 ? "up" : "down";
172
+ return {
173
+ period,
174
+ trend,
175
+ current,
176
+ previous,
177
+ change: delta !== null && previous ? delta / Math.abs(previous) : null,
178
+ direction,
179
+ good: direction === "flat" ? null : (direction === "up") === (trend === "up-is-good"),
180
+ series: windows.map((x, i) => ({ start: x.start.toISOString().slice(0, 10), end: x.end.toISOString().slice(0, 10), value: values[i] ?? null })),
181
+ };
182
+ }
183
+ /** `label` is ready to show (default language); `key` is the raw group value so clients can localize it ("__other" = the rest). */
184
+ async series(entity, where, w) {
185
+ const g = w.groupBy;
186
+ const d = this.service.delegate(entity);
187
+ const f = entity.fields.find((x) => x.name === g.field);
188
+ if (g.by) {
189
+ // Date buckets computed from the rows (keeps every permission rule in the Prisma where).
190
+ const rows = await d.findMany({ where: { AND: [where, { [g.field]: { not: null } }] }, select: { [g.field]: true, ...(w.field ? { [w.field]: true } : {}) }, orderBy: { [g.field]: "asc" }, take: MAX_BUCKET_ROWS });
191
+ const buckets = new Map();
192
+ for (const r of rows) {
193
+ const key = bucketKey(new Date(r[g.field]), g.by);
194
+ buckets.set(key, [...(buckets.get(key) ?? []), w.field ? toNumber(r[w.field]) : 1]);
195
+ }
196
+ const keys = [...buckets.keys()];
197
+ if (!keys.length)
198
+ return [];
199
+ const out = [];
200
+ for (let k = keys[0];; k = nextBucket(k, g.by)) {
201
+ const values = buckets.get(k) ?? [];
202
+ out.push({ label: k, key: k, value: reduce(values, w.aggregate) });
203
+ if (k >= keys[keys.length - 1] || out.length > 1000)
204
+ break;
205
+ }
206
+ return out;
207
+ }
208
+ const column = f?.relation ? f.relation.foreignKey : g.field;
209
+ const grouped = await d.groupBy({
210
+ by: [column],
211
+ where,
212
+ _count: { _all: true },
213
+ ...(w.aggregate !== "count" ? { [`_${w.aggregate}`]: { [w.field]: true } } : {}),
214
+ });
215
+ const value = (r) => (w.aggregate === "count" ? r._count._all : toNumber(r[`_${w.aggregate}`]?.[w.field]));
216
+ let labels = new Map();
217
+ if (f?.relation) {
218
+ const ids = grouped.map((r) => r[column]).filter(Boolean);
219
+ const target = f.relation.entity === "User" ? this.service.prisma.user : this.service.prisma[f.relation.delegate];
220
+ const display = f.relation.targetDisplay;
221
+ const rows = ids.length ? await target.findMany({ where: { id: { in: ids } }, select: { id: true, [display]: true } }) : [];
222
+ labels = new Map(rows.map((r) => [r.id, String(r[display] ?? r.id)]));
223
+ }
224
+ const label = (v) => {
225
+ if (v === null || v === undefined)
226
+ return "None";
227
+ if (f?.enumValues)
228
+ return f.enumValues.find((e) => e.value === v)?.label ?? String(v);
229
+ if (f?.kind === "boolean")
230
+ return v ? "Yes" : "No";
231
+ return labels.get(String(v)) ?? String(v);
232
+ };
233
+ const data = grouped
234
+ .map((r) => ({ label: label(r[column]), key: r[column] === null || r[column] === undefined ? null : String(r[column]), value: value(r), href: `/${entity.path}?${encodeURIComponent(g.field)}=${r[column] === null ? "null" : encodeURIComponent(String(r[column]))}` }))
235
+ .sort((a, b) => b.value - a.value);
236
+ if (data.length <= w.limit)
237
+ return data;
238
+ const rest = data.slice(w.limit - 1);
239
+ return [...data.slice(0, w.limit - 1), { label: "Other", key: "__other", value: rest.reduce((s, x) => s + x.value, 0) }];
240
+ }
241
+ }
242
+ function reduce(values, aggregate) {
243
+ if (!values.length)
244
+ return 0;
245
+ switch (aggregate) {
246
+ case "count":
247
+ return values.length;
248
+ case "sum":
249
+ return values.reduce((a, b) => a + b, 0);
250
+ case "avg":
251
+ return values.reduce((a, b) => a + b, 0) / values.length;
252
+ case "min":
253
+ return Math.min(...values);
254
+ case "max":
255
+ return Math.max(...values);
256
+ }
257
+ }
258
+ //# sourceMappingURL=dashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../src/dashboard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ import { type ArgumentsHost, type ExceptionFilter } from "@nestjs/common";
2
+ /** Normalizes every error into `{ statusCode, error, message, issues? }`, including Prisma errors. */
3
+ export declare class ForgeExceptionFilter implements ExceptionFilter {
4
+ private readonly logger;
5
+ catch(exception: unknown, host: ArgumentsHost): void;
6
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,55 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Catch, HttpException, Logger } from "@nestjs/common";
8
+ const STATUS_TEXT = { 400: "Bad Request", 404: "Not Found", 409: "Conflict", 500: "Internal Server Error" };
9
+ /** Normalizes every error into `{ statusCode, error, message, issues? }`, including Prisma errors. */
10
+ let ForgeExceptionFilter = class ForgeExceptionFilter {
11
+ logger = new Logger("Forge");
12
+ catch(exception, host) {
13
+ const res = host.switchToHttp().getResponse();
14
+ if (res.headersSent)
15
+ return;
16
+ if (exception instanceof HttpException) {
17
+ const status = exception.getStatus();
18
+ const body = exception.getResponse();
19
+ const payload = typeof body === "string"
20
+ ? { statusCode: status, error: STATUS_TEXT[status] ?? exception.name, message: body }
21
+ : { statusCode: status, error: STATUS_TEXT[status] ?? exception.name, ...body };
22
+ if (Array.isArray(payload.message))
23
+ payload.message = payload.message.join("; ");
24
+ res.status(status).json(payload);
25
+ return;
26
+ }
27
+ const code = exception?.code;
28
+ const meta = exception?.meta;
29
+ if (code === "P2002") {
30
+ const fields = Array.isArray(meta?.target) ? meta.target : (meta?.driverAdapterError?.cause?.constraint?.fields ?? (typeof meta?.target === "string" ? [meta.target] : []));
31
+ const target = fields.map((f) => f.replace(/"/g, "")).filter((t) => t !== "organizationId" && t !== "organization_id").join(", ") || undefined;
32
+ res.status(409).json({ statusCode: 409, error: "Conflict", message: `A record with this ${target ?? "value"} already exists`, issues: target ? [{ path: String(target), message: "Already taken" }] : [] });
33
+ return;
34
+ }
35
+ if (code === "P2003") {
36
+ res.status(409).json({ statusCode: 409, error: "Conflict", message: "This record is referenced by other records and cannot be changed or deleted" });
37
+ return;
38
+ }
39
+ if (code === "P2025") {
40
+ res.status(404).json({ statusCode: 404, error: "Not Found", message: "Record not found" });
41
+ return;
42
+ }
43
+ if (exception?.type === "entity.parse.failed") {
44
+ res.status(400).json({ statusCode: 400, error: "Bad Request", message: "Malformed JSON body" });
45
+ return;
46
+ }
47
+ this.logger.error(exception instanceof Error ? (exception.stack ?? exception.message) : String(exception));
48
+ res.status(500).json({ statusCode: 500, error: "Internal Server Error", message: "Something went wrong" });
49
+ }
50
+ };
51
+ ForgeExceptionFilter = __decorate([
52
+ Catch()
53
+ ], ForgeExceptionFilter);
54
+ export { ForgeExceptionFilter };
55
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,39 @@
1
+ type Row = Record<string, any>;
2
+ export type ForgeEventAction = "created" | "updated" | "deleted" | "restored" | "transitioned" | "action";
3
+ /**
4
+ * A domain event, emitted after the database transaction commits.
5
+ * `type` is "<Entity>.<action>", e.g. "Opportunity.transitioned".
6
+ */
7
+ export interface ForgeEvent {
8
+ id: string;
9
+ type: string;
10
+ entity: string;
11
+ recordId: string;
12
+ action: ForgeEventAction;
13
+ /** Transition or action name for "transitioned" / "action" events. */
14
+ name?: string;
15
+ /** Full record (system view: every field, including ones some users cannot read). */
16
+ record: Row | null;
17
+ previous?: Row | null;
18
+ changes?: Record<string, {
19
+ from: unknown;
20
+ to: unknown;
21
+ }>;
22
+ actor: {
23
+ id: string;
24
+ name?: string;
25
+ } | null;
26
+ organizationId: string | null;
27
+ occurredAt: string;
28
+ }
29
+ export type EventHandler = (event: ForgeEvent) => void | Promise<void>;
30
+ /** Matches "Opportunity.created", "Opportunity.*", "*.deleted" or "*". */
31
+ export declare function eventMatches(pattern: string, type: string): boolean;
32
+ /** In-process event bus. Handlers run sequentially; a failing handler is logged and never breaks the request. */
33
+ export declare class EventBus {
34
+ private readonly handlers;
35
+ private readonly logger;
36
+ on(pattern: string, handler: EventHandler, name?: string): () => void;
37
+ emit(event: ForgeEvent): Promise<void>;
38
+ }
39
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,36 @@
1
+ import { Logger } from "@nestjs/common";
2
+ /** Matches "Opportunity.created", "Opportunity.*", "*.deleted" or "*". */
3
+ export function eventMatches(pattern, type) {
4
+ if (pattern === "*" || pattern === type)
5
+ return true;
6
+ const [pe, pa] = pattern.split(".");
7
+ const [te, ta] = type.split(".");
8
+ return (pe === "*" || pe === te) && (pa === "*" || pa === ta);
9
+ }
10
+ /** In-process event bus. Handlers run sequentially; a failing handler is logged and never breaks the request. */
11
+ export class EventBus {
12
+ handlers = [];
13
+ logger = new Logger("ForgeEvents");
14
+ on(pattern, handler, name = "handler") {
15
+ const entry = { pattern, handler, name };
16
+ this.handlers.push(entry);
17
+ return () => {
18
+ const i = this.handlers.indexOf(entry);
19
+ if (i >= 0)
20
+ this.handlers.splice(i, 1);
21
+ };
22
+ }
23
+ async emit(event) {
24
+ for (const h of this.handlers) {
25
+ if (!eventMatches(h.pattern, event.type))
26
+ continue;
27
+ try {
28
+ await h.handler(event);
29
+ }
30
+ catch (err) {
31
+ this.logger.error(`${h.name} failed for ${event.type} ${event.recordId}: ${err instanceof Error ? err.stack : String(err)}`);
32
+ }
33
+ }
34
+ }
35
+ }
36
+ //# sourceMappingURL=events.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events.js","sourceRoot":"","sources":["../src/events.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AA6BxC,0EAA0E;AAC1E,MAAM,UAAU,YAAY,CAAC,OAAe,EAAE,IAAY;IACxD,IAAI,OAAO,KAAK,GAAG,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IACrD,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,IAAI,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;AAChE,CAAC;AAED,iHAAiH;AACjH,MAAM,OAAO,QAAQ;IACF,QAAQ,GAA+D,EAAE,CAAC;IAC1E,MAAM,GAAG,IAAI,MAAM,CAAC,aAAa,CAAC,CAAC;IAEpD,EAAE,CAAC,OAAe,EAAE,OAAqB,EAAE,IAAI,GAAG,SAAS;QACzD,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;QACzC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC1B,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACvC,IAAI,CAAC,IAAI,CAAC;gBAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACzC,CAAC,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,KAAiB;QAC1B,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC9B,IAAI,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnD,IAAI,CAAC;gBACH,MAAM,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;YACzB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,eAAe,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,QAAQ,KAAK,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YAC/H,CAAC;QACH,CAAC;IACH,CAAC;CACF"}
@@ -0,0 +1,103 @@
1
+ import type { EntityIR } from "@tekki-forge/compiler";
2
+ import type { Principal } from "@tekki-forge/runtime-permissions";
3
+ import type { ForgeEvent } from "./events.js";
4
+ import type { MailSender, NotifyInput } from "./notifications.js";
5
+ export interface ForgeRequestContext {
6
+ principal: Principal | null;
7
+ user: {
8
+ id: string;
9
+ name: string;
10
+ email: string;
11
+ role?: string | null;
12
+ } | null;
13
+ session: {
14
+ id: string;
15
+ activeOrganizationId?: string | null;
16
+ impersonatedBy?: string | null;
17
+ } | null;
18
+ ip?: string;
19
+ /** Set when the request authenticated with an API key (then `user` and `session` are null). */
20
+ apiKey?: {
21
+ id: string;
22
+ name: string;
23
+ prefix: string;
24
+ createdById: string | null;
25
+ };
26
+ }
27
+ export interface HookContext extends ForgeRequestContext {
28
+ entity: EntityIR;
29
+ /** Prisma client — inside before* hooks this is the transaction, so your writes commit or roll back with the record. */
30
+ prisma: any;
31
+ /** Create in-app notifications (and optionally emails) for users. */
32
+ notify(userIds: string[], input: NotifyInput): Promise<number>;
33
+ /** Templated email (see @tekki-forge/emails); undefined when the app has no mail service. */
34
+ mail?: MailSender;
35
+ /** Services shared by plugins, e.g. use<JobQueue>("jobs")?.enqueue("sendReport", { id }). */
36
+ use<T = unknown>(name: string): T | undefined;
37
+ }
38
+ type Row = Record<string, any>;
39
+ type MaybePromise<T> = T | Promise<T>;
40
+ export interface EntityHooks {
41
+ /** Return a new data object to replace the input, or nothing to keep it. Throw to reject. */
42
+ beforeCreate?(ctx: HookContext & {
43
+ data: Row;
44
+ }): MaybePromise<Row | void>;
45
+ afterCreate?(ctx: HookContext & {
46
+ record: Row;
47
+ }): MaybePromise<void>;
48
+ beforeUpdate?(ctx: HookContext & {
49
+ data: Row;
50
+ existing: Row;
51
+ }): MaybePromise<Row | void>;
52
+ afterUpdate?(ctx: HookContext & {
53
+ record: Row;
54
+ previous: Row;
55
+ }): MaybePromise<void>;
56
+ beforeDelete?(ctx: HookContext & {
57
+ record: Row;
58
+ }): MaybePromise<void>;
59
+ afterDelete?(ctx: HookContext & {
60
+ record: Row;
61
+ }): MaybePromise<void>;
62
+ }
63
+ export interface EntityPolicy {
64
+ /** Record-level decision for get/update/delete/actions. */
65
+ check(ctx: HookContext & {
66
+ record: Row;
67
+ }): MaybePromise<boolean>;
68
+ /** Prisma `where` used to filter lists. Without it, the policy grants nothing on lists. */
69
+ where?(ctx: HookContext): Row | undefined;
70
+ }
71
+ export type ActionHandler = (ctx: HookContext & {
72
+ record: Row;
73
+ input: Row;
74
+ }) => MaybePromise<unknown>;
75
+ export interface TransitionHandlers {
76
+ /** Return false or an explanation to block the transition (e.g. "Add at least one line first"). */
77
+ guard?(ctx: HookContext & {
78
+ record: Row;
79
+ input: Row;
80
+ }): MaybePromise<boolean | string | void>;
81
+ /** Runs after the transition is committed. */
82
+ after?(ctx: HookContext & {
83
+ record: Row;
84
+ previous: Row;
85
+ input: Row;
86
+ }): MaybePromise<void>;
87
+ }
88
+ export interface EntityExtension {
89
+ entity: string;
90
+ hooks?: EntityHooks;
91
+ actions?: Record<string, ActionHandler>;
92
+ policies?: Record<string, EntityPolicy>;
93
+ /** Business rules for workflow transitions declared in forge.config.ts. */
94
+ transitions?: Record<string, TransitionHandlers>;
95
+ /** Event subscriptions, e.g. { "Invoice.transitioned": ({ record }) => … } — run after commit. */
96
+ on?: Record<string, (event: ForgeEvent) => MaybePromise<void>>;
97
+ }
98
+ /**
99
+ * Developer-owned behavior for an entity. Lives in apps/api/src/modules/<entity>/<entity>.extension.ts
100
+ * and is never touched by `forge generate`.
101
+ */
102
+ export declare function defineExtension(entity: string, extension: Omit<EntityExtension, "entity">): EntityExtension;
103
+ export {};
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Developer-owned behavior for an entity. Lives in apps/api/src/modules/<entity>/<entity>.extension.ts
3
+ * and is never touched by `forge generate`.
4
+ */
5
+ export function defineExtension(entity, extension) {
6
+ return { entity, ...extension };
7
+ }
8
+ //# sourceMappingURL=extension.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"extension.js","sourceRoot":"","sources":["../src/extension.ts"],"names":[],"mappings":"AAkEA;;;GAGG;AACH,MAAM,UAAU,eAAe,CAAC,MAAc,EAAE,SAA0C;IACxF,OAAO,EAAE,MAAM,EAAE,GAAG,SAAS,EAAE,CAAC;AAClC,CAAC"}