@c9up/station 0.1.5

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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/dist/ResourceRegistry.d.ts +18 -0
  4. package/dist/ResourceRegistry.d.ts.map +1 -0
  5. package/dist/ResourceRegistry.js +38 -0
  6. package/dist/ResourceRegistry.js.map +1 -0
  7. package/dist/StationProvider.d.ts +109 -0
  8. package/dist/StationProvider.d.ts.map +1 -0
  9. package/dist/StationProvider.js +1144 -0
  10. package/dist/StationProvider.js.map +1 -0
  11. package/dist/casing.d.ts +27 -0
  12. package/dist/casing.d.ts.map +1 -0
  13. package/dist/casing.js +75 -0
  14. package/dist/casing.js.map +1 -0
  15. package/dist/defineResource.d.ts +9 -0
  16. package/dist/defineResource.d.ts.map +1 -0
  17. package/dist/defineResource.js +84 -0
  18. package/dist/defineResource.js.map +1 -0
  19. package/dist/index.d.ts +7 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/services/main.d.ts +18 -0
  24. package/dist/services/main.d.ts.map +1 -0
  25. package/dist/services/main.js +31 -0
  26. package/dist/services/main.js.map +1 -0
  27. package/dist/types.d.ts +85 -0
  28. package/dist/types.d.ts.map +1 -0
  29. package/dist/types.js +8 -0
  30. package/dist/types.js.map +1 -0
  31. package/dist/views/errors/404.d.ts +12 -0
  32. package/dist/views/errors/404.d.ts.map +1 -0
  33. package/dist/views/errors/404.js +19 -0
  34. package/dist/views/errors/404.js.map +1 -0
  35. package/dist/views/escape.d.ts +30 -0
  36. package/dist/views/escape.d.ts.map +1 -0
  37. package/dist/views/escape.js +34 -0
  38. package/dist/views/escape.js.map +1 -0
  39. package/dist/views/form.d.ts +34 -0
  40. package/dist/views/form.d.ts.map +1 -0
  41. package/dist/views/form.js +139 -0
  42. package/dist/views/form.js.map +1 -0
  43. package/dist/views/layout.d.ts +24 -0
  44. package/dist/views/layout.d.ts.map +1 -0
  45. package/dist/views/layout.js +85 -0
  46. package/dist/views/layout.js.map +1 -0
  47. package/dist/views/list.d.ts +22 -0
  48. package/dist/views/list.d.ts.map +1 -0
  49. package/dist/views/list.js +85 -0
  50. package/dist/views/list.js.map +1 -0
  51. package/dist/views/login.d.ts +25 -0
  52. package/dist/views/login.d.ts.map +1 -0
  53. package/dist/views/login.js +44 -0
  54. package/dist/views/login.js.map +1 -0
  55. package/dist/views/show.d.ts +17 -0
  56. package/dist/views/show.d.ts.map +1 -0
  57. package/dist/views/show.js +24 -0
  58. package/dist/views/show.js.map +1 -0
  59. package/package.json +63 -0
  60. package/src/ResourceRegistry.ts +49 -0
  61. package/src/StationProvider.ts +1579 -0
  62. package/src/casing.ts +86 -0
  63. package/src/defineResource.ts +126 -0
  64. package/src/index.ts +14 -0
  65. package/src/services/main.ts +39 -0
  66. package/src/types.ts +108 -0
  67. package/src/views/errors/404.ts +27 -0
  68. package/src/views/escape.ts +46 -0
  69. package/src/views/form.ts +191 -0
  70. package/src/views/layout.ts +90 -0
  71. package/src/views/list.ts +121 -0
  72. package/src/views/login.ts +65 -0
  73. package/src/views/show.ts +37 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `GET /admin/<slug>/new` + `GET /admin/<slug>/:id/edit` — form view
