@fougere/app 0.2.0-alpha.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 (50) hide show
  1. package/LICENSE +21 -0
  2. package/dist/auth.d.ts +11 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +10 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/boot.d.ts +35 -0
  7. package/dist/boot.d.ts.map +1 -0
  8. package/dist/boot.js +208 -0
  9. package/dist/boot.js.map +1 -0
  10. package/dist/client.d.ts +64 -0
  11. package/dist/client.d.ts.map +1 -0
  12. package/dist/client.js +124 -0
  13. package/dist/client.js.map +1 -0
  14. package/dist/express.d.ts +25 -0
  15. package/dist/express.d.ts.map +1 -0
  16. package/dist/express.js +180 -0
  17. package/dist/express.js.map +1 -0
  18. package/dist/form.d.ts +102 -0
  19. package/dist/form.d.ts.map +1 -0
  20. package/dist/form.js +104 -0
  21. package/dist/form.js.map +1 -0
  22. package/dist/graphql.d.ts +34 -0
  23. package/dist/graphql.d.ts.map +1 -0
  24. package/dist/graphql.js +58 -0
  25. package/dist/graphql.js.map +1 -0
  26. package/dist/index.d.ts +20 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +20 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/rest.d.ts +41 -0
  31. package/dist/rest.d.ts.map +1 -0
  32. package/dist/rest.js +92 -0
  33. package/dist/rest.js.map +1 -0
  34. package/dist/serve.d.ts +110 -0
  35. package/dist/serve.d.ts.map +1 -0
  36. package/dist/serve.js +136 -0
  37. package/dist/serve.js.map +1 -0
  38. package/dist/session.d.ts +14 -0
  39. package/dist/session.d.ts.map +1 -0
  40. package/dist/session.js +17 -0
  41. package/dist/session.js.map +1 -0
  42. package/dist/state.d.ts +3 -0
  43. package/dist/state.d.ts.map +1 -0
  44. package/dist/state.js +32 -0
  45. package/dist/state.js.map +1 -0
  46. package/dist/web.d.ts +25 -0
  47. package/dist/web.d.ts.map +1 -0
  48. package/dist/web.js +93 -0
  49. package/dist/web.js.map +1 -0
  50. package/package.json +86 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * The doors as Express middlewares — the form an Express app expects.
