@tekki-forge/runtime-api 0.1.0 → 0.3.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/dist/admin.d.ts +146 -0
- package/dist/admin.js +256 -0
- package/dist/admin.js.map +1 -0
- package/dist/bootstrap.js +17 -2
- package/dist/bootstrap.js.map +1 -1
- package/dist/controllers.d.ts +273 -4
- package/dist/controllers.js +157 -7
- package/dist/controllers.js.map +1 -1
- package/dist/dashboard.d.ts +102 -5
- package/dist/dashboard.js +493 -73
- package/dist/dashboard.js.map +1 -1
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/seed.js +10 -0
- package/dist/seed.js.map +1 -1
- package/dist/service.d.ts +3 -0
- package/dist/service.js +15 -2
- package/dist/service.js.map +1 -1
- package/dist/settings.d.ts +88 -0
- package/dist/settings.js +252 -0
- package/dist/settings.js.map +1 -0
- package/dist/site-controller.d.ts +115 -0
- package/dist/site-controller.js +373 -0
- package/dist/site-controller.js.map +1 -0
- package/dist/site.d.ts +410 -0
- package/dist/site.js +1413 -0
- package/dist/site.js.map +1 -0
- package/dist/support.d.ts +175 -0
- package/dist/support.js +251 -0
- package/dist/support.js.map +1 -0
- package/package.json +5 -5
package/dist/site.d.ts
ADDED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { type AppIR, type SiteChrome, type SiteIR, type SitePageIR } from "@tekki-forge/compiler";
|
|
2
|
+
import type { ForgeRequestContext } from "./extension.js";
|
|
3
|
+
import type { ForgeService } from "./service.js";
|
|
4
|
+
type Row = Record<string, any>;
|
|
5
|
+
type Issue = {
|
|
6
|
+
path: string;
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
export type PostKind = "post" | "guide" | "webinar" | "case-study";
|
|
10
|
+
export interface SiteSectionData {
|
|
11
|
+
id: string;
|
|
12
|
+
key: string;
|
|
13
|
+
type: string;
|
|
14
|
+
variant: string;
|
|
15
|
+
hidden: boolean;
|
|
16
|
+
props: Record<string, unknown>;
|
|
17
|
+
data?: SectionData;
|
|
18
|
+
}
|
|
19
|
+
export type SectionData = {
|
|
20
|
+
kind: "stats";
|
|
21
|
+
values: (string | null)[];
|
|
22
|
+
} | {
|
|
23
|
+
kind: "records";
|
|
24
|
+
entity: string;
|
|
25
|
+
fields: string[];
|
|
26
|
+
rows: Record<string, unknown>[];
|
|
27
|
+
} | {
|
|
28
|
+
kind: "posts";
|
|
29
|
+
posts: SitePostSummary[];
|
|
30
|
+
} | {
|
|
31
|
+
kind: "plans";
|
|
32
|
+
plans: {
|
|
33
|
+
id: string;
|
|
34
|
+
name: string;
|
|
35
|
+
price: string | null;
|
|
36
|
+
description: string | null;
|
|
37
|
+
features: string[];
|
|
38
|
+
}[];
|
|
39
|
+
};
|
|
40
|
+
export interface SitePostSummary {
|
|
41
|
+
id: string;
|
|
42
|
+
kind: PostKind;
|
|
43
|
+
slug: string;
|
|
44
|
+
title: string;
|
|
45
|
+
excerpt: string | null;
|
|
46
|
+
coverImage: string | null;
|
|
47
|
+
authorName: string | null;
|
|
48
|
+
publishedAt: string | null;
|
|
49
|
+
tags: string[];
|
|
50
|
+
url: string | null;
|
|
51
|
+
}
|
|
52
|
+
export interface SitePostData extends SitePostSummary {
|
|
53
|
+
body: string;
|
|
54
|
+
seo: {
|
|
55
|
+
title?: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
image?: string;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export interface SitePageData {
|
|
61
|
+
id: string;
|
|
62
|
+
key: string | null;
|
|
63
|
+
path: string;
|
|
64
|
+
title: string;
|
|
65
|
+
template: string | null;
|
|
66
|
+
status: "published" | "draft";
|
|
67
|
+
seo: {
|
|
68
|
+
title?: string;
|
|
69
|
+
description?: string;
|
|
70
|
+
image?: string;
|
|
71
|
+
};
|
|
72
|
+
sections: SiteSectionData[];
|
|
73
|
+
updatedAt: string;
|
|
74
|
+
}
|
|
75
|
+
export interface SiteRender {
|
|
76
|
+
kind: "page" | "post" | "redirect";
|
|
77
|
+
page?: SitePageData;
|
|
78
|
+
post?: SitePostData;
|
|
79
|
+
redirect?: string;
|
|
80
|
+
chrome: SiteChrome;
|
|
81
|
+
blog: Partial<Record<PostKind, string>>;
|
|
82
|
+
}
|
|
83
|
+
export interface SitePageListItem {
|
|
84
|
+
id: string;
|
|
85
|
+
key: string | null;
|
|
86
|
+
path: string;
|
|
87
|
+
title: string;
|
|
88
|
+
template: string | null;
|
|
89
|
+
status: "published" | "draft";
|
|
90
|
+
origin: "config" | "admin";
|
|
91
|
+
edited: boolean;
|
|
92
|
+
conflict: boolean;
|
|
93
|
+
sectionConflicts: number;
|
|
94
|
+
sections: number;
|
|
95
|
+
redirect: string | null;
|
|
96
|
+
updatedAt: string;
|
|
97
|
+
}
|
|
98
|
+
export interface SiteSectionDetail extends SiteSectionData {
|
|
99
|
+
origin: "config" | "admin";
|
|
100
|
+
edited: boolean;
|
|
101
|
+
conflict: boolean;
|
|
102
|
+
removed: boolean;
|
|
103
|
+
position: number;
|
|
104
|
+
config: {
|
|
105
|
+
variant: string;
|
|
106
|
+
hidden: boolean;
|
|
107
|
+
props: Record<string, unknown>;
|
|
108
|
+
} | null;
|
|
109
|
+
}
|
|
110
|
+
export interface SitePageDetail extends Omit<SitePageData, "sections"> {
|
|
111
|
+
origin: "config" | "admin";
|
|
112
|
+
edited: boolean;
|
|
113
|
+
conflict: boolean;
|
|
114
|
+
nav: {
|
|
115
|
+
group?: string;
|
|
116
|
+
label?: string;
|
|
117
|
+
footerOnly?: boolean;
|
|
118
|
+
} | false | null;
|
|
119
|
+
redirect: string | null;
|
|
120
|
+
config: {
|
|
121
|
+
title: string;
|
|
122
|
+
path: string;
|
|
123
|
+
seo: object;
|
|
124
|
+
status: string;
|
|
125
|
+
nav: unknown;
|
|
126
|
+
redirect: string | null;
|
|
127
|
+
} | null;
|
|
128
|
+
sections: SiteSectionDetail[];
|
|
129
|
+
}
|
|
130
|
+
export interface SitePostAdmin extends SitePostData {
|
|
131
|
+
status: "draft" | "published";
|
|
132
|
+
authorId: string | null;
|
|
133
|
+
createdAt: string;
|
|
134
|
+
updatedAt: string;
|
|
135
|
+
}
|
|
136
|
+
/** A signed-in person (never an API key) whose global roles include one of `site.editors`. */
|
|
137
|
+
export declare function canEditSite(app: AppIR, ctx: ForgeRequestContext): boolean;
|
|
138
|
+
/** Prisma "table/column does not exist" (P2021/P2022), or a client generated before the site models. */
|
|
139
|
+
export declare function siteTablesMissing(e: unknown): boolean;
|
|
140
|
+
export interface SiteSyncReport {
|
|
141
|
+
created: number;
|
|
142
|
+
updated: number;
|
|
143
|
+
conflicts: number;
|
|
144
|
+
removed: number;
|
|
145
|
+
}
|
|
146
|
+
export interface SyncSectionRow {
|
|
147
|
+
id: string;
|
|
148
|
+
key: string;
|
|
149
|
+
origin: string;
|
|
150
|
+
configHash: string | null;
|
|
151
|
+
edited: boolean;
|
|
152
|
+
conflict: boolean;
|
|
153
|
+
removed: boolean;
|
|
154
|
+
position: number;
|
|
155
|
+
}
|
|
156
|
+
export interface SyncPageRow {
|
|
157
|
+
id: string;
|
|
158
|
+
key: string | null;
|
|
159
|
+
path: string;
|
|
160
|
+
origin: string;
|
|
161
|
+
configHash: string | null;
|
|
162
|
+
edited: boolean;
|
|
163
|
+
conflict: boolean;
|
|
164
|
+
orderEdited: boolean;
|
|
165
|
+
position: number;
|
|
166
|
+
publishedAt?: Date | null;
|
|
167
|
+
sections: SyncSectionRow[];
|
|
168
|
+
}
|
|
169
|
+
export type SyncOp = {
|
|
170
|
+
op: "createPage";
|
|
171
|
+
data: Row;
|
|
172
|
+
sections: Row[];
|
|
173
|
+
} | {
|
|
174
|
+
op: "updatePage";
|
|
175
|
+
id: string;
|
|
176
|
+
data: Row;
|
|
177
|
+
} | {
|
|
178
|
+
op: "deletePage";
|
|
179
|
+
id: string;
|
|
180
|
+
} | {
|
|
181
|
+
op: "createSection";
|
|
182
|
+
data: Row;
|
|
183
|
+
} | {
|
|
184
|
+
op: "updateSection";
|
|
185
|
+
id: string;
|
|
186
|
+
data: Row;
|
|
187
|
+
} | {
|
|
188
|
+
op: "deleteSection";
|
|
189
|
+
id: string;
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* What to do with a row that comes from the config and is still in it:
|
|
193
|
+
* - keep: nothing changed since the last sync (or the conflict is already flagged);
|
|
194
|
+
* - apply: the config changed and nobody edited the row in the admin → take the config version;
|
|
195
|
+
* - conflict: both changed (or the admin deleted it) → keep the admin version, flag it;
|
|
196
|
+
* - resolve: the config went back to the version last applied → the conflict is gone.
|
|
197
|
+
*/
|
|
198
|
+
export declare function syncDecision(row: {
|
|
199
|
+
configHash: string | null;
|
|
200
|
+
edited: boolean;
|
|
201
|
+
conflict: boolean;
|
|
202
|
+
removed?: boolean;
|
|
203
|
+
}, hash: string): "keep" | "apply" | "conflict" | "resolve";
|
|
204
|
+
/** The page columns a config page sets. */
|
|
205
|
+
export declare function configPageFields(p: SitePageIR): Row;
|
|
206
|
+
/**
|
|
207
|
+
* Pure part of the sync: compare the config (SiteIR) with the stored pages and list the writes to make.
|
|
208
|
+
* Admin work always wins over the config; conflicts are flagged, never overwritten.
|
|
209
|
+
*/
|
|
210
|
+
export declare function planSiteSync(site: SiteIR, rows: SyncPageRow[], now?: Date): {
|
|
211
|
+
ops: SyncOp[];
|
|
212
|
+
report: SiteSyncReport;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* Seed the website from forge.config.ts (idempotent): new pages and sections are inserted, untouched ones follow
|
|
216
|
+
* the config, admin edits are kept (and flagged when the config changed too). Runs at API start and in `forge db seed`,
|
|
217
|
+
* under a transaction-level advisory lock so several API processes starting together don't race. Never throws:
|
|
218
|
+
* returns null when there is no site or the tables are missing (with a "run forge db migrate" warning).
|
|
219
|
+
*/
|
|
220
|
+
export declare function syncSite(app: AppIR, prisma: any, options?: {
|
|
221
|
+
log?: boolean;
|
|
222
|
+
}): Promise<SiteSyncReport | null>;
|
|
223
|
+
export interface ContactInput {
|
|
224
|
+
name: string;
|
|
225
|
+
email: string;
|
|
226
|
+
company?: string;
|
|
227
|
+
message: string;
|
|
228
|
+
page?: string;
|
|
229
|
+
}
|
|
230
|
+
/** Bot traps: the hidden "website" field is filled, or the form was sent less than 2 s after it was shown. */
|
|
231
|
+
export declare function looksLikeSpam(body: Row, now?: number): boolean;
|
|
232
|
+
export declare function validateContact(body: unknown): {
|
|
233
|
+
issues: Issue[];
|
|
234
|
+
value: ContactInput;
|
|
235
|
+
};
|
|
236
|
+
export declare function validateNewsletter(body: unknown): {
|
|
237
|
+
issues: Issue[];
|
|
238
|
+
value: {
|
|
239
|
+
email: string;
|
|
240
|
+
source?: string;
|
|
241
|
+
};
|
|
242
|
+
};
|
|
243
|
+
/** Forget every contact/newsletter rate limit (tests). */
|
|
244
|
+
export declare function resetSiteRateLimits(): void;
|
|
245
|
+
/** "Hello, World!" → "hello-world" (accents dropped, at most 100 characters). */
|
|
246
|
+
export declare function slugify(input: string): string;
|
|
247
|
+
/** A few published posts per post kind with a public listing, when there are no posts yet (`forge db seed`). */
|
|
248
|
+
export declare function seedSitePosts(app: AppIR, prisma: any, userId: string | null): Promise<number>;
|
|
249
|
+
/**
|
|
250
|
+
* The public website: rendering for visitors (pages, posts, sitemap), the contact form and newsletter, and the
|
|
251
|
+
* admin API of site editors (pages, sections, posts, messages, subscribers). Visitors only ever see published
|
|
252
|
+
* content and data of public, non-tenant entities, computed with an anonymous context.
|
|
253
|
+
*/
|
|
254
|
+
export declare class SiteService {
|
|
255
|
+
private readonly service;
|
|
256
|
+
constructor(service: ForgeService);
|
|
257
|
+
private get app();
|
|
258
|
+
private get prisma();
|
|
259
|
+
private get site();
|
|
260
|
+
/** The site models of the Prisma client (absent when it was generated before the site existed). */
|
|
261
|
+
private get db();
|
|
262
|
+
/** Admin routes: a signed-in site editor, never an API key. */
|
|
263
|
+
private editor;
|
|
264
|
+
/** Turn "tables missing" into a clear 503 on admin and form routes. */
|
|
265
|
+
private guard;
|
|
266
|
+
private checkApp;
|
|
267
|
+
private templateCtx;
|
|
268
|
+
private takenPaths;
|
|
269
|
+
private configPage;
|
|
270
|
+
/** Live values of a section (stats, records, posts, plans); undefined for sections without data or on failure. */
|
|
271
|
+
sectionData(type: string, variant: string, props: Record<string, unknown>): Promise<SectionData | undefined>;
|
|
272
|
+
/** Static values as typed; live ones aggregated like dashboard metrics, as an anonymous visitor (public entities only). */
|
|
273
|
+
private stats;
|
|
274
|
+
/** Read scope of an anonymous visitor + the section's filter (same rules as the dashboard and the list API). */
|
|
275
|
+
private publicWhere;
|
|
276
|
+
private records;
|
|
277
|
+
private postSummary;
|
|
278
|
+
private postData;
|
|
279
|
+
private sections;
|
|
280
|
+
private pageData;
|
|
281
|
+
private chrome;
|
|
282
|
+
/** `?path=/pricing[&preview=1]` → the page, the post or the redirect at that path, with the header and footer. */
|
|
283
|
+
render(query: Row, ctx: ForgeRequestContext): Promise<SiteRender>;
|
|
284
|
+
/** Until `forge db migrate` creates the tables, pages are rendered straight from the config. */
|
|
285
|
+
private renderFromConfig;
|
|
286
|
+
private publishedPosts;
|
|
287
|
+
/** `?kind=post&limit=20&page=1&tag=x`: published posts, newest first. */
|
|
288
|
+
posts(query: Row): Promise<{
|
|
289
|
+
data: SitePostSummary[];
|
|
290
|
+
total: number;
|
|
291
|
+
}>;
|
|
292
|
+
sitemap(): Promise<{
|
|
293
|
+
pages: {
|
|
294
|
+
path: string;
|
|
295
|
+
updatedAt: string;
|
|
296
|
+
}[];
|
|
297
|
+
posts: {
|
|
298
|
+
path: string;
|
|
299
|
+
updatedAt: string;
|
|
300
|
+
}[];
|
|
301
|
+
}>;
|
|
302
|
+
/** Users whose global roles make them site editors (contact form notifications). */
|
|
303
|
+
private editorIds;
|
|
304
|
+
contact(body: unknown, meta: {
|
|
305
|
+
ip?: string;
|
|
306
|
+
userAgent?: string;
|
|
307
|
+
}): Promise<{
|
|
308
|
+
ok: true;
|
|
309
|
+
}>;
|
|
310
|
+
newsletter(body: unknown, meta: {
|
|
311
|
+
ip?: string;
|
|
312
|
+
}): Promise<{
|
|
313
|
+
ok: true;
|
|
314
|
+
}>;
|
|
315
|
+
private listItem;
|
|
316
|
+
overview(ctx: ForgeRequestContext): Promise<{
|
|
317
|
+
pages: SitePageListItem[];
|
|
318
|
+
conflicts: number;
|
|
319
|
+
messages: {
|
|
320
|
+
new: number;
|
|
321
|
+
};
|
|
322
|
+
subscribers: number;
|
|
323
|
+
storage: boolean;
|
|
324
|
+
payments: boolean;
|
|
325
|
+
blogKinds: {
|
|
326
|
+
post?: string | undefined;
|
|
327
|
+
guide?: string | undefined;
|
|
328
|
+
webinar?: string | undefined;
|
|
329
|
+
"case-study"?: string | undefined;
|
|
330
|
+
};
|
|
331
|
+
}>;
|
|
332
|
+
private findPage;
|
|
333
|
+
private pathTaken;
|
|
334
|
+
private detail;
|
|
335
|
+
page(id: string, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
336
|
+
/** Validated page fields (title, path, seo, nav, status, redirect); only the keys present in `body`. */
|
|
337
|
+
private pageFields;
|
|
338
|
+
/** New admin page, optionally from a template (sections with placeholder copy). Starts as a draft. */
|
|
339
|
+
createPage(body: unknown, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
340
|
+
updatePage(id: string, body: unknown, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
341
|
+
deletePage(id: string, ctx: ForgeRequestContext): Promise<void>;
|
|
342
|
+
revertPage(id: string, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
343
|
+
private checkSection;
|
|
344
|
+
private findSection;
|
|
345
|
+
private configSection;
|
|
346
|
+
/** `after`: index (among the visible order) of the section the new one follows; -1 = first; default: last. */
|
|
347
|
+
addSection(pageId: string, body: unknown, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
348
|
+
reorder(pageId: string, body: unknown, ctx: ForgeRequestContext): Promise<SitePageDetail>;
|
|
349
|
+
updateSection(id: string, body: unknown, ctx: ForgeRequestContext): Promise<SiteSectionDetail>;
|
|
350
|
+
private sectionDetail;
|
|
351
|
+
/** Config sections are only marked removed (so the next seed doesn't bring them back); admin sections are deleted. */
|
|
352
|
+
deleteSection(id: string, ctx: ForgeRequestContext): Promise<void>;
|
|
353
|
+
revertSection(id: string, ctx: ForgeRequestContext): Promise<SiteSectionDetail>;
|
|
354
|
+
/** Live data for unsaved sections of the editor's preview; an entry is null when the section is invalid or has no data. */
|
|
355
|
+
preview(body: unknown, ctx: ForgeRequestContext): Promise<{
|
|
356
|
+
data: (SectionData | null)[];
|
|
357
|
+
}>;
|
|
358
|
+
private postAdmin;
|
|
359
|
+
private postFields;
|
|
360
|
+
private findPost;
|
|
361
|
+
listPosts(query: Row, ctx: ForgeRequestContext): Promise<{
|
|
362
|
+
data: SitePostAdmin[];
|
|
363
|
+
total: number;
|
|
364
|
+
}>;
|
|
365
|
+
getPost(id: string, ctx: ForgeRequestContext): Promise<SitePostAdmin>;
|
|
366
|
+
createPost(body: unknown, ctx: ForgeRequestContext): Promise<SitePostAdmin>;
|
|
367
|
+
updatePost(id: string, body: unknown, ctx: ForgeRequestContext): Promise<SitePostAdmin>;
|
|
368
|
+
deletePost(id: string, ctx: ForgeRequestContext): Promise<void>;
|
|
369
|
+
private message;
|
|
370
|
+
messages(query: Row, ctx: ForgeRequestContext): Promise<{
|
|
371
|
+
data: {
|
|
372
|
+
id: any;
|
|
373
|
+
name: any;
|
|
374
|
+
email: any;
|
|
375
|
+
company: any;
|
|
376
|
+
message: any;
|
|
377
|
+
page: any;
|
|
378
|
+
status: any;
|
|
379
|
+
createdAt: string;
|
|
380
|
+
}[];
|
|
381
|
+
total: number;
|
|
382
|
+
}>;
|
|
383
|
+
updateMessage(id: string, body: unknown, ctx: ForgeRequestContext): Promise<{
|
|
384
|
+
id: any;
|
|
385
|
+
name: any;
|
|
386
|
+
email: any;
|
|
387
|
+
company: any;
|
|
388
|
+
message: any;
|
|
389
|
+
page: any;
|
|
390
|
+
status: any;
|
|
391
|
+
createdAt: string;
|
|
392
|
+
}>;
|
|
393
|
+
deleteMessage(id: string, ctx: ForgeRequestContext): Promise<void>;
|
|
394
|
+
private subscriber;
|
|
395
|
+
subscribers(query: Row, ctx: ForgeRequestContext): Promise<{
|
|
396
|
+
data: {
|
|
397
|
+
id: any;
|
|
398
|
+
email: any;
|
|
399
|
+
status: any;
|
|
400
|
+
source: any;
|
|
401
|
+
createdAt: string;
|
|
402
|
+
updatedAt: string;
|
|
403
|
+
}[];
|
|
404
|
+
total: number;
|
|
405
|
+
}>;
|
|
406
|
+
/** Subscribed addresses as CSV (cells that a spreadsheet would run as formulas are neutralized). */
|
|
407
|
+
exportSubscribers(ctx: ForgeRequestContext): Promise<string>;
|
|
408
|
+
deleteSubscriber(id: string, ctx: ForgeRequestContext): Promise<void>;
|
|
409
|
+
}
|
|
410
|
+
export {};
|