3
+ * shared between the create and edit actions (Story 54.3).
4
+ *
5
+ * Field rendering is INFERRED from the entity's `@Column` metadata
6
+ * (Story 54.5): column type maps to an `<input type>`, boolean →
7
+ * checkbox, text-long → textarea, etc. The primary key and any
8
+ * timestamp columns (`created_at` / `updated_at` / `deleted_at`) are
9
+ * skipped from the form body. Per-field overrides declared on
10
+ * `Resource.formFields` win over the inferred defaults.
11
+ *
12
+ * Every dynamic value flows through `escapeHtml()` so a malicious
13
+ * column value rendered into an edit form cannot smuggle markup. The
14
+ * lint sweep at `tests/unit/no-unescaped-interpolation.test.ts`
15
+ * enforces the convention.
16
+ */
17
+ import type { ColumnMetadata } from "@c9up/atlas";
18
+ import type { Resource } from "../types.js";
19
+ export interface FormPageInput {
20
+ resource: Resource;
21
+ columns: ReadonlyArray<ColumnMetadata>;
22
+ pkColumn: string;
23
+ /** Existing row for edit; undefined for create. */
24
+ row?: Readonly<Record<string, unknown>>;
25
+ /** Validation errors keyed by column propertyKey. */
26
+ errors?: Readonly<Record<string, string>>;
27
+ /** Caller-controlled hidden inputs (e.g. CSRF token). */
28
+ hiddenInputs?: ReadonlyArray<{
29
+ name: string;
30
+ value: string;
31
+ }>;
32
+ }
33
+ export declare function renderFormPage(input: FormPageInput): string;
34
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../../src/views/form.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAqB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAU/D,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,mDAAmD;IACnD,GAAG,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACxC,qDAAqD;IACrD,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1C,yDAAyD;IACzD,YAAY,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAkD3D"}
@@ -0,0 +1,139 @@
1
+ /**
2
+ * `GET /admin/<slug>/new` + `GET /admin/<slug>/:id/edit` — form view
3
+ * shared between the create and edit actions (Story 54.3).
4
+ *
5
+ * Field rendering is INFERRED from the entity's `@Column` metadata
6
+ * (Story 54.5): column type maps to an `<input type>`, boolean →
7
+ * checkbox, text-long → textarea, etc. The primary key and any
8
+ * timestamp columns (`created_at` / `updated_at` / `deleted_at`) are
9
+ * skipped from the form body. Per-field overrides declared on
10
+ * `Resource.formFields` win over the inferred defaults.
11
+ *
12
+ * Every dynamic value flows through `escapeHtml()` so a malicious
13
+ * column value rendered into an edit form cannot smuggle markup. The
14
+ * lint sweep at `tests/unit/no-unescaped-interpolation.test.ts`
15
+ * enforces the convention.
16
+ */
17
+ import { escapeHtml, safeHtml } from "./escape.js";
18
+ import { renderLayout } from "./layout.js";
19
+ const SKIPPED_COLUMN_NAMES = new Set([
20
+ "created_at",
21
+ "updated_at",
22
+ "deleted_at",
23
+ ]);
24
+ export function renderFormPage(input) {
25
+ const { resource, columns, pkColumn, row, errors, hiddenInputs } = input;
26
+ const isEdit = row !== undefined;
27
+ const slug = encodeURIComponent(resource.name);
28
+ const id = isEdit ? String(row[pkColumn] ?? "") : "";
29
+ const action = isEdit
30
+ ? `/admin/${slug}/${encodeURIComponent(id)}`
31
+ : `/admin/${slug}`;
32
+ const methodOverride = isEdit
33
+ ? `<input type="hidden" name="_method" value="PUT">`
34
+ : "";
35
+ const heading = isEdit
36
+ ? `Edit ${escapeHtml(resource.label)} #${escapeHtml(id)}`
37
+ : `New ${escapeHtml(resource.label)}`;
38
+ const hiddens = (hiddenInputs ?? [])
39
+ .map((h) => `<input type="hidden" name="${escapeHtml(h.name)}" value="${escapeHtml(h.value)}">`)
40
+ .join("");
41
+ const fieldHtml = columns
42
+ .filter((c) => !shouldSkipColumn(c, pkColumn, resource.formFields[c.propertyKey]))
43
+ .map((c) => renderField(c, row, errors, resource.formFields[c.propertyKey]))
44
+ .join("");
45
+ const submitLabel = isEdit ? "Update" : "Create";
46
+ const cancelUrl = isEdit
47
+ ? `/admin/${slug}/${encodeURIComponent(id)}`
48
+ : `/admin/${slug}`;
49
+ const body = `<h1>${heading}</h1>` +
50
+ `<form class="st-form" method="POST" action="${escapeHtml(action)}">` +
51
+ methodOverride +
52
+ hiddens +
53
+ fieldHtml +
54
+ `<div class="st-form-actions">` +
55
+ `<button type="submit">${escapeHtml(submitLabel)}</button>` +
56
+ `<a class="st-cancel" href="${escapeHtml(cancelUrl)}">Cancel</a>` +
57
+ `</div>` +
58
+ `</form>`;
59
+ return renderLayout({
60
+ title: isEdit ? `Edit ${resource.label} #${id}` : `New ${resource.label}`,
61
+ bodyHtml: safeHtml(body),
62
+ });
63
+ }
64
+ function shouldSkipColumn(c, pkColumn, override) {
65
+ if (override?.hidden === true)
66
+ return true;
67
+ // Atlas's ColumnMetadata exposes the property key (camelCase). The
68
+ // pkColumn passed by the provider is also camelCase (it comes from
69
+ // `atlas.getPrimaryKey(entity)`), so the comparison is direct.
70
+ // snake_case fallbacks live in SKIPPED_COLUMN_NAMES below for the
71
+ // timestamps convention.
72
+ if (c.propertyKey === pkColumn)
73
+ return true;
74
+ const snake = c.propertyKey.replace(/([A-Z])/g, "_$1").toLowerCase();
75
+ if (SKIPPED_COLUMN_NAMES.has(snake))
76
+ return true;
77
+ if (SKIPPED_COLUMN_NAMES.has(c.propertyKey))
78
+ return true;
79
+ return false;
80
+ }
81
+ function renderField(c, row, errors, override) {
82
+ const inputType = override?.inputType ?? inferInputType(c);
83
+ const labelText = override?.label ?? titleise(c.propertyKey);
84
+ const required = override?.required === true ? " required" : "";
85
+ const placeholder = override?.placeholder !== undefined
86
+ ? ` placeholder="${escapeHtml(override.placeholder)}"`
87
+ : "";
88
+ const name = c.propertyKey;
89
+ const fieldId = `f-${escapeHtml(name)}`;
90
+ const rawValue = row?.[name];
91
+ const errorMsg = errors?.[name];
92
+ const errorHtml = errorMsg !== undefined
93
+ ? `<p class="st-field-error">${escapeHtml(errorMsg)}</p>`
94
+ : "";
95
+ const input = renderInput(inputType, fieldId, name, rawValue, required, placeholder);
96
+ return (`<div class="st-field">` +
97
+ `<label for="${escapeHtml(fieldId)}">${escapeHtml(labelText)}</label>` +
98
+ input +
99
+ errorHtml +
100
+ `</div>`);
101
+ }
102
+ /** Render the `<input>` / `<textarea>` element for a field by input type. */
103
+ function renderInput(inputType, fieldId, name, rawValue, required, placeholder) {
104
+ if (inputType === "checkbox") {
105
+ const checked = rawValue ? " checked" : "";
106
+ return `<input id="${escapeHtml(fieldId)}" type="checkbox" name="${escapeHtml(name)}" value="1"${checked}>`;
107
+ }
108
+ const value = rawValue === undefined || rawValue === null ? "" : String(rawValue);
109
+ if (inputType === "textarea") {
110
+ return `<textarea id="${escapeHtml(fieldId)}" name="${escapeHtml(name)}"${required}${placeholder}>${escapeHtml(value)}</textarea>`;
111
+ }
112
+ return `<input id="${escapeHtml(fieldId)}" type="${escapeHtml(inputType)}" name="${escapeHtml(name)}" value="${escapeHtml(value)}"${required}${placeholder}>`;
113
+ }
114
+ function inferInputType(c) {
115
+ const type = (c.type ?? "").toString().toLowerCase();
116
+ if (type === "boolean")
117
+ return "checkbox";
118
+ if (type === "integer" || type === "bigint" || type === "number") {
119
+ return "number";
120
+ }
121
+ if (type === "date")
122
+ return "date";
123
+ if (type === "datetime" || type === "timestamp")
124
+ return "datetime-local";
125
+ if (type === "text" || type === "longtext")
126
+ return "textarea";
127
+ const name = c.propertyKey.toLowerCase();
128
+ if (name.includes("email"))
129
+ return "email";
130
+ if (name.includes("password"))
131
+ return "password";
132
+ return "text";
133
+ }
134
+ function titleise(propertyKey) {
135
+ // camelCase → "Camel Case"
136
+ const spaced = propertyKey.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
137
+ return spaced.charAt(0).toUpperCase() + spaced.slice(1);
138
+ }
139
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.js","sourceRoot":"","sources":["../../src/views/form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Layout shell shared by every Station page (list / show / 404 in this
3
+ * story; create / edit will join later). Pure string output so consumers
4
+ * can `response.send(html)` directly without bundler glue.
5
+ *
6
+ * Inline CSS lives here rather than `/_assets/station/*` because Story
7
+ * 54.2 ships zero assets — the route mount that serves a separate
8
+ * stylesheet lands in 54.7 alongside the Warden + login plumbing. The
9
+ * size budget (~80 lines, ~3 KB) keeps the inline approach cheaper than
10
+ * an extra round-trip per page until then.
11
+ */
12
+ import { type SafeHtml } from "./escape.js";
13
+ /** ~80 LOC inline stylesheet. Dependency-free; no PostCSS, no Tailwind. */
14
+ export declare const STATION_INLINE_CSS: string;
15
+ /**
16
+ * Render the outer HTML shell. `bodyHtml` is a `SafeHtml` brand to make
17
+ * the trust boundary explicit: callers must escape every dynamic value
18
+ * inside the body, then wrap the final string via `safeHtml()`.
19
+ */
20
+ export declare function renderLayout(input: {
21
+ title: string;
22
+ bodyHtml: SafeHtml;
23
+ }): string;
24
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/views/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAc,KAAK,QAAQ,EAAE,MAAM,aAAa,CAAC;AAExD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,QAkDvB,CAAC;AAET;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,QAAQ,CAAC;CACnB,GAAG,MAAM,CAcT"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Layout shell shared by every Station page (list / show / 404 in this
3
+ * story; create / edit will join later). Pure string output so consumers
4
+ * can `response.send(html)` directly without bundler glue.
5
+ *
6
+ * Inline CSS lives here rather than `/_assets/station/*` because Story
7
+ * 54.2 ships zero assets — the route mount that serves a separate
8
+ * stylesheet lands in 54.7 alongside the Warden + login plumbing. The
9
+ * size budget (~80 lines, ~3 KB) keeps the inline approach cheaper than
10
+ * an extra round-trip per page until then.
11
+ */
12
+ import { escapeHtml } from "./escape.js";
13
+ /** ~80 LOC inline stylesheet. Dependency-free; no PostCSS, no Tailwind. */
14
+ export const STATION_INLINE_CSS = `
15
+ :root {
16
+ color-scheme: light dark;
17
+ --st-fg: #1f2937;
18
+ --st-fg-muted: #6b7280;
19
+ --st-bg: #ffffff;
20
+ --st-bg-alt: #f9fafb;
21
+ --st-border: #e5e7eb;
22
+ --st-accent: #2563eb;
23
+ --st-accent-hover: #1d4ed8;
24
+ --st-danger: #b91c1c;
25
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
26
+ }
27
+ @media (prefers-color-scheme: dark) {
28
+ :root {
29
+ --st-fg: #e5e7eb;
30
+ --st-fg-muted: #9ca3af;
31
+ --st-bg: #0f172a;
32
+ --st-bg-alt: #1e293b;
33
+ --st-border: #334155;
34
+ --st-accent: #60a5fa;
35
+ --st-accent-hover: #93c5fd;
36
+ }
37
+ }
38
+ body { margin: 0; color: var(--st-fg); background: var(--st-bg); }
39
+ main { max-width: 64rem; margin: 0 auto; padding: 2rem 1.5rem; }
40
+ h1 { font-size: 1.75rem; margin: 0 0 1.5rem; }
41
+ a { color: var(--st-accent); text-decoration: none; }
42
+ a:hover { color: var(--st-accent-hover); text-decoration: underline; }
43
+ table { width: 100%; border-collapse: collapse; margin-bottom: 1.5rem; }
44
+ th, td {
45
+ text-align: left;
46
+ padding: 0.5rem 0.75rem;
47
+ border-bottom: 1px solid var(--st-border);
48
+ vertical-align: top;
49
+ }
50
+ th { background: var(--st-bg-alt); font-weight: 600; }
51
+ tbody tr:hover { background: var(--st-bg-alt); }
52
+ .st-empty { color: var(--st-fg-muted); font-style: italic; }
53
+ .st-pager { display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; margin-bottom: 0.75rem; }
54
+ .st-pager a, .st-pager span { padding: 0.25rem 0.5rem; border-radius: 0.25rem; }
55
+ .st-pager a { border: 1px solid var(--st-border); }
56
+ .st-pager strong { padding: 0.25rem 0.5rem; background: var(--st-accent); color: var(--st-bg); border-radius: 0.25rem; }
57
+ .st-pager .st-disabled { color: var(--st-fg-muted); border: 1px solid var(--st-border); }
58
+ .st-pager .st-ellipsis { color: var(--st-fg-muted); }
59
+ .st-caption { color: var(--st-fg-muted); font-size: 0.875rem; }
60
+ dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.5rem 1.5rem; }
61
+ dt { font-weight: 600; color: var(--st-fg-muted); }
62
+ dd { margin: 0; }
63
+ .st-backlink { display: inline-block; margin-top: 1.5rem; }
64
+ `.trim();
65
+ /**
66
+ * Render the outer HTML shell. `bodyHtml` is a `SafeHtml` brand to make
67
+ * the trust boundary explicit: callers must escape every dynamic value
68
+ * inside the body, then wrap the final string via `safeHtml()`.
69
+ */
70
+ export function renderLayout(input) {
71
+ const titleEscaped = escapeHtml(input.title);
72
+ return `<!doctype html>
73
+ <html lang="en">
74
+ <head>
75
+ <meta charset="utf-8">
76
+ <meta name="viewport" content="width=device-width,initial-scale=1">
77
+ <title>${titleEscaped} · Station</title>
78
+ <style>${STATION_INLINE_CSS}</style>
79
+ </head>
80
+ <body>
81
+ <main>${input.bodyHtml.html}</main>
82
+ </body>
83
+ </html>`;
84
+ }
85
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"layout.js","sourceRoot":"","sources":["../../src/views/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,UAAU,EAAiB,MAAM,aAAa,CAAC;AAExD,2EAA2E;AAC3E,MAAM,CAAC,MAAM,kBAAkB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkDjC,CAAC,IAAI,EAAE,CAAC;AAET;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,KAG5B;IACA,MAAM,YAAY,GAAG,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC7C,OAAO;;;;;WAKG,YAAY;WACZ,kBAAkB;;;UAGnB,KAAK,CAAC,QAAQ,CAAC,IAAI;;QAErB,CAAC;AACT,CAAC"}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * `GET /admin/<slug>` — paginated list view.
3
+ *
4
+ * Pure `(input) => string` renderer. Every dynamic value flows through
5
+ * `escapeHtml()` or `encodeURIComponent()` (URL components). The
6
+ * "interpolation must wrap" rule is grep-enforced by
7
+ * `tests/unit/no-unescaped-interpolation.test.ts`.
8
+ */
9
+ import type { ColumnMetadata } from "@c9up/atlas";
10
+ import type { Resource } from "../types.js";
11
+ export interface ListPageInput {
12
+ resource: Resource;
13
+ rows: ReadonlyArray<Record<string, unknown>>;
14
+ columns: ReadonlyArray<ColumnMetadata>;
15
+ pkColumn: string;
16
+ page: number;
17
+ perPage: number;
18
+ total: number;
19
+ lastPage: number;
20
+ }
21
+ export declare function renderListPage(input: ListPageInput): string;
22
+ //# sourceMappingURL=list.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/views/list.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,IAAI,EAAE,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC7C,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAgB3D"}
@@ -0,0 +1,85 @@
1
+ /**
2
+ * `GET /admin/<slug>` — paginated list view.
3
+ *
4
+ * Pure `(input) => string` renderer. Every dynamic value flows through
5
+ * `escapeHtml()` or `encodeURIComponent()` (URL components). The
6
+ * "interpolation must wrap" rule is grep-enforced by
7
+ * `tests/unit/no-unescaped-interpolation.test.ts`.
8
+ */
9
+ import { escapeHtml, safeHtml } from "./escape.js";
10
+ import { renderLayout } from "./layout.js";
11
+ export function renderListPage(input) {
12
+ const { resource, rows, columns, pkColumn, page, perPage, total, lastPage } = input;
13
+ const slug = encodeURIComponent(resource.name);
14
+ const body = rows.length === 0
15
+ ? renderEmptyState(resource)
16
+ : renderTable(rows, columns, pkColumn, slug);
17
+ const pager = renderPager(slug, page, perPage, lastPage);
18
+ const caption = renderCaption(page, perPage, total);
19
+ return renderLayout({
20
+ title: resource.label,
21
+ bodyHtml: safeHtml(`<h1>${escapeHtml(resource.label)}</h1>${body}${pager}${caption}`),
22
+ });
23
+ }
24
+ function renderEmptyState(resource) {
25
+ return `<p class="st-empty">No ${escapeHtml(resource.label.toLowerCase())} yet.</p>`;
26
+ }
27
+ function renderTable(rows, columns, pkColumn, slug) {
28
+ const headers = columns
29
+ .map((c) => `<th>${escapeHtml(c.propertyKey)}</th>`)
30
+ .join("");
31
+ const headerRow = `<thead><tr>${headers}<th></th></tr></thead>`;
32
+ const bodyRows = rows
33
+ .map((row) => {
34
+ const cells = columns
35
+ .map((c) => `<td>${escapeHtml(row[c.propertyKey])}</td>`)
36
+ .join("");
37
+ const id = String(row[pkColumn] ?? "");
38
+ const showLink = `<td><a href="/admin/${slug}/${encodeURIComponent(id)}">Show</a></td>`;
39
+ return `<tr>${cells}${showLink}</tr>`;
40
+ })
41
+ .join("");
42
+ return `<table>${headerRow}<tbody>${bodyRows}</tbody></table>`;
43
+ }
44
+ function renderPager(slug, page, perPage, lastPage) {
45
+ const pp = encodeURIComponent(String(perPage));
46
+ const prev = page > 1
47
+ ? `<a href="/admin/${slug}?page=${page - 1}&perPage=${pp}">« Prev</a>`
48
+ : `<span class="st-disabled">« Prev</span>`;
49
+ const next = page < lastPage
50
+ ? `<a href="/admin/${slug}?page=${page + 1}&perPage=${pp}">Next »</a>`
51
+ : `<span class="st-disabled">Next »</span>`;
52
+ const numbers = renderPageNumbers(slug, page, lastPage, pp);
53
+ return `<div class="st-pager">${prev}${numbers}${next}</div>`;
54
+ }
55
+ function renderPageNumbers(slug, page, lastPage, pp) {
56
+ const link = (n) => n === page
57
+ ? `<strong>${n}</strong>`
58
+ : `<a href="/admin/${slug}?page=${n}&perPage=${pp}">${n}</a>`;
59
+ if (lastPage <= 7) {
60
+ const parts = [];
61
+ for (let i = 1; i <= lastPage; i++)
62
+ parts.push(link(i));
63
+ return parts.join("");
64
+ }
65
+ // Collapsed shape: 1 … current-1, current, current+1 … lastPage
66
+ const parts = [link(1)];
67
+ const start = Math.max(2, page - 1);
68
+ const end = Math.min(lastPage - 1, page + 1);
69
+ if (start > 2)
70
+ parts.push(`<span class="st-ellipsis">…</span>`);
71
+ for (let i = start; i <= end; i++)
72
+ parts.push(link(i));
73
+ if (end < lastPage - 1)
74
+ parts.push(`<span class="st-ellipsis">…</span>`);
75
+ parts.push(link(lastPage));
76
+ return parts.join("");
77
+ }
78
+ function renderCaption(page, perPage, total) {
79
+ if (total === 0)
80
+ return "";
81
+ const start = (page - 1) * perPage + 1;
82
+ const end = Math.min(page * perPage, total);
83
+ return `<p class="st-caption">Showing ${start}–${end} of ${total}</p>`;
84
+ }
85
+ //# sourceMappingURL=list.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/views/list.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,25 @@
1
+ /**
2
+ * `GET /admin/login` — sign-in form rendered by StationProvider when
3
+ * the host has `@c9up/warden` wired (Story 54.7). The form POSTs
4
+ * `email` + `password` to `/admin/login`; the provider's
5
+ * `buildLoginHandler` runs them through `auth.authenticate(...)` and
6
+ * sets a session cookie on success.
7
+ *
8
+ * Every dynamic value flows through `escapeHtml()` so a tampered
9
+ * `?error=…` query parameter cannot smuggle markup into the page.
10
+ */
11
+ export interface LoginPageInput {
12
+ /** Pre-fill the email field (e.g. after a failed attempt). */
13
+ email?: string;
14
+ /** Optional error message to display above the form. */
15
+ error?: string;
16
+ /** Form action path. Default `/admin/login`. */
17
+ action?: string;
18
+ /** Caller-controlled hidden inputs (typically `_csrf`). */
19
+ hiddenInputs?: ReadonlyArray<{
20
+ name: string;
21
+ value: string;
22
+ }>;
23
+ }
24
+ export declare function renderLoginPage(input: LoginPageInput): string;
25
+ //# sourceMappingURL=login.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.d.ts","sourceRoot":"","sources":["../../src/views/login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,MAAM,WAAW,cAAc;IAC9B,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gDAAgD;IAChD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2DAA2D;IAC3D,YAAY,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC9D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAuC7D"}
@@ -0,0 +1,44 @@
1
+ /**
2
+ * `GET /admin/login` — sign-in form rendered by StationProvider when
3
+ * the host has `@c9up/warden` wired (Story 54.7). The form POSTs
4
+ * `email` + `password` to `/admin/login`; the provider's
5
+ * `buildLoginHandler` runs them through `auth.authenticate(...)` and
6
+ * sets a session cookie on success.
7
+ *
8
+ * Every dynamic value flows through `escapeHtml()` so a tampered
9
+ * `?error=…` query parameter cannot smuggle markup into the page.
10
+ */
11
+ import { escapeHtml, safeHtml } from "./escape.js";
12
+ import { renderLayout } from "./layout.js";
13
+ export function renderLoginPage(input) {
14
+ const email = input.email ?? "";
15
+ const error = input.error;
16
+ const action = input.action ?? "/admin/login";
17
+ const hiddens = (input.hiddenInputs ?? [])
18
+ .map((h) => `<input type="hidden" name="${escapeHtml(h.name)}" value="${escapeHtml(h.value)}">`)
19
+ .join("");
20
+ const errorBlock = error !== undefined
21
+ ? `<p class="st-form-error" role="alert">${escapeHtml(error)}</p>`
22
+ : "";
23
+ const body = `<h1>Sign in</h1>` +
24
+ errorBlock +
25
+ `<form class="st-form" method="POST" action="${escapeHtml(action)}">` +
26
+ hiddens +
27
+ `<div class="st-field">` +
28
+ `<label for="f-email">Email</label>` +
29
+ `<input id="f-email" type="email" name="email" value="${escapeHtml(email)}" required autocomplete="email" autofocus>` +
30
+ `</div>` +
31
+ `<div class="st-field">` +
32
+ `<label for="f-password">Password</label>` +
33
+ `<input id="f-password" type="password" name="password" required autocomplete="current-password">` +
34
+ `</div>` +
35
+ `<div class="st-form-actions">` +
36
+ `<button type="submit">Sign in</button>` +
37
+ `</div>` +
38
+ `</form>`;
39
+ return renderLayout({
40
+ title: "Sign in",
41
+ bodyHtml: safeHtml(body),
42
+ });
43
+ }
44
+ //# sourceMappingURL=login.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login.js","sourceRoot":"","sources":["../../src/views/login.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAa3C,MAAM,UAAU,eAAe,CAAC,KAAqB;IACpD,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC;IAChC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1B,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,IAAI,cAAc,CAAC;IAE9C,MAAM,OAAO,GAAG,CAAC,KAAK,CAAC,YAAY,IAAI,EAAE,CAAC;SACxC,GAAG,CACH,CAAC,CAAC,EAAE,EAAE,CACL,8BAA8B,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CACpF;SACA,IAAI,CAAC,EAAE,CAAC,CAAC;IAEX,MAAM,UAAU,GACf,KAAK,KAAK,SAAS;QAClB,CAAC,CAAC,yCAAyC,UAAU,CAAC,KAAK,CAAC,MAAM;QAClE,CAAC,CAAC,EAAE,CAAC;IAEP,MAAM,IAAI,GACT,kBAAkB;QAClB,UAAU;QACV,+CAA+C,UAAU,CAAC,MAAM,CAAC,IAAI;QACrE,OAAO;QACP,wBAAwB;QACxB,oCAAoC;QACpC,wDAAwD,UAAU,CAAC,KAAK,CAAC,4CAA4C;QACrH,QAAQ;QACR,wBAAwB;QACxB,0CAA0C;QAC1C,kGAAkG;QAClG,QAAQ;QACR,+BAA+B;QAC/B,wCAAwC;QACxC,QAAQ;QACR,SAAS,CAAC;IAEX,OAAO,YAAY,CAAC;QACnB,KAAK,EAAE,SAAS;QAChB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC;KACxB,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * `GET /admin/<slug>/:id` — detail view (columns only in 54.2).
3
+ *
4
+ * Relations are deferred — see story spec AC17 Spec Deviations. Every
5
+ * dynamic value flows through `escapeHtml()`; URL components through
6
+ * `encodeURIComponent()`.
7
+ */
8
+ import type { ColumnMetadata } from "@c9up/atlas";
9
+ import type { Resource } from "../types.js";
10
+ export interface ShowPageInput {
11
+ resource: Resource;
12
+ row: Record<string, unknown>;
13
+ columns: ReadonlyArray<ColumnMetadata>;
14
+ pkColumn: string;
15
+ }
16
+ export declare function renderShowPage(input: ShowPageInput): string;
17
+ //# sourceMappingURL=show.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show.d.ts","sourceRoot":"","sources":["../../src/views/show.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAClD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAI5C,MAAM,WAAW,aAAa;IAC7B,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7B,OAAO,EAAE,aAAa,CAAC,cAAc,CAAC,CAAC;IACvC,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,cAAc,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAgB3D"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `GET /admin/<slug>/:id` — detail view (columns only in 54.2).
3
+ *
4
+ * Relations are deferred — see story spec AC17 Spec Deviations. Every
5
+ * dynamic value flows through `escapeHtml()`; URL components through
6
+ * `encodeURIComponent()`.
7
+ */
8
+ import { escapeHtml, safeHtml } from "./escape.js";
9
+ import { renderLayout } from "./layout.js";
10
+ export function renderShowPage(input) {
11
+ const { resource, row, columns, pkColumn } = input;
12
+ const id = String(row[pkColumn] ?? "");
13
+ const slug = encodeURIComponent(resource.name);
14
+ const heading = `${escapeHtml(resource.label)} #${escapeHtml(id)}`;
15
+ const dl = columns
16
+ .map((c) => `<dt>${escapeHtml(c.propertyKey)}</dt><dd>${escapeHtml(row[c.propertyKey])}</dd>`)
17
+ .join("");
18
+ const backLink = `<a class="st-backlink" href="/admin/${slug}">← Back to ${escapeHtml(resource.label)}</a>`;
19
+ return renderLayout({
20
+ title: `${resource.label} #${id}`,
21
+ bodyHtml: safeHtml(`<h1>${heading}</h1><dl>${dl}</dl>${backLink}`),
22
+ });
23
+ }
24
+ //# sourceMappingURL=show.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"show.js","sourceRoot":"","sources":["../../src/views/show.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAIH,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAS3C,MAAM,UAAU,cAAc,CAAC,KAAoB;IAClD,MAAM,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,QAAQ,EAAE,GAAG,KAAK,CAAC;IACnD,MAAM,EAAE,GAAG,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACvC,MAAM,IAAI,GAAG,kBAAkB,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,OAAO,GAAG,GAAG,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,KAAK,UAAU,CAAC,EAAE,CAAC,EAAE,CAAC;IACnE,MAAM,EAAE,GAAG,OAAO;SAChB,GAAG,CACH,CAAC,CAAC,EAAE,EAAE,CACL,OAAO,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,OAAO,CAClF;SACA,IAAI,CAAC,EAAE,CAAC,CAAC;IACX,MAAM,QAAQ,GAAG,uCAAuC,IAAI,eAAe,UAAU,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC;IAC5G,OAAO,YAAY,CAAC;QACnB,KAAK,EAAE,GAAG,QAAQ,CAAC,KAAK,KAAK,EAAE,EAAE;QACjC,QAAQ,EAAE,QAAQ,CAAC,OAAO,OAAO,YAAY,EAAE,QAAQ,QAAQ,EAAE,CAAC;KAClE,CAAC,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@c9up/station",
3
+ "version": "0.1.5",
4
+ "description": "Station — admin scaffolding for the Ream framework. Declarative resource catalogue (defineResource) backing list/show/create/edit/destroy routes, audit, policies.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ },
14
+ "./services/main": {
15
+ "types": "./dist/services/main.d.ts",
16
+ "import": "./dist/services/main.js"
17
+ }
18
+ },
19
+ "peerDependencies": {
20
+ "@c9up/atlas": "^0.1.0",
21
+ "@c9up/ream": "^0.1.0",
22
+ "@c9up/warden": "^0.1.0"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "@c9up/atlas": {
26
+ "optional": true
27
+ },
28
+ "@c9up/ream": {
29
+ "optional": true
30
+ },
31
+ "@c9up/warden": {
32
+ "optional": true
33
+ }
34
+ },
35
+ "devDependencies": {
36
+ "@c9up/atlas": "^0.1.0",
37
+ "@types/node": "^22.19.15",
38
+ "@vitest/coverage-v8": "^4.1.2",
39
+ "reflect-metadata": "^0.2.2",
40
+ "typescript": "^6.0.2",
41
+ "vitest": "^4.1.2"
42
+ },
43
+ "files": [
44
+ "LICENSE",
45
+ "README.md",
46
+ "dist",
47
+ "src"
48
+ ],
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/C9up/station.git"
55
+ },
56
+ "scripts": {
57
+ "build": "tsc -p tsconfig.build.json",
58
+ "typecheck": "tsc --noEmit",
59
+ "test": "vitest run",
60
+ "lint": "biome check src/",
61
+ "test:coverage": "vitest run --coverage"
62
+ }
63
+ }