@fougere/nuxt 0.1.0-alpha.0 → 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.
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +24 -1
- package/dist/module.js.map +1 -1
- package/package.json +15 -6
- package/src/runtime/composables/useCurrentUser.ts +1 -1
- package/src/runtime/composables/useFormFor.ts +1 -1
- package/src/runtime/composables/useFougereData.ts +41 -75
- package/src/runtime/plugins/session.server.ts +1 -1
- package/src/runtime/server/api/crud.ts +37 -74
- package/src/runtime/server/auth/middleware/auth.ts +1 -1
- package/src/runtime/server/auth/routes/auth/[...].ts +1 -1
- package/src/runtime/server/routes/call.post.ts +13 -37
- package/src/runtime/server/routes/session.get.ts +1 -1
- package/src/runtime/server/utils/boot.ts +12 -0
- package/src/runtime/server/utils/invoke.ts +7 -15
- package/src/runtime/form/fields.ts +0 -185
- package/src/runtime/server/utils/fougereApp.ts +0 -209
- package/src/runtime/server/utils/fougereAuth.ts +0 -20
- package/src/runtime/session/view.ts +0 -20
package/dist/module.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,EAAE,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,QAAA,MAAM,MAAM,
|
|
1
|
+
{"version":3,"file":"module.d.ts","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAK9D,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,EAAE,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAGD,QAAA,MAAM,MAAM,sFAoLV,CAAC;eAEY,MAAM;AAMrB,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,SAAS,EAAE,EAClB,cAAc,EAAE,MAAM,GACrB,MAAM,CA+DR"}
|
package/dist/module.js
CHANGED
|
@@ -72,6 +72,29 @@ const module = defineNuxtModule({
|
|
|
72
72
|
: [frond.source.path];
|
|
73
73
|
nuxt.options.watch.push(...watched);
|
|
74
74
|
}
|
|
75
|
+
// ── 1b-bis. Keep entity names through minification ──────────────────────
|
|
76
|
+
//
|
|
77
|
+
// Designation is class + verb: `useQuery(Post, 'list')` reads `Post.name`, and
|
|
78
|
+
// that name travels — it is the JSON-RPC method (`post.list`) and the REST path.
|
|
79
|
+
//
|
|
80
|
+
// Rollup cannot keep `class Post extends entity({…})` as a hoisted declaration
|
|
81
|
+
// (its heritage clause is a CALL), so it emits `var Post = class extends …`.
|
|
82
|
+
// The class is ANONYMOUS; the name comes from JavaScript's inference on the
|
|
83
|
+
// variable. `keep_classnames` therefore protects nothing — measured, in
|
|
84
|
+
// `compress` and in `mangle` both. Reserving the IDENTIFIER is what works.
|
|
85
|
+
//
|
|
86
|
+
// The list is exact rather than guessed: the scan just ran, so these are the
|
|
87
|
+
// classes themselves, read before any bundler touched them. `@fougere/vite`
|
|
88
|
+
// does the same for the hosts that have no module to do it for them.
|
|
89
|
+
const entityNames = fronds.flatMap((frond) => frond.entities.map((e) => e.entityClass.name));
|
|
90
|
+
if (entityNames.length > 0) {
|
|
91
|
+
const vite = (nuxt.options.vite ??= {});
|
|
92
|
+
const build = (vite.build ??= {});
|
|
93
|
+
build.minify = 'terser';
|
|
94
|
+
const terser = (build.terserOptions ??= {});
|
|
95
|
+
const mangle = (terser.mangle ??= {});
|
|
96
|
+
mangle.reserved = [...new Set([...(mangle.reserved ?? []), ...entityNames])];
|
|
97
|
+
}
|
|
75
98
|
// ── 1c. Register aliases for synced remote fronds (.fougere/remotes.json) ──
|
|
76
99
|
const remotesPath = resolve(rootDir, '.fougere', 'remotes.json');
|
|
77
100
|
if (existsSync(remotesPath)) {
|
|
@@ -145,7 +168,7 @@ const module = defineNuxtModule({
|
|
|
145
168
|
const bootTpl = addTemplate({
|
|
146
169
|
filename: 'fougere-boot.ts',
|
|
147
170
|
write: true,
|
|
148
|
-
getContents: () => generateBootPlugin(config, allSeeds, runtimeResolve('server/utils/
|
|
171
|
+
getContents: () => generateBootPlugin(config, allSeeds, runtimeResolve('server/utils/boot')),
|
|
149
172
|
});
|
|
150
173
|
addServerPlugin(bootTpl.dst);
|
|
151
174
|
},
|
package/dist/module.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,EACf,SAAS,EACT,WAAW,EACX,UAAU,EACV,cAAc,GACf,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,UAAU,EAAE,eAAe,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACvH,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAenD,MAAM,MAAM,GAAG,gBAAgB,CAAuB;IACpD,IAAI,EAAE;QACJ,IAAI,EAAE,eAAe;QACrB,SAAS,EAAE,SAAS;KACrB;IAED,QAAQ,EAAE;QACR,SAAS,EAAE,QAAQ;KACpB;IAED,KAAK,CAAC,KAAK,CAAC,OAA6B,EAAE,IAAU;QACnD,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,GAAG,cAAc,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;QACnE,MAAM,cAAc,GAAG,CAAC,GAAG,IAAc,EAAE,EAAE,CAC3C,aAAa,CAAC,gBAAgB,EAAE,GAAG,IAAI,CAAC,CAAC;QAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;QACrC,4FAA4F;QAC5F,MAAM,QAAQ,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC;QACzE,2EAA2E;QAC3E,8EAA8E;QAC9E,OAAO,CAAC,GAAG,CAAC,YAAY,GAAG,QAAQ,CAAC;QAEpC,mFAAmF;QACnF,IAAI,OAAO,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;YACnD,OAAO,CAAC,GAAG,CAAC,WAAW,KAAK,GAAG,CAAC;QAClC,CAAC;QAED,+DAA+D;QAC/D,8EAA8E;QAC9E,qDAAqD;QACrD,8DAA8D;QAC7D,IAAY,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,WAAgB,EAAE,EAAE;YACtD,WAAW,CAAC,YAAY,KAAK,EAAE,CAAC;YAChC,WAAW,CAAC,YAAY,CAAC,QAAQ,KAAK,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACrD,WAAW,CAAC,YAAY,CAAC,QAAQ,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YACvD,CAAC;YACD,yEAAyE;YACzE,WAAW,CAAC,YAAY,GAAG,EAAE,GAAG,WAAW,CAAC,YAAY,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QACjF,CAAC,CAAC,CAAC;QAEH,uEAAuE;QACvE,gFAAgF;QAChF,oFAAoF;QACpF,yCAAyC;QACzC,MAAM,IAAI,GAAG,UAAU,CAAC,OAAO,IAAI,CAAC,GAAG,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;QACxG,eAAe,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAqC,CAAC,CAAC;QAEzF,6EAA6E;QAC7E,kFAAkF;QAClF,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC/D,MAAM,eAAe,GAAG,MAAM,CAAC,WAAW,CACxC,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CACjC,CAAC;QAC5B,MAAM,MAAM,GAAkB,EAAE,EAAE,EAAE,QAAQ,EAAE,GAAG,UAAU,EAAE,GAAG,eAAe,EAAE,CAAC;QAElF,4DAA4D;QAC5D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QACjG,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,WAAW,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;QAE7D,qEAAqE;QACrE,8EAA8E;QAC9E,mFAAmF;QACnF,oFAAoF;QACpF,sEAAsE;QACtE,mFAAmF;QACnF,4EAA4E;QAC5E,gFAAgF;QAChF,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,KAAK,CAAC,IAAI,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC;YAC/D,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ;gBAC5C,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;gBACjD,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;YACxB,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC;QACtC,CAAC;QAED,8EAA8E;QAC9E,MAAM,WAAW,GAAG,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,cAAc,CAAC,CAAC;QACjE,IAAI,UAAU,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,WAAW,EAAE,OAAO,CAAC,CAAkD,CAAC;gBAChH,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;oBACnD,wCAAwC;oBACxC,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,CAAC;wBAC1C,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC;wBACjD,4DAA4D;wBAC5D,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;oBAC/C,CAAC;gBACH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,iCAAiC,CAAC,CAAC;QAC/C,CAAC;QAED,sDAAsD;QACtD,UAAU,CAAC;YACT,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,CAAC,4BAA4B,CAAC,EAAE;YACxE,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,CAAC,4BAA4B,CAAC,EAAE;YAC1E,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,cAAc,CAAC,wBAAwB,CAAC,EAAE;YACtE,EAAE,IAAI,EAAE,gBAAgB,EAAE,IAAI,EAAE,cAAc,CAAC,4BAA4B,CAAC,EAAE;SAC/E,CAAC,CAAC;QAEH,6EAA6E;QAC7E,gBAAgB,CAAC;YACf,KAAK,EAAE,gBAAgB;YACvB,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,cAAc,CAAC,yBAAyB,CAAC;SACnD,CAAC,CAAC;QACH,gFAAgF;QAChF,kFAAkF;QAClF,uCAAuC;QACvC,gBAAgB,CAAC;YACf,KAAK,EAAE,mBAAmB;YAC1B,MAAM,EAAE,MAAM;YACd,OAAO,EAAE,cAAc,CAAC,yBAAyB,CAAC;SACnD,CAAC,CAAC;QACH,gBAAgB,CAAC;YACf,KAAK,EAAE,mBAAmB;YAC1B,MAAM,EAAE,KAAK;YACb,OAAO,EAAE,cAAc,CAAC,2BAA2B,CAAC;SACrD,CAAC,CAAC;QACH,kEAAkE;QAClE,SAAS,CAAC,EAAE,GAAG,EAAE,cAAc,CAAC,wBAAwB,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC7E,gBAAgB,CAAC;YACf,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,cAAc,CAAC,iBAAiB,CAAC;SAC3C,CAAC,CAAC;QACH,mBAAmB,CAAC,cAAc,CAAC,cAAc,CAAC,CAAC,CAAC;QAEpD,kEAAkE;QAClE,IAAI,MAAM,CAAC,IAAI,EAAE,CAAC;YAChB,sDAAsD;YACtD,gBAAgB,CAAC;gBACf,UAAU,EAAE,IAAI;gBAChB,OAAO,EAAE,cAAc,CAAC,6BAA6B,CAAC;aACvD,CAAC,CAAC;YACH,yDAAyD;YACzD,gBAAgB,CAAC;gBACf,KAAK,EAAE,UAAU;gBACjB,OAAO,EAAE,cAAc,CAAC,+BAA+B,CAAC;aACzD,CAAC,CAAC;YACH,kCAAkC;YAClC,gBAAgB,CAAC;gBACf,KAAK,EAAE,SAAS;gBAChB,MAAM,EAAE,KAAK;gBACb,OAAO,EAAE,cAAc,CAAC,+BAA+B,CAAC;aACzD,CAAC,CAAC;QACL,CAAC;QAED,oDAAoD;QACpD,MAAM,QAAQ,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;QACpC,MAAM,OAAO,GAAG,WAAW,CAAC;YAC1B,QAAQ,EAAE,iBAAiB;YAC3B,KAAK,EAAE,IAAI;YACX,WAAW,EAAE,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,cAAc,CAAC,
|
|
1
|
+
{"version":3,"file":"module.js","sourceRoot":"","sources":["../src/module.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fougere/nuxt",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-alpha.0",
|
|
4
4
|
"description": "The Nuxt module: Fougere's client primitives and server surface.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"fougere",
|
|
7
|
+
"nuxt",
|
|
8
|
+
"nuxt-module",
|
|
9
|
+
"vue",
|
|
10
|
+
"typescript",
|
|
11
|
+
"fullstack",
|
|
12
|
+
"rpc"
|
|
13
|
+
],
|
|
5
14
|
"license": "MIT",
|
|
6
15
|
"repository": {
|
|
7
16
|
"type": "git",
|
|
@@ -24,20 +33,20 @@
|
|
|
24
33
|
"dependencies": {
|
|
25
34
|
"@nuxt/kit": "^4.5.1",
|
|
26
35
|
"jiti": "^2.6.1",
|
|
27
|
-
"@fougere/
|
|
28
|
-
"@fougere/runtime": "0.
|
|
29
|
-
"@fougere/
|
|
30
|
-
"@fougere/core": "0.1.0-alpha.0",
|
|
31
|
-
"@fougere/transport-http": "0.1.0-alpha.0"
|
|
36
|
+
"@fougere/core": "0.2.0-alpha.0",
|
|
37
|
+
"@fougere/runtime": "0.2.0-alpha.0",
|
|
38
|
+
"@fougere/app": "0.2.0-alpha.0"
|
|
32
39
|
},
|
|
33
40
|
"devDependencies": {
|
|
34
41
|
"@nuxt/schema": "^4.5.1",
|
|
42
|
+
"h3": "^1.15.11",
|
|
35
43
|
"nitropack": "^2.13.2",
|
|
36
44
|
"nuxt": "^4.5.1",
|
|
37
45
|
"vue": "^3.5.40",
|
|
38
46
|
"vitest": "^4.1.0"
|
|
39
47
|
},
|
|
40
48
|
"peerDependencies": {
|
|
49
|
+
"h3": "^1.15.0",
|
|
41
50
|
"nuxt": "^4.0.0",
|
|
42
51
|
"vue": "^3.5.0"
|
|
43
52
|
},
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { useState, useRequestFetch } from '#imports';
|
|
10
10
|
import { computed } from 'vue';
|
|
11
|
-
import type { SessionView } from '
|
|
11
|
+
import type { SessionView } from '@fougere/app/client';
|
|
12
12
|
|
|
13
13
|
export function useCurrentUser<TUser = Record<string, unknown>>() {
|
|
14
14
|
const session = useState<SessionView>('fougere:session', () => ({ user: null }));
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { reactive, computed } from 'vue';
|
|
12
12
|
import { FougereError, ErrorCode, toRegistrationName } from '@fougere/core/contract';
|
|
13
13
|
import { useCommand } from './useFougereData.js';
|
|
14
|
-
import { formFieldsOf, payloadOf, errorsByField, type FormEntity, type FormField } from '
|
|
14
|
+
import { formFieldsOf, payloadOf, errorsByField, type FormEntity, type FormField } from '@fougere/app/client';
|
|
15
15
|
|
|
16
16
|
export interface FormOptions {
|
|
17
17
|
/** Command the submit rides. Default: 'create'. */
|
|
@@ -1,52 +1,38 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The couple — useQuery (reads) and useCommand (writes), the two dual
|
|
3
|
-
*
|
|
4
|
-
* the imported entity class carries the metadata, its name carries the
|
|
5
|
-
* registration key.
|
|
2
|
+
* The couple — useQuery (reads) and useCommand (writes), the two dual gestures of
|
|
3
|
+
* a page talking to a Frond, in Vue.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* What the gestures ARE lives in `@fougere/app/client`: designation is class +
|
|
6
|
+
* verb, the key derives from designation plus input, and a successful command on
|
|
7
|
+
* an entity revalidates every mounted query on that entity. What lives here is
|
|
8
|
+
* Vue — `useAsyncData` for the read, refs for the pending flags, `onScopeDispose`
|
|
9
|
+
* for unregistration. The React adapter states the same three facts against its
|
|
10
|
+
* own primitives, and neither restates the rules.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Both gestures ride the call envelope: the browser POSTs JSON-RPC to
|
|
13
|
+
* /_fougere/call; during SSR Nuxt collapses the same call to an in-process fetch
|
|
14
|
+
* (no network, no port).
|
|
14
15
|
*/
|
|
15
16
|
import { useAsyncData, useRequestFetch, refreshNuxtData } from '#imports';
|
|
16
17
|
import { ref, computed, toValue, onScopeDispose, type MaybeRefOrGetter, type Ref } from 'vue';
|
|
18
|
+
import type { FougereError } from '@fougere/core/contract';
|
|
17
19
|
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
20
|
+
callOf,
|
|
21
|
+
entityKeyOf,
|
|
22
|
+
invocationOf,
|
|
23
|
+
queryKeyOf,
|
|
24
|
+
sendCall,
|
|
25
|
+
trackQuery,
|
|
26
|
+
mountedKeys,
|
|
27
|
+
itemsOf,
|
|
28
|
+
pageOf,
|
|
29
|
+
asFougereError,
|
|
30
|
+
type CallInput,
|
|
31
|
+
type EntityClass,
|
|
32
|
+
type Fetcher,
|
|
33
|
+
} from '@fougere/app/client';
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
export type CallInput = Partial<Pick<InvocationContext, 'params' | 'query' | 'body'>>;
|
|
31
|
-
|
|
32
|
-
let nextId = 1;
|
|
33
|
-
|
|
34
|
-
/** Mounted queries per entity — the command side of the link reads this. */
|
|
35
|
-
const mounted = new Map<string, Set<string>>();
|
|
36
|
-
|
|
37
|
-
function invocationOf(input?: CallInput): InvocationContext {
|
|
38
|
-
return { params: {}, query: {}, body: undefined, state: {}, ...input };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
type Fetcher = <T>(url: string, options: { method: 'POST'; body: unknown }) => Promise<T>;
|
|
42
|
-
|
|
43
|
-
async function send(fetcher: Fetcher, call: FrondCall, invocation: InvocationContext): Promise<unknown> {
|
|
44
|
-
const response = await fetcher<RpcResponse>('/_fougere/call', {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
body: frameCall(call, invocation, nextId++),
|
|
47
|
-
});
|
|
48
|
-
return unframeResponse(response, call);
|
|
49
|
-
}
|
|
35
|
+
export type { CallInput };
|
|
50
36
|
|
|
51
37
|
export async function useQuery<T = Record<string, unknown>>(
|
|
52
38
|
entity: EntityClass,
|
|
@@ -54,46 +40,35 @@ export async function useQuery<T = Record<string, unknown>>(
|
|
|
54
40
|
input?: MaybeRefOrGetter<CallInput | undefined>,
|
|
55
41
|
opts?: { immediate?: boolean },
|
|
56
42
|
) {
|
|
57
|
-
const entityKey =
|
|
58
|
-
const call
|
|
59
|
-
const key =
|
|
43
|
+
const entityKey = entityKeyOf(entity);
|
|
44
|
+
const call = callOf(entity, op);
|
|
45
|
+
const key = queryKeyOf(entityKey, op, toValue(input));
|
|
60
46
|
const fetcher = useRequestFetch() as Fetcher;
|
|
61
47
|
|
|
62
48
|
// Register before any await — the link and scope cleanup need the setup scope.
|
|
63
49
|
if (import.meta.client) {
|
|
64
|
-
|
|
65
|
-
keys.add(key);
|
|
66
|
-
mounted.set(entityKey, keys);
|
|
67
|
-
onScopeDispose(() => keys.delete(key));
|
|
50
|
+
onScopeDispose(trackQuery(entityKey, key));
|
|
68
51
|
}
|
|
69
52
|
|
|
70
53
|
const { data, pending, error, refresh } = await useAsyncData(
|
|
71
54
|
key,
|
|
72
|
-
() =>
|
|
55
|
+
() => sendCall(fetcher, call, invocationOf(toValue(input))),
|
|
73
56
|
{
|
|
74
57
|
...(input === undefined ? {} : { watch: [() => toValue(input)] }),
|
|
75
58
|
...(opts?.immediate === false ? { immediate: false } : {}),
|
|
76
59
|
},
|
|
77
60
|
);
|
|
78
61
|
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
if (Array.isArray(v)) return v as T[];
|
|
83
|
-
if (v && typeof v === 'object' && Array.isArray((v as { items?: unknown }).items)) {
|
|
84
|
-
return (v as { items: T[] }).items;
|
|
85
|
-
}
|
|
86
|
-
return [];
|
|
87
|
-
});
|
|
88
|
-
const total = computed(() => (data.value as { total?: number } | null)?.total);
|
|
89
|
-
const hasMore = computed(() => (data.value as { hasMore?: boolean } | null)?.hasMore);
|
|
62
|
+
const items = computed<T[]>(() => itemsOf<T>(data.value));
|
|
63
|
+
const total = computed(() => pageOf(data.value).total);
|
|
64
|
+
const hasMore = computed(() => pageOf(data.value).hasMore);
|
|
90
65
|
|
|
91
66
|
return { data: data as Ref<T | null>, items, total, hasMore, loading: pending, error, refresh };
|
|
92
67
|
}
|
|
93
68
|
|
|
94
69
|
export function useCommand<T = unknown>(entity: EntityClass, op: string) {
|
|
95
|
-
const entityKey =
|
|
96
|
-
const call
|
|
70
|
+
const entityKey = entityKeyOf(entity);
|
|
71
|
+
const call = callOf(entity, op);
|
|
97
72
|
const fetcher = useRequestFetch() as Fetcher;
|
|
98
73
|
const loading = ref(false);
|
|
99
74
|
const error = ref<FougereError | null>(null);
|
|
@@ -102,22 +77,13 @@ export function useCommand<T = unknown>(entity: EntityClass, op: string) {
|
|
|
102
77
|
loading.value = true;
|
|
103
78
|
error.value = null;
|
|
104
79
|
try {
|
|
105
|
-
const result = (await
|
|
80
|
+
const result = (await sendCall(fetcher, call, invocationOf(input))) as T;
|
|
106
81
|
// The link: same entity designated on both sides → revalidate its queries.
|
|
107
|
-
const keys =
|
|
108
|
-
if (keys
|
|
82
|
+
const keys = mountedKeys(entityKey);
|
|
83
|
+
if (keys.length) await refreshNuxtData(keys);
|
|
109
84
|
return result;
|
|
110
85
|
} catch (err) {
|
|
111
|
-
error.value =
|
|
112
|
-
err instanceof FougereError
|
|
113
|
-
? err
|
|
114
|
-
: new FougereError({
|
|
115
|
-
code: ErrorCode.SERVICE_UNAVAILABLE,
|
|
116
|
-
message: (err as Error)?.message ?? String(err),
|
|
117
|
-
entity: entityKey,
|
|
118
|
-
operation: op,
|
|
119
|
-
cause: err,
|
|
120
|
-
});
|
|
86
|
+
error.value = asFougereError(err, entityKey, op);
|
|
121
87
|
throw error.value;
|
|
122
88
|
} finally {
|
|
123
89
|
loading.value = false;
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* page arrives already knowing its user — no round-trip.
|
|
6
6
|
*/
|
|
7
7
|
import { defineNuxtPlugin, useRequestEvent, useState } from '#imports';
|
|
8
|
-
import { sessionViewOf, type SessionView } from '
|
|
8
|
+
import { sessionViewOf, type SessionView } from '@fougere/app/client';
|
|
9
9
|
|
|
10
10
|
export default defineNuxtPlugin(() => {
|
|
11
11
|
const event = useRequestEvent();
|
|
@@ -1,86 +1,49 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* REST catch-all —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* REST catch-all — the h3 half of a door whose decisions live in `@fougere/app`.
|
|
3
|
+
*
|
|
4
|
+
* It used to derive its own answers: its own `pluralize`, its own "segment = op or
|
|
5
|
+
* id", and its own method rule — which was no rule at all. A known operation name
|
|
6
|
+
* won whatever the verb, so `GET /api/blog/posts/delete?id=…` reached the mutating
|
|
7
|
+
* method with the session cookie a browser attaches to any navigation, and a
|
|
8
|
+
* body-less `DELETE /api/blog/posts` was read as `create`.
|
|
9
|
+
*
|
|
10
|
+
* `schema-rest` already answers all three in one place, `exposed === false`
|
|
11
|
+
* included — so the fix was not a guard added here, it was the removal of the
|
|
12
|
+
* second answer. `serveRest` is now the third: the same decision serves Next, and
|
|
13
|
+
* neither adapter restates it.
|
|
14
|
+
*
|
|
15
|
+
* What stays here is translation: read the h3 event, write the h3 answer. The state
|
|
16
|
+
* is stamped from `event.context` — what the server resolved, never the wire.
|
|
5
17
|
*/
|
|
6
|
-
import { defineEventHandler, readBody, getQuery, createError } from 'h3';
|
|
7
|
-
import {
|
|
8
|
-
import { useFougereApp } from '../utils/fougereApp';
|
|
9
|
-
import { invoke } from '../utils/invoke';
|
|
10
|
-
|
|
11
|
-
function pluralize(name: string): string {
|
|
12
|
-
return name.endsWith('y') ? name.slice(0, -1) + 'ies' : name + 's';
|
|
13
|
-
}
|
|
18
|
+
import { defineEventHandler, readBody, getQuery, createError, setResponseStatus, setResponseHeaders } from 'h3';
|
|
19
|
+
import { serveRest, useFougereApp } from '@fougere/app';
|
|
14
20
|
|
|
15
21
|
export default defineEventHandler(async (event) => {
|
|
16
22
|
const app = await useFougereApp();
|
|
17
23
|
|
|
18
|
-
// ── parse: /api/{frondName}/{plural}[/{idOrOp}] → FrondCall + invocation
|
|
19
|
-
const path = event.path.replace(/^\/api\//, '').replace(/\?.*$/, '');
|
|
20
|
-
const segments = path.split('/').filter(Boolean);
|
|
21
|
-
if (segments.length < 2) return;
|
|
22
|
-
|
|
23
|
-
const frond = app.fronds.find((f) => f.name === segments[0]);
|
|
24
|
-
if (!frond) return;
|
|
25
|
-
const entity = frond.entities.find((e) => pluralize(e.name) === segments[1]);
|
|
26
|
-
if (!entity) return;
|
|
27
|
-
|
|
28
|
-
// The default handler, explicitly: this catch-all IS the default surface, and
|
|
29
|
-
// the call below lands on the default façade. Without `!h.surface` the op list
|
|
30
|
-
// that arbitrates "segment = op or id" could come from a surface handler while
|
|
31
|
-
// the call went somewhere else — two answers for one route.
|
|
32
|
-
const ops = [
|
|
33
|
-
...(frond.handlers.find((h) => h.address === entity.name && !h.surface)?.operations.keys() ?? []),
|
|
34
|
-
];
|
|
35
|
-
|
|
36
24
|
const method = event.method.toUpperCase();
|
|
37
|
-
const extra: string | undefined = segments[2];
|
|
38
|
-
const params: Record<string, string> = {};
|
|
39
|
-
let op: string;
|
|
40
|
-
|
|
41
|
-
if (!extra) {
|
|
42
|
-
op = method === 'GET' ? 'list' : 'create';
|
|
43
|
-
} else {
|
|
44
|
-
// /{idOrOp}: kebab-case → camelCase, known operation wins, else it's an id
|
|
45
|
-
const asOp = extra.replace(/-([a-z])/g, (_: string, c: string) => c.toUpperCase());
|
|
46
|
-
if (ops.includes(asOp)) {
|
|
47
|
-
op = asOp;
|
|
48
|
-
} else {
|
|
49
|
-
params.id = extra;
|
|
50
|
-
if (method === 'GET') op = 'findById';
|
|
51
|
-
else if (method === 'PUT' || method === 'PATCH') op = 'update';
|
|
52
|
-
else if (method === 'DELETE') op = 'delete';
|
|
53
|
-
else throw createError({ statusCode: 405, message: 'Method not allowed' });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const query = getQuery(event) as Record<string, string>;
|
|
58
25
|
const hasBody = method === 'POST' || method === 'PUT' || method === 'PATCH';
|
|
59
|
-
const body = hasBody ? await readBody(event) : undefined;
|
|
60
|
-
|
|
61
|
-
// ── invoke: the runner routes it — local façade or remote doublure
|
|
62
|
-
let result: unknown;
|
|
63
|
-
try {
|
|
64
|
-
result = await invoke({ entity: entity.name, op }, { params, query, body });
|
|
65
|
-
} catch (err) {
|
|
66
|
-
const { status, body: payload } = toHttpError(err);
|
|
67
|
-
throw createError({ statusCode: status, message: payload.message, data: payload });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ── format
|
|
71
|
-
if (result === null) {
|
|
72
|
-
throw createError({ statusCode: 404, message: 'Not found' });
|
|
73
|
-
}
|
|
74
26
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
27
|
+
const outcome = await serveRest(app, {
|
|
28
|
+
method,
|
|
29
|
+
path: event.path.replace(/^\/api\//, '').replace(/\?.*$/, ''),
|
|
30
|
+
query: getQuery(event) as Record<string, string>,
|
|
31
|
+
body: hasBody ? await readBody(event) : undefined,
|
|
32
|
+
state: (event.context ?? {}) as Record<string, unknown>,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Nothing here is ours — an app's own /api/* routes must still reach their handler.
|
|
36
|
+
if (outcome.kind === 'pass') return;
|
|
37
|
+
|
|
38
|
+
if (outcome.kind === 'error') {
|
|
39
|
+
if (outcome.headers) setResponseHeaders(event, outcome.headers);
|
|
40
|
+
throw createError({
|
|
41
|
+
statusCode: outcome.status,
|
|
42
|
+
message: outcome.body.message,
|
|
43
|
+
data: outcome.body,
|
|
44
|
+
});
|
|
83
45
|
}
|
|
84
46
|
|
|
85
|
-
|
|
47
|
+
setResponseStatus(event, outcome.status);
|
|
48
|
+
return outcome.body;
|
|
86
49
|
});
|
|
@@ -1,17 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Receiving end for the browser —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Receiving end for the browser — the h3 half. The decision (which audience, which
|
|
3
|
+
* runner, what a parse failure answers) lives in `@fougere/app`; what stays here is
|
|
4
|
+
* getting a JSON payload out of an h3 event, which is the one thing that genuinely
|
|
5
|
+
* differs between hosts. Next reads `await request.json()` and needs none of this.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Trust boundary, unchanged: the browser sits outside the topology, so `state` is
|
|
8
|
+
* stamped from the server-resolved session (event.context), never taken from the wire.
|
|
9
9
|
*/
|
|
10
10
|
import { defineEventHandler } from 'h3';
|
|
11
|
-
import {
|
|
12
|
-
import { createAppRunner } from '@fougere/core';
|
|
13
|
-
import type { Transport } from '@fougere/core';
|
|
14
|
-
import { useFougereApp } from '../utils/fougereApp';
|
|
11
|
+
import { serveRpc, rpcParseError, useFougereApp } from '@fougere/app';
|
|
15
12
|
|
|
16
13
|
type NodeReq = {
|
|
17
14
|
body?: unknown;
|
|
@@ -122,37 +119,16 @@ async function readJsonBody(event: { req?: unknown; node?: { req?: unknown } }):
|
|
|
122
119
|
return {};
|
|
123
120
|
}
|
|
124
121
|
|
|
125
|
-
/**
|
|
126
|
-
* The audience this door serves — the path segment after `/_fougere/call`.
|
|
127
|
-
*
|
|
128
|
-
* The envelope is a surface like REST and GraphQL, so it selects its audience like they
|
|
129
|
-
* do; the difference is only that it takes it from the path instead of an option, because
|
|
130
|
-
* a door is mounted, not called. The same word names the directory
|
|
131
|
-
* (`handlers/public/`), the config key (`surfaces: { public: [...] }`) and this segment —
|
|
132
|
-
* derived, never configured.
|
|
133
|
-
*
|
|
134
|
-
* No escalation to guard: a named surface serves the entities it names and nothing else
|
|
135
|
-
* (closed by naming), so every one of them is a subset of what the bare path already
|
|
136
|
-
* serves.
|
|
137
|
-
*/
|
|
138
|
-
function surfaceOf(path: string): string | undefined {
|
|
139
|
-
const named = /^\/_fougere\/call\/([A-Za-z0-9_-]+)/.exec(path.replace(/\?.*$/, ''));
|
|
140
|
-
return named?.[1];
|
|
141
|
-
}
|
|
142
|
-
|
|
143
122
|
export default defineEventHandler(async (event) => {
|
|
144
123
|
const app = await useFougereApp();
|
|
145
|
-
const runner = createAppRunner(app, surfaceOf(event.path));
|
|
146
|
-
const stamped: Transport = (call, invocation) =>
|
|
147
|
-
runner(call, { ...invocation, state: (event.context ?? {}) as Record<string, unknown> });
|
|
148
124
|
try {
|
|
149
|
-
return
|
|
125
|
+
return await serveRpc(app, {
|
|
126
|
+
path: event.path,
|
|
127
|
+
body: await readJsonBody(event),
|
|
128
|
+
state: (event.context ?? {}) as Record<string, unknown>,
|
|
129
|
+
});
|
|
150
130
|
} catch (err) {
|
|
151
131
|
if ((err as { statusCode?: number })?.statusCode === 413) throw err;
|
|
152
|
-
return
|
|
153
|
-
jsonrpc: '2.0' as const,
|
|
154
|
-
id: null,
|
|
155
|
-
error: { code: PARSE_ERROR, message: 'Parse error' },
|
|
156
|
-
};
|
|
132
|
+
return rpcParseError();
|
|
157
133
|
}
|
|
158
134
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* family as /_fougere/call.
|
|
5
5
|
*/
|
|
6
6
|
import { defineEventHandler } from 'h3';
|
|
7
|
-
import { sessionViewOf } from '
|
|
7
|
+
import { sessionViewOf } from '@fougere/app';
|
|
8
8
|
|
|
9
9
|
export default defineEventHandler((event) =>
|
|
10
10
|
sessionViewOf((event.context ?? {}) as Record<string, unknown>),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The boot, re-exported under a path inside this package.
|
|
3
|
+
*
|
|
4
|
+
* Every runtime file here reaches `@fougere/app` by name, because it is resolved
|
|
5
|
+
* from this package's own node_modules. The generated Nitro plugin cannot: it is
|
|
6
|
+
* written into the user's `.nuxt/`, so a bare `@fougere/app` would be resolved from
|
|
7
|
+
* the USER's project, where a transitive dependency is not visible under pnpm.
|
|
8
|
+
*
|
|
9
|
+
* So the codegen imports this file by absolute path, and this file — which does sit
|
|
10
|
+
* in `@fougere/nuxt` — does the resolving. One indirection, one reason.
|
|
11
|
+
*/
|
|
12
|
+
export { configureFougere, useFougereApp, type FougereServerConfig } from '@fougere/app';
|
|
@@ -1,20 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The server dual of the couple — same designation (class + verb), same
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* The server dual of the couple — same designation (class + verb), same return,
|
|
3
|
+
* same errors. `invokeOn` places the call (local façade → direct in-memory
|
|
4
|
+
* execution, a frond in `remotes` → JSON-RPC on the wire); what this file owns is
|
|
5
|
+
* the one host-specific part: finding the current request.
|
|
6
6
|
*
|
|
7
7
|
* In request context the current session rides along (event.context →
|
|
8
8
|
* invocation.state); outside a request, state is empty or explicit.
|
|
9
9
|
*/
|
|
10
10
|
import { useEvent } from 'nitropack/runtime';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
callValueOf,
|
|
14
|
-
type FrondCall,
|
|
15
|
-
type InvocationContext,
|
|
16
|
-
} from '@fougere/core';
|
|
17
|
-
import { useFougereApp } from './fougereApp';
|
|
11
|
+
import { invokeOn, useFougereApp } from '@fougere/app';
|
|
12
|
+
import type { FrondCall, InvocationContext } from '@fougere/core';
|
|
18
13
|
|
|
19
14
|
type EntityClass = { name: string };
|
|
20
15
|
type CallInput = Partial<InvocationContext>;
|
|
@@ -26,11 +21,8 @@ export async function invoke<T = unknown>(
|
|
|
26
21
|
opOrInput?: string | CallInput,
|
|
27
22
|
input?: CallInput,
|
|
28
23
|
): Promise<T> {
|
|
29
|
-
const given = typeof opOrInput === 'string' ? input : opOrInput;
|
|
30
|
-
const { call, invocation } = callValueOf(target, opOrInput, input);
|
|
31
|
-
const state = given?.state ?? requestState();
|
|
32
24
|
const app = await useFougereApp();
|
|
33
|
-
return (
|
|
25
|
+
return invokeOn<T>(app, target, opOrInput, input, requestState());
|
|
34
26
|
}
|
|
35
27
|
|
|
36
28
|
function requestState(): Record<string, unknown> {
|
|
@@ -1,185 +0,0 @@
|
|
|
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
|
-
import type { ValidationError } from '@fougere/schema';
|
|
8
|
-
|
|
9
|
-
/** What an entity class exposes to a form — the schema statics it already has. */
|
|
10
|
-
export interface FormEntity {
|
|
11
|
-
name: string;
|
|
12
|
-
getFields(): Record<string, FieldLike>;
|
|
13
|
-
validate(input: unknown): { success: true; data: unknown } | { success: false; errors: ValidationError[] };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
interface FieldLike {
|
|
17
|
-
shape?: {
|
|
18
|
-
type?: unknown;
|
|
19
|
-
enum?: readonly unknown[];
|
|
20
|
-
format?: string;
|
|
21
|
-
properties?: unknown;
|
|
22
|
-
minLength?: number;
|
|
23
|
-
maxLength?: number;
|
|
24
|
-
minimum?: number;
|
|
25
|
-
maximum?: number;
|
|
26
|
-
pattern?: string;
|
|
27
|
-
};
|
|
28
|
-
lifecycle?: { create?: unknown };
|
|
29
|
-
role?: { primary?: boolean; relation?: { kind: string } };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* The literal a field is born with, when it declares one.
|
|
34
|
-
*
|
|
35
|
-
* `text({ default: 'x' })` and `oneOf('a', 'b', { default: 'a' })` both compile to
|
|
36
|
-
* `lifecycle.create = { value }` — the create rule that answers the field's absence.
|
|
37
|
-
* The other create rules ('now', { generate }, 'optional') name no literal: their value
|
|
38
|
-
* is decided at write time, so a form has nothing to show for them.
|
|
39
|
-
*/
|
|
40
|
-
function defaultOf(field: FieldLike): unknown {
|
|
41
|
-
const create = field.lifecycle?.create;
|
|
42
|
-
return create !== null && typeof create === 'object' && 'value' in create
|
|
43
|
-
? (create as { value: unknown }).value
|
|
44
|
-
: undefined;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface FormField {
|
|
48
|
-
name: string;
|
|
49
|
-
/** Rendering hint derived from the shape — the page maps it to widgets. */
|
|
50
|
-
control: 'text' | 'email' | 'url' | 'number' | 'boolean' | 'date' | 'select';
|
|
51
|
-
required: boolean;
|
|
52
|
-
/** i18n key by convention: `entity.field`. The schema never carries display text. */
|
|
53
|
-
labelKey: string;
|
|
54
|
-
/** Fallback label when no i18n message fills the key. */
|
|
55
|
-
label: string;
|
|
56
|
-
/** Enum values, when control is 'select'. */
|
|
57
|
-
options?: string[];
|
|
58
|
-
/**
|
|
59
|
-
* What the browser enforces, under the names it already knows — spread this on the
|
|
60
|
-
* input and the page states no rule of its own.
|
|
61
|
-
*
|
|
62
|
-
* The shape holds `minLength`/`maximum`/`pattern`; a browser holds `minlength`/
|
|
63
|
-
* `max`/`pattern` and enforces them with no JavaScript at all. Carrying them here
|
|
64
|
-
* is a projection, not a second rule: the judge reads the same shape, and a form
|
|
65
|
-
* that ignores these still gets the same verdict — it just gets it later, and a
|
|
66
|
-
* screen reader never gets it at all.
|
|
67
|
-
*
|
|
68
|
-
* `type` is part of the contract, not decoration: `email` and `url` are formats the
|
|
69
|
-
* shape states and the browser checks live, per field, as one types. A page writing
|
|
70
|
-
* `type="email"` by hand is spelling a second time what the card already said.
|
|
71
|
-
*
|
|
72
|
-
* Three deliberate absences, each one a place where the attribute would mean
|
|
73
|
-
* something the shape does not say:
|
|
74
|
-
* - a `date` field gets no `type` — neither `date` nor `datetime-local` produces the
|
|
75
|
-
* RFC 3339 string a `date-time` shape judges, so the browser would accept what the
|
|
76
|
-
* judge refuses;
|
|
77
|
-
* - `select` and `boolean` are not inputs — the page picks the widget, `control` says
|
|
78
|
-
* which;
|
|
79
|
-
* - a required `boolean` gets no `required` — on a checkbox that attribute means
|
|
80
|
-
* "must be CHECKED", where the shape only says the value must be supplied.
|
|
81
|
-
*/
|
|
82
|
-
attrs?: {
|
|
83
|
-
type?: 'text' | 'email' | 'url' | 'number';
|
|
84
|
-
required?: boolean;
|
|
85
|
-
minlength?: number;
|
|
86
|
-
maxlength?: number;
|
|
87
|
-
min?: number;
|
|
88
|
-
max?: number;
|
|
89
|
-
pattern?: string;
|
|
90
|
-
};
|
|
91
|
-
/**
|
|
92
|
-
* The value the field is born with — the literal its `lifecycle.create` rule names.
|
|
93
|
-
* Present so the form can SHOW what is about to be written; the storage realizes it
|
|
94
|
-
* either way, so a form that ignores this still produces the same row.
|
|
95
|
-
*/
|
|
96
|
-
default?: unknown;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The base JSON type of a shape — unwraps the `[T,'null']` union. */
|
|
100
|
-
function baseType(type: unknown): string {
|
|
101
|
-
if (Array.isArray(type)) return (type.find((t) => t !== 'null') as string) ?? 'string';
|
|
102
|
-
return (type as string) ?? 'string';
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** The formats a browser has an input type for — the rest stay `text`, judged later. */
|
|
106
|
-
const CONTROL_BY_FORMAT: Record<string, FormField['control']> = {
|
|
107
|
-
'date-time': 'date',
|
|
108
|
-
email: 'email',
|
|
109
|
-
uri: 'url',
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
function controlOf(field: FieldLike): FormField['control'] {
|
|
113
|
-
const shape = field.shape ?? {};
|
|
114
|
-
if (Array.isArray(shape.enum) && shape.enum.length) return 'select';
|
|
115
|
-
const base = baseType(shape.type);
|
|
116
|
-
if (base === 'number' || base === 'integer') return 'number';
|
|
117
|
-
if (base === 'boolean') return 'boolean';
|
|
118
|
-
if (base === 'string' && shape.format) return CONTROL_BY_FORMAT[shape.format] ?? 'text';
|
|
119
|
-
return 'text';
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Controls that ARE an `<input type>` — see the two absences on {@link FormField.attrs}. */
|
|
123
|
-
const INPUT_TYPES = new Set(['text', 'email', 'url', 'number']);
|
|
124
|
-
|
|
125
|
-
/** The shape's bounds, under the names a browser already enforces. */
|
|
126
|
-
function attrsOf(field: FieldLike, control: FormField['control'], required: boolean): NonNullable<FormField['attrs']> {
|
|
127
|
-
const s = field.shape ?? {};
|
|
128
|
-
const attrs = {
|
|
129
|
-
type: INPUT_TYPES.has(control) ? control : undefined,
|
|
130
|
-
required: (required && control !== 'boolean') || undefined,
|
|
131
|
-
minlength: s.minLength,
|
|
132
|
-
maxlength: s.maxLength,
|
|
133
|
-
min: s.minimum,
|
|
134
|
-
max: s.maximum,
|
|
135
|
-
pattern: s.pattern,
|
|
136
|
-
};
|
|
137
|
-
return Object.fromEntries(Object.entries(attrs).filter(([, v]) => v !== undefined));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* The fields a create form is made of: membership from the io projection
|
|
142
|
-
* (`inputFields` — what a client may supply), requiredness from the
|
|
143
|
-
* lifecycle axis (any create rule makes absence legal).
|
|
144
|
-
*/
|
|
145
|
-
export function formFieldsOf(entity: FormEntity, entityKey: string): FormField[] {
|
|
146
|
-
return Object.entries(inputFields(entity.getFields() as never)).map(([name, field]) => {
|
|
147
|
-
const f = field as FieldLike;
|
|
148
|
-
const control = controlOf(f);
|
|
149
|
-
const required = f.lifecycle?.create === undefined;
|
|
150
|
-
const attrs = attrsOf(f, control, required);
|
|
151
|
-
return {
|
|
152
|
-
name,
|
|
153
|
-
control,
|
|
154
|
-
required,
|
|
155
|
-
labelKey: `${entityKey}.${name}`,
|
|
156
|
-
label: name.charAt(0).toUpperCase() + name.slice(1),
|
|
157
|
-
...(Array.isArray(f.shape?.enum)
|
|
158
|
-
? { options: f.shape.enum.filter((value): value is string => typeof value === 'string') }
|
|
159
|
-
: {}),
|
|
160
|
-
...(Object.keys(attrs).length ? { attrs } : {}),
|
|
161
|
-
...(defaultOf(f) !== undefined ? { default: defaultOf(f) } : {}),
|
|
162
|
-
};
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* The wire body of the form's values — an empty control is an absent value
|
|
168
|
-
* at the create boundary (absence is judged by the lifecycle axis, an empty
|
|
169
|
-
* string would be judged as a present bad value).
|
|
170
|
-
*/
|
|
171
|
-
export function payloadOf(values: Record<string, unknown>): Record<string, unknown> {
|
|
172
|
-
return Object.fromEntries(
|
|
173
|
-
Object.entries(values).filter(([, v]) => v !== undefined && v !== ''),
|
|
174
|
-
);
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** Index judge errors by field — local judge and remote judge share this shape. */
|
|
178
|
-
export function errorsByField(errors: ValidationError[]): Record<string, string> {
|
|
179
|
-
const byField: Record<string, string> = {};
|
|
180
|
-
for (const err of errors) {
|
|
181
|
-
const field = err.path.split('.')[0] || err.path;
|
|
182
|
-
byField[field] ??= err.message;
|
|
183
|
-
}
|
|
184
|
-
return byField;
|
|
185
|
-
}
|
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fougere server bootstrap — single entry point for app lifecycle in Nuxt.
|
|
3
|
-
*
|
|
4
|
-
* Default path (zero-config): fougere.config.ts declares `db: 'sqlite'` and
|
|
5
|
-
* everything else; this module auto-resolves the storage handle, builds an
|
|
6
|
-
* ormFactory, runs auto-DDL from the entities, and boots the app.
|
|
7
|
-
*
|
|
8
|
-
* Escape hatch: `configureFougere({ db, ormFactory })` can be called from a
|
|
9
|
-
* Nitro plugin if the user wants a custom data layer (alternative driver,
|
|
10
|
-
* managed migrations, etc.).
|
|
11
|
-
*/
|
|
12
|
-
import { createApp, loadCascadedConfig, setModuleLoader, frondAliases, Logger } from '@fougere/core';
|
|
13
|
-
import { createContainer } from '@fougere/container-fougere';
|
|
14
|
-
import type { App, EntityOrm, FougereConfig, Transport } from '@fougere/core';
|
|
15
|
-
import { applyCreate, applyUpdate, type SchemaLike } from '@fougere/schema';
|
|
16
|
-
|
|
17
|
-
// ── Public types ─────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export interface FougereServerConfig {
|
|
20
|
-
/** Storage handle. Forwarded to the auth provider via AuthContext.db. */
|
|
21
|
-
db?: unknown;
|
|
22
|
-
/** Per-entity ORM factory. */
|
|
23
|
-
ormFactory?: (entity: SchemaLike, name: string) => EntityOrm;
|
|
24
|
-
/** Called after app is created. Use for migrations, seeding, etc. */
|
|
25
|
-
afterBoot?: (app: App) => void | Promise<void>;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// ── State ────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
let _config: FougereServerConfig = {};
|
|
31
|
-
let _appPromise: Promise<App> | null = null;
|
|
32
|
-
|
|
33
|
-
// ── Public API ───────────────────────────────────
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Override the data layer — only needed if you don't want the convention-driven
|
|
37
|
-
* setup based on `config.db` in fougere.config.ts.
|
|
38
|
-
*/
|
|
39
|
-
export function configureFougere(config: FougereServerConfig) {
|
|
40
|
-
_config = config;
|
|
41
|
-
_appPromise = null;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Get the booted Fougere app. Lazy — boots on first call, then caches. */
|
|
45
|
-
export function useFougereApp(): Promise<App> {
|
|
46
|
-
if (!_appPromise) {
|
|
47
|
-
_appPromise = boot();
|
|
48
|
-
}
|
|
49
|
-
return _appPromise;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// ── Boot ─────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
async function boot(): Promise<App> {
|
|
55
|
-
const bootStart = performance.now();
|
|
56
|
-
const log = new Logger('boot', { level: 'debug' });
|
|
57
|
-
|
|
58
|
-
log.info('booting (Nuxt/Nitro)');
|
|
59
|
-
|
|
60
|
-
const { createJiti } = await import('jiti');
|
|
61
|
-
// Nitro serves from a bundle, but the scan still reads frond sources from disk — so the
|
|
62
|
-
// named form a frond uses for its neighbour has to resolve here too.
|
|
63
|
-
const jiti = createJiti(import.meta.url, {
|
|
64
|
-
interopDefault: true,
|
|
65
|
-
alias: await frondAliases(process.env.FOUGERE_ROOT ?? process.cwd()),
|
|
66
|
-
});
|
|
67
|
-
setModuleLoader((filePath) => jiti.import(filePath) as Promise<Record<string, unknown>>);
|
|
68
|
-
|
|
69
|
-
// Config cascades along the workspace→app frontier: the workspace root (via
|
|
70
|
-
// FOUGERE_ROOT, where `remotes`/shared db live) is the base, the app (cwd)
|
|
71
|
-
// overrides. Same boundary the fronds cascade along. No `root` → both equal.
|
|
72
|
-
const configRoot = process.cwd();
|
|
73
|
-
const root = process.env.FOUGERE_ROOT ?? configRoot;
|
|
74
|
-
const fileConfig: FougereConfig = await loadCascadedConfig(root, configRoot);
|
|
75
|
-
|
|
76
|
-
// Auto-resolve the data layer from config.db when the user didn't provide a
|
|
77
|
-
// custom one via configureFougere. The resolution itself lives in @fougere/runtime
|
|
78
|
-
// — this host must not know which storage package backs `db:`.
|
|
79
|
-
let db = _config.db;
|
|
80
|
-
let ormFactory = _config.ormFactory;
|
|
81
|
-
let migrateSchema: ((app: never) => Promise<void> | void) | undefined;
|
|
82
|
-
if (!ormFactory) {
|
|
83
|
-
const { resolveStorage } = await import('@fougere/runtime');
|
|
84
|
-
const storage = resolveStorage(fileConfig.db as never);
|
|
85
|
-
if (storage.ormFactory) {
|
|
86
|
-
log.debug('auto-resolving storage from config.db');
|
|
87
|
-
db = storage.db;
|
|
88
|
-
ormFactory = storage.ormFactory;
|
|
89
|
-
migrateSchema = storage.afterBoot as never;
|
|
90
|
-
} else {
|
|
91
|
-
log.debug('no db declared — falling back to in-memory ORM');
|
|
92
|
-
ormFactory = createMemoryOrm;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Layer-2 wiring: `remotes: { catalog: 'http://...' }` in fougere.config.ts
|
|
97
|
-
// is all the user writes — the default transport comes from here.
|
|
98
|
-
let remoteTransport: ((url: string) => Transport) | undefined;
|
|
99
|
-
if (Object.keys(fileConfig.remotes ?? {}).length > 0) {
|
|
100
|
-
log.debug(`remotes declared (${Object.keys(fileConfig.remotes!).join(', ')}) — wiring HTTP transport`);
|
|
101
|
-
const { createHttpTransport } = await import('@fougere/transport-http');
|
|
102
|
-
remoteTransport = (url) => createHttpTransport(url);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const app = await createApp({
|
|
106
|
-
root,
|
|
107
|
-
createContainer,
|
|
108
|
-
ormFactory,
|
|
109
|
-
db,
|
|
110
|
-
auth: fileConfig.auth,
|
|
111
|
-
remotes: fileConfig.remotes,
|
|
112
|
-
remoteTransport,
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
// Bring the schema up to date — creates missing tables and adds columns an
|
|
116
|
-
// entity gained. Auth entities travel with the app, no synthetic frond needed.
|
|
117
|
-
if (migrateSchema) {
|
|
118
|
-
log.debug('migrating schema from entities');
|
|
119
|
-
await migrateSchema(app as never);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (_config.afterBoot) {
|
|
123
|
-
log.debug('running afterBoot');
|
|
124
|
-
await _config.afterBoot(app);
|
|
125
|
-
log.info('afterBoot done');
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const ms = (performance.now() - bootStart).toFixed(0);
|
|
129
|
-
log.info(`ready in ${ms}ms — ${app.fronds.length} frond(s)${app.auth ? ` + auth (${app.auth.basePath})` : ''}`);
|
|
130
|
-
|
|
131
|
-
return app;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── Fallback ORM ─────────────────────────────────
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* The store an app with no `db` runs on.
|
|
138
|
-
*
|
|
139
|
-
* It used to ignore both its arguments — `(_entity, _name)` — so it forced the field
|
|
140
|
-
* name `id`, minted a uuid whatever the entity declared, and realized none of the
|
|
141
|
-
* lifecycle rules: `auto()` stamped nothing, a declared default stayed absent. The
|
|
142
|
-
* same page therefore behaved one way here and another way on SQLite.
|
|
143
|
-
*
|
|
144
|
-
* It reads the axes now, through the one realization every storage shares.
|
|
145
|
-
*/
|
|
146
|
-
export function createMemoryOrm(entity: SchemaLike, name: string): EntityOrm {
|
|
147
|
-
const fields = entity.getFields();
|
|
148
|
-
const pk = Object.entries(fields).find(([, field]) => field.role?.primary)?.[0] ?? 'id';
|
|
149
|
-
const store = new Map<string, Record<string, unknown>>();
|
|
150
|
-
// `EntityOrm.findById(id: string)` — but a key can hold a number, and a Map keyed on
|
|
151
|
-
// `1` does not answer `'1'`. SQL never had the question; here the divergence was
|
|
152
|
-
// silent and only on this storage.
|
|
153
|
-
const keyOf = (value: unknown) => String(value);
|
|
154
|
-
const matches = (row: Record<string, unknown>, criteria: Record<string, unknown>) =>
|
|
155
|
-
Object.entries(criteria).every(([key, value]) => Object.is(row[key], value));
|
|
156
|
-
return {
|
|
157
|
-
client: store,
|
|
158
|
-
async list(options?: any) {
|
|
159
|
-
let items = [...store.values()];
|
|
160
|
-
if (options?.where) items = items.filter((row) => matches(row, options.where));
|
|
161
|
-
const limit = options?.limit;
|
|
162
|
-
const offset = options?.page && limit ? (options.page - 1) * limit : options?.offset ?? 0;
|
|
163
|
-
if (offset > 0) items = items.slice(offset);
|
|
164
|
-
const hasMore = limit ? items.length > limit : false;
|
|
165
|
-
if (limit) items = items.slice(0, limit);
|
|
166
|
-
const result = items as any;
|
|
167
|
-
result.hasMore = hasMore;
|
|
168
|
-
result.endCursor = items.length > 0 ? String((items[items.length - 1] as any)[pk] ?? '') : undefined;
|
|
169
|
-
if (options?.count) result.total = store.size;
|
|
170
|
-
return result;
|
|
171
|
-
},
|
|
172
|
-
async findById(id: string) { return store.get(keyOf(id)); },
|
|
173
|
-
async findBy(criteria: Record<string, unknown>) {
|
|
174
|
-
return [...store.values()].find((row) => matches(row, criteria));
|
|
175
|
-
},
|
|
176
|
-
async findAllBy(criteria: Record<string, unknown>) {
|
|
177
|
-
return [...store.values()].filter((row) => matches(row, criteria));
|
|
178
|
-
},
|
|
179
|
-
async create(input: Partial<Record<string, unknown>>) {
|
|
180
|
-
const record = applyCreate(fields, input);
|
|
181
|
-
const id = record[pk] as string | undefined;
|
|
182
|
-
// `primary(text())` declares no generator, so nothing fills the hole and the
|
|
183
|
-
// caller has to. Keying on `undefined` would let the second create overwrite the
|
|
184
|
-
// first, in silence — the old version hid this by inventing an `id` field the
|
|
185
|
-
// entity never declared.
|
|
186
|
-
if (id === undefined) {
|
|
187
|
-
throw new Error(`${name}.create: '${pk}' is the primary key and nothing supplied it — this entity declares no generator for it.`);
|
|
188
|
-
}
|
|
189
|
-
// A create is not an upsert. `Map.set` overwrites, so a second create under the
|
|
190
|
-
// same key answered "created" while destroying the previous row — SQL answers a
|
|
191
|
-
// constraint violation, and a store that loses data silently is worse than one
|
|
192
|
-
// that fails.
|
|
193
|
-
if (store.has(keyOf(id))) {
|
|
194
|
-
throw new Error(`${name}.create: '${pk}' ${JSON.stringify(id)} already exists.`);
|
|
195
|
-
}
|
|
196
|
-
store.set(keyOf(id), record);
|
|
197
|
-
return record;
|
|
198
|
-
},
|
|
199
|
-
async update(id: string, input: Partial<Record<string, unknown>>) {
|
|
200
|
-
const existing = store.get(keyOf(id));
|
|
201
|
-
if (!existing) throw new Error(`Not found: ${id}`);
|
|
202
|
-
const updated = { ...existing, ...applyUpdate(fields, input), [pk]: existing[pk] };
|
|
203
|
-
store.set(keyOf(id), updated);
|
|
204
|
-
return updated;
|
|
205
|
-
},
|
|
206
|
-
async delete(id: string) { return store.delete(keyOf(id)); },
|
|
207
|
-
output(_schema: SchemaLike) { return this; },
|
|
208
|
-
};
|
|
209
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auth accessor — reads the AuthRuntime that the core mounted on the app.
|
|
3
|
-
*
|
|
4
|
-
* Auth is no longer a separate singleton: it lives on `app.auth`, built once
|
|
5
|
-
* during boot from the `auth` field of fougere.config.ts. This accessor is a
|
|
6
|
-
* thin async helper for server code (middleware, routes, /api/me).
|
|
7
|
-
*/
|
|
8
|
-
import type { AuthRuntime } from '@fougere/core';
|
|
9
|
-
import { useFougereApp } from './fougereApp';
|
|
10
|
-
|
|
11
|
-
/** Get the auth runtime resolved at boot. Throws if no `auth` was declared in fougere.config.ts. */
|
|
12
|
-
export async function useFougereAuth(): Promise<AuthRuntime> {
|
|
13
|
-
const app = await useFougereApp();
|
|
14
|
-
if (!app.auth) {
|
|
15
|
-
throw new Error(
|
|
16
|
-
'Auth not configured — declare `auth: { provider, ... }` in fougere.config.ts.',
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
return app.auth;
|
|
20
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The session view — the one place that turns the server-resolved
|
|
3
|
-
* request context (filled by the auth middleware) into what the client
|
|
4
|
-
* is allowed to see. One resolution, three readers: the page by
|
|
5
|
-
* hydration, the refresh route over the wire, handlers by invocation.
|
|
6
|
-
*
|
|
7
|
-
* The app-declared context (viewer enrichment) will attach here when
|
|
8
|
-
* a real case lands — this function is the seam.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export interface SessionView {
|
|
12
|
-
user: Record<string, unknown> | null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function sessionViewOf(context: Record<string, unknown>): SessionView {
|
|
16
|
-
const raw = context.user as Record<string, unknown> | undefined;
|
|
17
|
-
if (!raw) return { user: null };
|
|
18
|
-
const { passwordHash: _passwordHash, ...user } = raw;
|
|
19
|
-
return { user };
|
|
20
|
-
}
|