3
+ *
4
+ * ```ts
5
+ * const app = express();
6
+ * app.use(express.json());
7
+ * app.use(fougere()); // ← one line, like cors() or express.json()
8
+ * ```
9
+ *
10
+ * This replaces an earlier `mountDoors(createExpressRouter(app))`, which was wrong in
11
+ * two ways at once. It handed the app INTO a function instead of adding something to
12
+ * the app, and it made the caller name Express 5's wildcard syntax (`/api/*splat`) —
13
+ * a detail that belongs to the framework, not to its user. Worse, it mounted all
14
+ * three doors together with no way to take two of them.
15
+ *
16
+ * A middleware fixes both by construction, because Express already says WHERE:
17
+ *
18
+ * ```ts
19
+ * app.use(fougere()); // the three doors, at the paths the client knows
20
+ * app.use('/admin', fougereRest()); // REST only, wherever you want it
21
+ * ```
22
+ *
23
+ * And `next()` is the passthrough these doors were already imitating: `serveRest`
24
+ * answers `{ kind: 'pass' }` for a path it does not serve, which is exactly what
25
+ * Express means by calling the next handler. The middleware stops inventing it.
26
+ *
27
+ * The names mirror `@fougere/app/web` on purpose — same doors, host-shaped. `/web`
28
+ * gives you `Request` → `Response` handlers; this gives you middlewares.
29
+ *
30
+ * Nothing here imports express: the shapes are structural, so the package keeps its
31
+ * dependency list and a test can hand it a plain object.
32
+ */
33
+ import { readExpressBody } from '@fougere/http';
34
+ import { serveRest, serveRpc, rpcParseError } from './serve.js';
35
+ import { serveGraphQL } from './graphql.js';
36
+ import { sessionViewOf } from './session.js';
37
+ import { useFougereApp } from './boot.js';
38
+ /**
39
+ * Who the caller is, from what ran before.
40
+ *
41
+ * Express has no ambient request, so an app that resolves its own session says so by
42
+ * putting it on the request — `req.fougereState`, or a bare `req.user`, which is what
43
+ * passport and most middlewares already set. Nothing is taken from the payload: the
44
+ * browser sits outside the topology.
45
+ */
46
+ function stateOf(req) {
47
+ if (req.fougereState)
48
+ return req.fougereState;
49
+ return req.user ? { user: req.user } : {};
50
+ }
51
+ function pathOf(req) {
52
+ return req.path ?? String(req.originalUrl ?? req.url ?? '').split('?')[0];
53
+ }
54
+ function queryOf(req) {
55
+ return Object.fromEntries(Object.entries(req.query ?? {}).map(([key, value]) => [
56
+ key,
57
+ String(Array.isArray(value) ? value[0] : value),
58
+ ]));
59
+ }
60
+ /** Turn a thrown failure into Express's own error pipeline, unless it is a refusal we own. */
61
+ function fail(res, next, err) {
62
+ const code = err?.name === 'MalformedJsonError' ? 400 : 0;
63
+ if (code === 400) {
64
+ res.status(400).json({ code: 'BAD_REQUEST', message: 'Malformed JSON body' });
65
+ return;
66
+ }
67
+ next(err);
68
+ }
69
+ /**
70
+ * The call envelope, at `/_fougere/call` — the door the browser primitives use.
71
+ *
72
+ * Mounted with `app.use()`, Express strips nothing, so the path still carries the
73
+ * audience segment (`/_fougere/call/public`) that `surfaceOf` reads.
74
+ */
75
+ export function fougereCall(mountPath = '/_fougere/call') {
76
+ return (req, res, next) => {
77
+ const path = pathOf(req);
78
+ if (req.method !== 'POST' || !path.startsWith(mountPath))
79
+ return next();
80
+ void (async () => {
81
+ try {
82
+ const app = await useFougereApp();
83
+ let body;
84
+ try {
85
+ body = await readExpressBody(req);
86
+ }
87
+ catch {
88
+ res.status(200).json(rpcParseError());
89
+ return;
90
+ }
91
+ res.status(200).json(await serveRpc(app, { path, body, state: stateOf(req) }));
92
+ }
93
+ catch (err) {
94
+ fail(res, next, err);
95
+ }
96
+ })();
97
+ };
98
+ }
99
+ /** The session view, at `/_fougere/session`. */
100
+ export function fougereSession(mountPath = '/_fougere/session') {
101
+ return (req, res, next) => {
102
+ if (req.method !== 'GET' || pathOf(req) !== mountPath)
103
+ return next();
104
+ res.status(200).json(sessionViewOf(stateOf(req)));
105
+ };
106
+ }
107
+ /**
108
+ * The REST projection, under `/api` by default.
109
+ *
110
+ * A path this app does not serve calls `next()` — so an app's own `/api/health` keeps
111
+ * answering whether it was registered before or after this middleware.
112
+ */
113
+ export function fougereRest(mountPath = '/api') {
114
+ return (req, res, next) => {
115
+ const path = pathOf(req);
116
+ if (!path.startsWith(`${mountPath}/`))
117
+ return next();
118
+ void (async () => {
119
+ try {
120
+ const app = await useFougereApp();
121
+ const outcome = await serveRest(app, {
122
+ method: req.method,
123
+ path: path.slice(mountPath.length + 1),
124
+ query: queryOf(req),
125
+ body: await readExpressBody(req),
126
+ state: stateOf(req),
127
+ });
128
+ // Not ours — Express's own passthrough, which is what `pass` always meant.
129
+ if (outcome.kind === 'pass')
130
+ return next();
131
+ if (outcome.kind === 'error' && outcome.headers) {
132
+ for (const [key, value] of Object.entries(outcome.headers))
133
+ res.set(key, value);
134
+ }
135
+ res.status(outcome.status).json(outcome.body);
136
+ }
137
+ catch (err) {
138
+ fail(res, next, err);
139
+ }
140
+ })();
141
+ };
142
+ }
143
+ /** GraphQL, at `/graphql` by default. Declines when the app declares no such adapter. */
144
+ export function fougereGraphQL(mountPath = '/graphql') {
145
+ return (req, res, next) => {
146
+ if (req.method !== 'POST' || pathOf(req) !== mountPath)
147
+ return next();
148
+ void (async () => {
149
+ try {
150
+ const app = await useFougereApp();
151
+ const body = ((await readExpressBody(req)) ?? {});
152
+ const outcome = await serveGraphQL(app, { ...body, state: stateOf(req) });
153
+ if (outcome.kind === 'pass')
154
+ return next();
155
+ res.status(outcome.status).json(outcome.body);
156
+ }
157
+ catch (err) {
158
+ fail(res, next, err);
159
+ }
160
+ })();
161
+ };
162
+ }
163
+ /** Every door, for an app that wants all of them. What each one SERVES is still the
164
+ * app's declaration — mounting is not publishing. */
165
+ export function fougere() {
166
+ const doors = [fougereCall(), fougereSession(), fougereRest(), fougereGraphQL()];
167
+ return (req, res, next) => {
168
+ let index = 0;
169
+ const step = (err) => {
170
+ if (err)
171
+ return next(err);
172
+ const door = doors[index++];
173
+ if (!door)
174
+ return next();
175
+ door(req, res, step);
176
+ };
177
+ step();
178
+ };
179
+ }
180
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../src/express.ts"],"names":[],"mappings":"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"}
package/dist/form.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { ValidationError } from '@fougere/schema';
2
+ /** What an entity class exposes to a form — the schema statics it already has. */
3
+ export interface FormEntity {
4
+ name: string;
5
+ getFields(): Record<string, FieldLike>;
6
+ validate(input: unknown): {
7
+ success: true;
8
+ data: unknown;
9
+ } | {
10
+ success: false;
11
+ errors: ValidationError[];
12
+ };
13
+ }
14
+ interface FieldLike {
15
+ shape?: {
16
+ type?: unknown;
17
+ enum?: readonly unknown[];
18
+ format?: string;
19
+ properties?: unknown;
20
+ minLength?: number;
21
+ maxLength?: number;
22
+ minimum?: number;
23
+ maximum?: number;
24
+ pattern?: string;
25
+ };
26
+ lifecycle?: {
27
+ create?: unknown;
28
+ };
29
+ role?: {
30
+ primary?: boolean;
31
+ relation?: {
32
+ kind: string;
33
+ };
34
+ };
35
+ }
36
+ export interface FormField {
37
+ name: string;
38
+ /** Rendering hint derived from the shape — the page maps it to widgets. */
39
+ control: 'text' | 'email' | 'url' | 'number' | 'boolean' | 'date' | 'select';
40
+ required: boolean;
41
+ /** i18n key by convention: `entity.field`. The schema never carries display text. */
42
+ labelKey: string;
43
+ /** Fallback label when no i18n message fills the key. */
44
+ label: string;
45
+ /** Enum values, when control is 'select'. */
46
+ options?: string[];
47
+ /**
48
+ * What the browser enforces, under the names it already knows — spread this on the
49
+ * input and the page states no rule of its own.
50
+ *
51
+ * The shape holds `minLength`/`maximum`/`pattern`; a browser holds `minlength`/
52
+ * `max`/`pattern` and enforces them with no JavaScript at all. Carrying them here
53
+ * is a projection, not a second rule: the judge reads the same shape, and a form
54
+ * that ignores these still gets the same verdict — it just gets it later, and a
55
+ * screen reader never gets it at all.
56
+ *
57
+ * `type` is part of the contract, not decoration: `email` and `url` are formats the
58
+ * shape states and the browser checks live, per field, as one types. A page writing
59
+ * `type="email"` by hand is spelling a second time what the card already said.
60
+ *
61
+ * Three deliberate absences, each one a place where the attribute would mean
62
+ * something the shape does not say:
63
+ * - a `date` field gets no `type` — neither `date` nor `datetime-local` produces the
64
+ * RFC 3339 string a `date-time` shape judges, so the browser would accept what the
65
+ * judge refuses;
66
+ * - `select` and `boolean` are not inputs — the page picks the widget, `control` says
67
+ * which;
68
+ * - a required `boolean` gets no `required` — on a checkbox that attribute means
69
+ * "must be CHECKED", where the shape only says the value must be supplied.
70
+ */
71
+ attrs?: {
72
+ type?: 'text' | 'email' | 'url' | 'number';
73
+ required?: boolean;
74
+ minlength?: number;
75
+ maxlength?: number;
76
+ min?: number;
77
+ max?: number;
78
+ pattern?: string;
79
+ };
80
+ /**
81
+ * The value the field is born with — the literal its `lifecycle.create` rule names.
82
+ * Present so the form can SHOW what is about to be written; the storage realizes it
83
+ * either way, so a form that ignores this still produces the same row.
84
+ */
85
+ default?: unknown;
86
+ }
87
+ /**
88
+ * The fields a create form is made of: membership from the io projection
89
+ * (`inputFields` — what a client may supply), requiredness from the
90
+ * lifecycle axis (any create rule makes absence legal).
91
+ */
92
+ export declare function formFieldsOf(entity: FormEntity, entityKey: string): FormField[];
93
+ /**
94
+ * The wire body of the form's values — an empty control is an absent value
95
+ * at the create boundary (absence is judged by the lifecycle axis, an empty
96
+ * string would be judged as a present bad value).
97
+ */
98
+ export declare function payloadOf(values: Record<string, unknown>): Record<string, unknown>;
99
+ /** Index judge errors by field — local judge and remote judge share this shape. */
100
+ export declare function errorsByField(errors: ValidationError[]): Record<string, string>;
101
+ export {};
102
+ //# sourceMappingURL=form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.d.ts","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAEvD,kFAAkF;AAClF,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IACvC,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG;QAAE,OAAO,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAE,GAAG;QAAE,OAAO,EAAE,KAAK,CAAC;QAAC,MAAM,EAAE,eAAe,EAAE,CAAA;KAAE,CAAC;CAC5G;AAED,UAAU,SAAS;IACjB,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,IAAI,CAAC,EAAE,SAAS,OAAO,EAAE,CAAC;QAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,UAAU,CAAC,EAAE,OAAO,CAAC;QACrB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,SAAS,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACjC,IAAI,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,EAAE;YAAE,IAAI,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAC3D;AAiBD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,2EAA2E;IAC3E,OAAO,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;IAC7E,QAAQ,EAAE,OAAO,CAAC;IAClB,qFAAqF;IACrF,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC;QAC3C,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AA2CD;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,CAmB/E;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAIlF;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAO/E"}
package/dist/form.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Form contract, pure part — derives what a create/edit form is made of
3
+ * from the entity's field axes. No Vue, no Nuxt: testable headless,
4
+ * usable by any renderer (the page owns the widgets).
5
+ */
6
+ import { inputFields } from '@fougere/schema';
7
+ /**
8
+ * The literal a field is born with, when it declares one.
9
+ *
10
+ * `text({ default: 'x' })` and `oneOf('a', 'b', { default: 'a' })` both compile to
11
+ * `lifecycle.create = { value }` — the create rule that answers the field's absence.
12
+ * The other create rules ('now', { generate }, 'optional') name no literal: their value
13
+ * is decided at write time, so a form has nothing to show for them.
14
+ */
15
+ function defaultOf(field) {
16
+ const create = field.lifecycle?.create;
17
+ return create !== null && typeof create === 'object' && 'value' in create
18
+ ? create.value
19
+ : undefined;
20
+ }
21
+ /** The base JSON type of a shape — unwraps the `[T,'null']` union. */
22
+ function baseType(type) {
23
+ if (Array.isArray(type))
24
+ return type.find((t) => t !== 'null') ?? 'string';
25
+ return type ?? 'string';
26
+ }
27
+ /** The formats a browser has an input type for — the rest stay `text`, judged later. */
28
+ const CONTROL_BY_FORMAT = {
29
+ 'date-time': 'date',
30
+ email: 'email',
31
+ uri: 'url',
32
+ };
33
+ function controlOf(field) {
34
+ const shape = field.shape ?? {};
35
+ if (Array.isArray(shape.enum) && shape.enum.length)
36
+ return 'select';
37
+ const base = baseType(shape.type);
38
+ if (base === 'number' || base === 'integer')
39
+ return 'number';
40
+ if (base === 'boolean')
41
+ return 'boolean';
42
+ if (base === 'string' && shape.format)
43
+ return CONTROL_BY_FORMAT[shape.format] ?? 'text';
44
+ return 'text';
45
+ }
46
+ /** Controls that ARE an `<input type>` — see the two absences on {@link FormField.attrs}. */
47
+ const INPUT_TYPES = new Set(['text', 'email', 'url', 'number']);
48
+ /** The shape's bounds, under the names a browser already enforces. */
49
+ function attrsOf(field, control, required) {
50
+ const s = field.shape ?? {};
51
+ const attrs = {
52
+ type: INPUT_TYPES.has(control) ? control : undefined,
53
+ required: (required && control !== 'boolean') || undefined,
54
+ minlength: s.minLength,
55
+ maxlength: s.maxLength,
56
+ min: s.minimum,
57
+ max: s.maximum,
58
+ pattern: s.pattern,
59
+ };
60
+ return Object.fromEntries(Object.entries(attrs).filter(([, v]) => v !== undefined));
61
+ }
62
+ /**
63
+ * The fields a create form is made of: membership from the io projection
64
+ * (`inputFields` — what a client may supply), requiredness from the
65
+ * lifecycle axis (any create rule makes absence legal).
66
+ */
67
+ export function formFieldsOf(entity, entityKey) {
68
+ return Object.entries(inputFields(entity.getFields())).map(([name, field]) => {
69
+ const f = field;
70
+ const control = controlOf(f);
71
+ const required = f.lifecycle?.create === undefined;
72
+ const attrs = attrsOf(f, control, required);
73
+ return {
74
+ name,
75
+ control,
76
+ required,
77
+ labelKey: `${entityKey}.${name}`,
78
+ label: name.charAt(0).toUpperCase() + name.slice(1),
79
+ ...(Array.isArray(f.shape?.enum)
80
+ ? { options: f.shape.enum.filter((value) => typeof value === 'string') }
81
+ : {}),
82
+ ...(Object.keys(attrs).length ? { attrs } : {}),
83
+ ...(defaultOf(f) !== undefined ? { default: defaultOf(f) } : {}),
84
+ };
85
+ });
86
+ }
87
+ /**
88
+ * The wire body of the form's values — an empty control is an absent value
89
+ * at the create boundary (absence is judged by the lifecycle axis, an empty
90
+ * string would be judged as a present bad value).
91
+ */
92
+ export function payloadOf(values) {
93
+ return Object.fromEntries(Object.entries(values).filter(([, v]) => v !== undefined && v !== ''));
94
+ }
95
+ /** Index judge errors by field — local judge and remote judge share this shape. */
96
+ export function errorsByField(errors) {
97
+ const byField = {};
98
+ for (const err of errors) {
99
+ const field = err.path.split('.')[0] || err.path;
100
+ byField[field] ??= err.message;
101
+ }
102
+ return byField;
103
+ }
104
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.js","sourceRoot":"","sources":["../src/form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The GraphQL door — declared like the others, mounted like the others.
3
+ *
4
+ * Until now GraphQL was the odd one out: `adapters: { graphql: true }` was
5
+ * declarable and nothing read it, while an app that wanted a schema wrote fifteen
6
+ * lines of its own (a Pothos builder, a query type, a mutation type, `registerAll`,
7
+ * then `registerGraphQL` on a router). Every one of those lines is convention —
8
+ * there is no decision in them — so the app declaring the adapter is enough.
9
+ *
10
+ * The packages are imported LAZILY, and that is the same shape `db: 'sqlite'` uses
11
+ * (`resolveStorage` pulls `@fougere/schema-sql` only when a database is declared).
12
+ * `graphql` and `@pothos/core` are heavy and most apps serve none, so a host must not
13
+ * carry them to find out. Declaring the adapter without installing them is refused by
14
+ * name, the way an unresolvable dialect is.
15
+ */
16
+ import type { App } from '@fougere/core';
17
+ import type { Outcome } from './serve.js';
18
+ /** What a GraphQL request carries, whatever host read it. */
19
+ export interface GraphQLRequest {
20
+ query?: string;
21
+ variables?: Record<string, unknown>;
22
+ operationName?: string;
23
+ /** The audience, when the door was mounted per surface. */
24
+ surface?: string;
25
+ state: Record<string, unknown>;
26
+ }
27
+ /**
28
+ * Execute a GraphQL request against the app's own operations.
29
+ *
30
+ * `pass` when the app declares no GraphQL adapter — the same answer `serveRest` gives,
31
+ * so a host that mounted the door takes nothing away from an app that did not ask for it.
32
+ */
33
+ export declare function serveGraphQL(app: App, request: GraphQLRequest): Promise<Outcome>;
34
+ //# sourceMappingURL=graphql.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql.d.ts","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC;AAE1C,6DAA6D;AAC7D,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAyCD;;;;;GAKG;AACH,wBAAsB,YAAY,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBtF"}
@@ -0,0 +1,58 @@
1
+ /** One executable schema per (app, audience) — building it walks every entity. */
2
+ const schemas = new WeakMap();
3
+ async function schemaFor(app, surface) {
4
+ const perApp = schemas.get(app) ?? new Map();
5
+ schemas.set(app, perApp);
6
+ const key = surface ?? '';
7
+ const built = perApp.get(key);
8
+ if (built)
9
+ return built;
10
+ let SchemaBuilder;
11
+ let registerAll;
12
+ try {
13
+ ({ default: SchemaBuilder } = await import('@pothos/core'));
14
+ ({ registerAll } = await import('@fougere/schema-graphql'));
15
+ }
16
+ catch (cause) {
17
+ throw new Error("adapters: { graphql: true } is declared, but the packages that serve it are not " +
18
+ "installed. Add `@fougere/schema-graphql` and `@pothos/core` — they are not " +
19
+ 'dependencies of the host, because an app that serves no GraphQL should not carry ' +
20
+ 'a schema builder to find that out.', { cause });
21
+ }
22
+ // Every line here is convention: a builder, the two root types, and the entities
23
+ // the app already scanned. Nothing an app could usefully say differently — which is
24
+ // why declaring the adapter is the whole configuration.
25
+ const builder = new SchemaBuilder({});
26
+ builder.queryType({});
27
+ builder.mutationType({});
28
+ registerAll(builder, app, surface ? { surface } : undefined);
29
+ const schema = builder.toSchema();
30
+ perApp.set(key, schema);
31
+ return schema;
32
+ }
33
+ /**
34
+ * Execute a GraphQL request against the app's own operations.
35
+ *
36
+ * `pass` when the app declares no GraphQL adapter — the same answer `serveRest` gives,
37
+ * so a host that mounted the door takes nothing away from an app that did not ask for it.
38
+ */
39
+ export async function serveGraphQL(app, request) {
40
+ if (!app.adapters?.graphql)
41
+ return { kind: 'pass' };
42
+ if (!request.query) {
43
+ return { kind: 'error', status: 400, body: { message: 'Missing query' } };
44
+ }
45
+ const { graphql } = await import('graphql');
46
+ const result = await graphql({
47
+ schema: (await schemaFor(app, request.surface)),
48
+ source: request.query,
49
+ variableValues: request.variables,
50
+ operationName: request.operationName,
51
+ // The same state every other door stamps: what the server resolved, never the wire.
52
+ contextValue: { state: request.state },
53
+ });
54
+ // A GraphQL error is not an HTTP error: the transport succeeded, and the errors ride
55
+ // in the body where a client is required to look for them.
56
+ return { kind: 'ok', status: 200, body: result };
57
+ }
58
+ //# sourceMappingURL=graphql.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graphql.js","sourceRoot":"","sources":["../src/graphql.ts"],"names":[],"mappings":"AA4BA,kFAAkF;AAClF,MAAM,OAAO,GAAG,IAAI,OAAO,EAA6B,CAAC;AAEzD,KAAK,UAAU,SAAS,CAAC,GAAQ,EAAE,OAAgB;IACjD,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,IAAI,GAAG,EAAmB,CAAC;IAC9D,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAEzB,MAAM,GAAG,GAAG,OAAO,IAAI,EAAE,CAAC;IAC1B,MAAM,KAAK,GAAG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,KAAK;QAAE,OAAO,KAAK,CAAC;IAExB,IAAI,aAAkB,CAAC;IACvB,IAAI,WAAgB,CAAC;IACrB,IAAI,CAAC;QACH,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC;QAC5D,CAAC,EAAE,WAAW,EAAE,GAAG,MAAM,MAAM,CAAC,yBAAyB,CAAC,CAAC,CAAC;IAC9D,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CACb,kFAAkF;YAClF,6EAA6E;YAC7E,mFAAmF;YACnF,oCAAoC,EACpC,EAAE,KAAK,EAAE,CACV,CAAC;IACJ,CAAC;IAED,iFAAiF;IACjF,oFAAoF;IACpF,wDAAwD;IACxD,MAAM,OAAO,GAAG,IAAI,aAAa,CAAC,EAAE,CAAC,CAAC;IACtC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC;IACtB,OAAO,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACzB,WAAW,CAAC,OAAO,EAAE,GAAY,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAEtE,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC;IAClC,MAAM,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IACxB,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,GAAQ,EAAE,OAAuB;IAClE,IAAI,CAAC,GAAG,CAAC,QAAQ,EAAE,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;IAEpD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACnB,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,EAAE,CAAC;IAC5E,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,CAAC;IAC5C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC;QAC3B,MAAM,EAAE,CAAC,MAAM,SAAS,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,CAAU;QACxD,MAAM,EAAE,OAAO,CAAC,KAAK;QACrB,cAAc,EAAE,OAAO,CAAC,SAAS;QACjC,aAAa,EAAE,OAAO,CAAC,aAAa;QACpC,oFAAoF;QACpF,YAAY,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE;KACvC,CAAC,CAAC;IAEH,qFAAqF;IACrF,2DAA2D;IAC3D,OAAO,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AACnD,CAAC"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@fougere/app` — what an app host needs and what no host owns.
3
+ *
4
+ * Two adapters read this package: `@fougere/nuxt` and `@fougere/next`. Between
5
+ * them they share the boot, the three doors' decisions, the REST table, the form
6
+ * contract and the session view; what they do NOT share is how a request arrives,
7
+ * how routes are mounted, and how a value becomes reactive.
8
+ *
9
+ * The root entry pulls in the boot, which reads the filesystem — client code wants
10
+ * `@fougere/app/client`.
11
+ */
12
+ export { configureFougere, useFougereApp, createMemoryOrm, type FougereServerConfig, } from './boot.js';
13
+ export { useFougereAuth } from './auth.js';
14
+ export { tableOf, matchRoute, type Matchable, type RouteMatch, } from './rest.js';
15
+ export { serveRest, shapeRest, serveRpc, surfaceOf, rpcParseError, invokeOn, type DoorRequest, type Outcome, } from './serve.js';
16
+ export { formFieldsOf, payloadOf, errorsByField, type FormEntity, type FormField, } from './form.js';
17
+ export { sessionViewOf, type SessionView } from './session.js';
18
+ export { stateFor } from './state.js';
19
+ export { serveGraphQL, type GraphQLRequest } from './graphql.js';
20
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,EACf,KAAK,mBAAmB,GACzB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EACL,OAAO,EACP,UAAU,EACV,KAAK,SAAS,EACd,KAAK,UAAU,GAChB,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,SAAS,EACT,QAAQ,EACR,SAAS,EACT,aAAa,EACb,QAAQ,EACR,KAAK,WAAW,EAChB,KAAK,OAAO,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,aAAa,EACb,KAAK,UAAU,EACf,KAAK,SAAS,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,aAAa,EAAE,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAE,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@fougere/app` — what an app host needs and what no host owns.
3
+ *
4
+ * Two adapters read this package: `@fougere/nuxt` and `@fougere/next`. Between
5
+ * them they share the boot, the three doors' decisions, the REST table, the form
6
+ * contract and the session view; what they do NOT share is how a request arrives,
7
+ * how routes are mounted, and how a value becomes reactive.
8
+ *
9
+ * The root entry pulls in the boot, which reads the filesystem — client code wants
10
+ * `@fougere/app/client`.
11
+ */
12
+ export { configureFougere, useFougereApp, createMemoryOrm, } from './boot.js';
13
+ export { useFougereAuth } from './auth.js';
14
+ export { tableOf, matchRoute, } from './rest.js';
15
+ export { serveRest, shapeRest, serveRpc, surfaceOf, rpcParseError, invokeOn, } from './serve.js';
16
+ export { formFieldsOf, payloadOf, errorsByField, } from './form.js';
17
+ export { sessionViewOf } from './session.js';
18
+ export { stateFor } from './state.js';
19
+ export { serveGraphQL } from './graphql.js';
20
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,EACL,gBAAgB,EAChB,aAAa,EACb,eAAe,GAEhB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EACL,OAAO,EACP,UAAU,GAGX,MAAM,WAAW,CAAC;AAEnB,OAAO,EACL,SAAS,EACT,SAAS,EACT,QAAQ,EACR,SAAS,EACT,aAAa,EACb,QAAQ,GAGT,MAAM,YAAY,CAAC;AAEpB,OAAO,EACL,YAAY,EACZ,SAAS,EACT,aAAa,GAGd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,aAAa,EAAoB,MAAM,cAAc,CAAC;AAE/D,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEtC,OAAO,EAAE,YAAY,EAAuB,MAAM,cAAc,CAAC"}
package/dist/rest.d.ts ADDED
@@ -0,0 +1,41 @@
1
+ import type { App } from '@fougere/core';
2
+ /** One row of the canonical table, in the form this door matches against. */
3
+ export interface Matchable {
4
+ method: string;
5
+ /** `route.path` split once: a literal segment, or `:name` to capture. */
6
+ segments: string[];
7
+ path: string;
8
+ entityName: string;
9
+ operationName: string;
10
+ }
11
+ export type RouteMatch = {
12
+ kind: 'match';
13
+ route: Matchable;
14
+ params: Record<string, string>;
15
+ }
16
+ /** The path is served, the verb is not — the answer that used to be a mutation. */
17
+ | {
18
+ kind: 'method-not-allowed';
19
+ allow: string[];
20
+ }
21
+ /** Not a Fougère path at all: the app's own `/api/*` handlers must still see it. */
22
+ | null;
23
+ /**
24
+ * The table, per frond.
25
+ *
26
+ * `generateRoutes` prefixes every path the same way, while this door addresses a frond by
27
+ * name (`/api/{frond}/{plural}`) — so it runs once per frond, each with its own prefix and
28
+ * a filter naming it. That frond loop is the only thing this file knows that `schema-rest`
29
+ * does not; the verbs, the paths and the membership rule all come from there.
30
+ */
31
+ export declare function tableOf(app: App): Matchable[];
32
+ /**
33
+ * Path first, method second — a router's order, and what makes a 405 possible at all.
34
+ *
35
+ * The order matters where the two overlap: `/posts/publish` and `/posts/:id` both accept
36
+ * `GET /posts/publish`. Taking the most specific path first means the answer is "that verb
37
+ * is refused here", not `findById('publish')` — and never `publish()`, which is what this
38
+ * door used to do with the caller's session cookie attached.
39
+ */
40
+ export declare function matchRoute(table: Matchable[], method: string, segments: string[]): RouteMatch;
41
+ //# sourceMappingURL=rest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest.d.ts","sourceRoot":"","sources":["../src/rest.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,GAAG,EAAE,MAAM,eAAe,CAAC;AAEzC,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,SAAS,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAAE;AACrE,mFAAmF;GACjF;IAAE,IAAI,EAAE,oBAAoB,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE;AACjD,oFAAoF;GAClF,IAAI,CAAC;AAMT;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,GAAG,GAAG,SAAS,EAAE,CAmB7C;AA+BD;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,UAAU,CAkB7F"}