@holo-js/adapter-nuxt 0.1.4 → 0.1.6
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.mts +67 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +127 -30
- package/dist/runtime/authorization-error.d.ts +0 -0
- package/dist/runtime/authorization-error.js +8 -0
- package/dist/runtime/composables/forms.d.ts +18 -2
- package/dist/runtime/composables/forms.js +261 -43
- package/dist/runtime/composables/index.d.ts +3 -0
- package/dist/runtime/composables/index.js +105 -1
- package/dist/runtime/plugins/forms.d.ts +0 -0
- package/dist/runtime/plugins/forms.js +18 -0
- package/dist/runtime/plugins/init.js +7 -1
- package/dist/runtime/server/auto-imports/holo.d.ts +0 -0
- package/dist/runtime/server/auto-imports/holo.js +10 -0
- package/dist/runtime/server/error.d.ts +0 -0
- package/dist/runtime/server/error.js +32 -0
- package/dist/runtime/server/form-failure.d.ts +0 -0
- package/dist/runtime/server/form-failure.js +100 -0
- package/dist/runtime/server/imports/holo.d.ts +1 -0
- package/dist/runtime/server/imports/holo.js +9 -5
- package/dist/runtime/server/imports/storage.d.ts +1 -0
- package/dist/runtime/server/imports/storage.js +4 -0
- package/dist/runtime/server/routes/storage.get.js +41 -7
- package/dist/runtime/shims.d.ts +12 -0
- package/package.json +13 -11
package/dist/module.d.mts
CHANGED
|
@@ -22,10 +22,74 @@ type StorageModuleOptions = {
|
|
|
22
22
|
routePrefix?: string;
|
|
23
23
|
disks?: Record<string, StorageDiskConfig>;
|
|
24
24
|
};
|
|
25
|
+
type RuntimeDiskConfig = {
|
|
26
|
+
name: string;
|
|
27
|
+
driver: StorageDriver;
|
|
28
|
+
visibility: StorageVisibility;
|
|
29
|
+
root?: string;
|
|
30
|
+
url?: string;
|
|
31
|
+
bucket?: string;
|
|
32
|
+
region?: string;
|
|
33
|
+
endpoint?: string;
|
|
34
|
+
accessKeyId?: string;
|
|
35
|
+
secretAccessKey?: string;
|
|
36
|
+
sessionToken?: string;
|
|
37
|
+
forcePathStyleEndpoint?: boolean;
|
|
38
|
+
};
|
|
39
|
+
type HoloStorageRuntimeConfig = {
|
|
40
|
+
defaultDisk: string | undefined;
|
|
41
|
+
diskNames: string[];
|
|
42
|
+
routePrefix: string;
|
|
43
|
+
disks: Record<string, RuntimeDiskConfig>;
|
|
44
|
+
};
|
|
25
45
|
type StorageS3Module = {
|
|
26
46
|
default: unknown;
|
|
27
47
|
};
|
|
28
48
|
declare function hasModuleNotFoundCode(error: unknown, expectedSpecifier: string): boolean;
|
|
49
|
+
declare function isModuleResolutionFailure(error: unknown): boolean;
|
|
50
|
+
type ViteOptimizeDepsOptions = {
|
|
51
|
+
include?: string[];
|
|
52
|
+
[key: string]: unknown;
|
|
53
|
+
};
|
|
54
|
+
type NuxtViteOptions = {
|
|
55
|
+
optimizeDeps?: ViteOptimizeDepsOptions;
|
|
56
|
+
[key: string]: unknown;
|
|
57
|
+
};
|
|
58
|
+
interface NuxtOptionsWithNitro {
|
|
59
|
+
nitro: {
|
|
60
|
+
storage: Record<string, unknown>;
|
|
61
|
+
errorHandler?: string | string[];
|
|
62
|
+
experimental?: {
|
|
63
|
+
asyncContext?: boolean;
|
|
64
|
+
[key: string]: unknown;
|
|
65
|
+
};
|
|
66
|
+
[key: string]: unknown;
|
|
67
|
+
};
|
|
68
|
+
runtimeConfig: {
|
|
69
|
+
public?: {
|
|
70
|
+
holo?: {
|
|
71
|
+
appName?: string;
|
|
72
|
+
[key: string]: unknown;
|
|
73
|
+
};
|
|
74
|
+
[key: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
holoStorage?: HoloStorageRuntimeConfig;
|
|
77
|
+
[key: string]: unknown;
|
|
78
|
+
};
|
|
79
|
+
build: {
|
|
80
|
+
transpile: string[];
|
|
81
|
+
};
|
|
82
|
+
vite?: NuxtViteOptions;
|
|
83
|
+
srcDir: string;
|
|
84
|
+
rootDir?: string;
|
|
85
|
+
_holoStorageModuleOptions?: StorageModuleOptions;
|
|
86
|
+
_holoStorageFinalizeRegistered?: boolean;
|
|
87
|
+
_holoStorageRuntimeRegistered?: boolean;
|
|
88
|
+
_holoCoreRuntimeRegistered?: boolean;
|
|
89
|
+
_holoTypesRegistered?: boolean;
|
|
90
|
+
}
|
|
91
|
+
declare function resolveClientOptimizeDeps(rootDir: string): string[];
|
|
92
|
+
declare function addViteOptimizeDeps(opts: NuxtOptionsWithNitro, deps: readonly string[]): void;
|
|
29
93
|
declare function importOptionalStorageS3Module(): Promise<StorageS3Module | undefined>;
|
|
30
94
|
declare function hasLoadedConfigFile(loaded: LoadedHoloConfig<HoloConfigMap>, configName: string): boolean;
|
|
31
95
|
declare function toStorageModuleOptions(loaded: LoadedHoloConfig<HoloConfigMap>): StorageModuleOptions;
|
|
@@ -38,9 +102,12 @@ declare const _default: {
|
|
|
38
102
|
};
|
|
39
103
|
|
|
40
104
|
declare const moduleInternals: {
|
|
105
|
+
addViteOptimizeDeps: typeof addViteOptimizeDeps;
|
|
41
106
|
hasModuleNotFoundCode: typeof hasModuleNotFoundCode;
|
|
42
107
|
hasLoadedConfigFile: typeof hasLoadedConfigFile;
|
|
43
108
|
importOptionalStorageS3Module: typeof importOptionalStorageS3Module;
|
|
109
|
+
isModuleResolutionFailure: typeof isModuleResolutionFailure;
|
|
110
|
+
resolveClientOptimizeDeps: typeof resolveClientOptimizeDeps;
|
|
44
111
|
};
|
|
45
112
|
declare const adapterNuxtInternals: {
|
|
46
113
|
toStorageModuleOptions: typeof toStorageModuleOptions;
|
package/dist/module.json
CHANGED
package/dist/module.mjs
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import { readdir, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { readdir, mkdir, writeFile, access } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
3
|
+
import { resolve, extname, relative, basename, dirname } from 'node:path';
|
|
4
|
+
import { defineNuxtModule, createResolver, addServerPlugin, addServerImportsDir, addServerHandler } from '@nuxt/kit';
|
|
4
5
|
import { loadConfigDirectory } from '@holo-js/config';
|
|
5
6
|
|
|
6
7
|
const MODEL_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".ts", ".mts", ".cts", ".js", ".mjs", ".cjs"]);
|
|
8
|
+
const HOLO_PACKAGE_SCOPE = "@holo-js/";
|
|
9
|
+
const CLIENT_OPTIMIZE_DEPS = [
|
|
10
|
+
{
|
|
11
|
+
packageName: `${HOLO_PACKAGE_SCOPE}forms`,
|
|
12
|
+
include: `${HOLO_PACKAGE_SCOPE}forms > ${HOLO_PACKAGE_SCOPE}validation > valibot`
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
packageName: `${HOLO_PACKAGE_SCOPE}validation`,
|
|
16
|
+
include: `${HOLO_PACKAGE_SCOPE}validation > valibot`
|
|
17
|
+
}
|
|
18
|
+
];
|
|
7
19
|
function escapeRegExp(value) {
|
|
8
20
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
9
21
|
}
|
|
@@ -28,6 +40,47 @@ function hasModuleNotFoundCode(error, expectedSpecifier) {
|
|
|
28
40
|
}
|
|
29
41
|
return false;
|
|
30
42
|
}
|
|
43
|
+
function isModuleResolutionFailure(error) {
|
|
44
|
+
if (!error || typeof error !== "object") {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return "code" in error && (error.code === "MODULE_NOT_FOUND" || error.code === "ERR_MODULE_NOT_FOUND");
|
|
48
|
+
}
|
|
49
|
+
function resolveClientOptimizeDeps(rootDir) {
|
|
50
|
+
const projectRequire = createRequire(resolve(rootDir, "package.json"));
|
|
51
|
+
const deps = [];
|
|
52
|
+
for (const { packageName, include } of CLIENT_OPTIMIZE_DEPS) {
|
|
53
|
+
try {
|
|
54
|
+
projectRequire.resolve(packageName);
|
|
55
|
+
deps.push(include);
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (!isModuleResolutionFailure(error)) {
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return deps;
|
|
63
|
+
}
|
|
64
|
+
function addViteOptimizeDeps(opts, deps) {
|
|
65
|
+
if (deps.length === 0) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
opts.vite = opts.vite || {};
|
|
69
|
+
opts.vite.optimizeDeps = opts.vite.optimizeDeps || {};
|
|
70
|
+
opts.vite.optimizeDeps.include = [
|
|
71
|
+
.../* @__PURE__ */ new Set([
|
|
72
|
+
...opts.vite.optimizeDeps.include || [],
|
|
73
|
+
...deps
|
|
74
|
+
])
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
function addNitroErrorHandler(opts, handler) {
|
|
78
|
+
const current = opts.nitro.errorHandler;
|
|
79
|
+
const handlers = Array.isArray(current) ? current : typeof current === "string" ? [current] : [];
|
|
80
|
+
if (!handlers.includes(handler)) {
|
|
81
|
+
opts.nitro.errorHandler = [...handlers, handler];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
31
84
|
async function importOptionalStorageModule() {
|
|
32
85
|
try {
|
|
33
86
|
return await import('@holo-js/storage');
|
|
@@ -55,6 +108,15 @@ function hasLoadedConfigFile(loaded, configName) {
|
|
|
55
108
|
return normalizedPath.endsWith(`/config/${configName}.ts`) || normalizedPath.endsWith(`/config/${configName}.mts`) || normalizedPath.endsWith(`/config/${configName}.js`) || normalizedPath.endsWith(`/config/${configName}.mjs`) || normalizedPath.endsWith(`/config/${configName}.cts`) || normalizedPath.endsWith(`/config/${configName}.cjs`);
|
|
56
109
|
});
|
|
57
110
|
}
|
|
111
|
+
async function ensureModelRegistryTypesPlaceholder(path) {
|
|
112
|
+
try {
|
|
113
|
+
await access(path);
|
|
114
|
+
return;
|
|
115
|
+
} catch {
|
|
116
|
+
await mkdir(dirname(path), { recursive: true });
|
|
117
|
+
await writeFile(path, "// Generated by holo prepare. Do not edit.\n\nexport {}\n", "utf8");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
58
120
|
function toStorageModuleOptions(loaded) {
|
|
59
121
|
return {
|
|
60
122
|
defaultDisk: loaded.storage.defaultDisk,
|
|
@@ -62,10 +124,12 @@ function toStorageModuleOptions(loaded) {
|
|
|
62
124
|
disks: { ...loaded.storage.disks }
|
|
63
125
|
};
|
|
64
126
|
}
|
|
65
|
-
async function createServerModelImports(sourceDir) {
|
|
66
|
-
const modelsDir = resolve(sourceDir,
|
|
127
|
+
async function createServerModelImports(sourceDir, modelsRelativePath, generatedSchemaRelativePath) {
|
|
128
|
+
const modelsDir = resolve(sourceDir, modelsRelativePath);
|
|
129
|
+
const generatedSchemaPath = resolve(sourceDir, generatedSchemaRelativePath);
|
|
67
130
|
const modelImportDir = resolve(sourceDir, ".holo-js/generated/nuxt-server-imports");
|
|
68
131
|
const modelImportFile = resolve(modelImportDir, "models.ts");
|
|
132
|
+
const modelPluginFile = resolve(modelImportDir, "plugin.ts");
|
|
69
133
|
let modelFiles;
|
|
70
134
|
try {
|
|
71
135
|
modelFiles = (await readdir(modelsDir)).filter((fileName) => MODEL_FILE_EXTENSIONS.has(extname(fileName))).sort((left, right) => left.localeCompare(right));
|
|
@@ -75,17 +139,34 @@ async function createServerModelImports(sourceDir) {
|
|
|
75
139
|
if (modelFiles.length === 0) {
|
|
76
140
|
return null;
|
|
77
141
|
}
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
142
|
+
const generatedSchemaImportPath = relative(modelImportDir, generatedSchemaPath).replaceAll("\\", "/");
|
|
143
|
+
const normalizedGeneratedSchemaImportPath = generatedSchemaImportPath.replace(/^(?!\.)/, "./");
|
|
144
|
+
const lines = [
|
|
145
|
+
`import '${normalizedGeneratedSchemaImportPath.slice(0, -extname(normalizedGeneratedSchemaImportPath).length)}'`,
|
|
146
|
+
"",
|
|
147
|
+
...modelFiles.map((fileName) => {
|
|
148
|
+
const modelName = basename(fileName, extname(fileName));
|
|
149
|
+
const importPath = relative(modelImportDir, resolve(modelsDir, fileName)).replaceAll("\\", "/");
|
|
150
|
+
const normalizedImportPath = importPath.replace(/^(?!\.)/, "./");
|
|
151
|
+
const extension = extname(normalizedImportPath);
|
|
152
|
+
return `export { default as ${modelName} } from '${normalizedImportPath.slice(0, -extension.length)}'`;
|
|
153
|
+
})
|
|
154
|
+
];
|
|
155
|
+
const pluginLines = [
|
|
156
|
+
`import '${normalizedGeneratedSchemaImportPath.slice(0, -extname(normalizedGeneratedSchemaImportPath).length)}'`,
|
|
157
|
+
"import './models'",
|
|
158
|
+
"",
|
|
159
|
+
"export default () => {}"
|
|
160
|
+
];
|
|
85
161
|
await mkdir(modelImportDir, { recursive: true });
|
|
86
162
|
await writeFile(modelImportFile, `${lines.join("\n")}
|
|
87
163
|
`, "utf8");
|
|
88
|
-
|
|
164
|
+
await writeFile(modelPluginFile, `${pluginLines.join("\n")}
|
|
165
|
+
`, "utf8");
|
|
166
|
+
return {
|
|
167
|
+
importDir: modelImportDir,
|
|
168
|
+
pluginFile: modelPluginFile
|
|
169
|
+
};
|
|
89
170
|
}
|
|
90
171
|
const module$1 = defineNuxtModule({
|
|
91
172
|
meta: {
|
|
@@ -97,6 +178,10 @@ const module$1 = defineNuxtModule({
|
|
|
97
178
|
const opts = nuxt.options;
|
|
98
179
|
const rootDir = opts.rootDir ?? opts.srcDir ?? process.cwd();
|
|
99
180
|
const sourceDir = opts.srcDir ?? rootDir;
|
|
181
|
+
const authTypesPath = resolve(rootDir, ".holo-js/generated/auth.d.ts");
|
|
182
|
+
const authorizationTypesPath = resolve(rootDir, ".holo-js/generated/authorization/types.d.ts");
|
|
183
|
+
const modelRegistryTypesPath = resolve(rootDir, ".holo-js/generated/model-registry.d.ts");
|
|
184
|
+
addViteOptimizeDeps(opts, resolveClientOptimizeDeps(rootDir));
|
|
100
185
|
const loaded = await loadConfigDirectory(rootDir, {
|
|
101
186
|
preferCache: process.env.NODE_ENV === "production",
|
|
102
187
|
processEnv: process.env
|
|
@@ -106,7 +191,17 @@ const module$1 = defineNuxtModule({
|
|
|
106
191
|
const s3Driver = resolver.resolve("./runtime/drivers/s3.js");
|
|
107
192
|
opts.nitro = opts.nitro || { storage: {} };
|
|
108
193
|
opts.nitro.storage = opts.nitro.storage || {};
|
|
194
|
+
addNitroErrorHandler(opts, resolver.resolve("./runtime/server/error"));
|
|
195
|
+
opts.nitro.experimental = {
|
|
196
|
+
...opts.nitro.experimental || {},
|
|
197
|
+
asyncContext: true
|
|
198
|
+
};
|
|
109
199
|
opts.runtimeConfig = opts.runtimeConfig || {};
|
|
200
|
+
opts.runtimeConfig.public = opts.runtimeConfig.public || {};
|
|
201
|
+
opts.runtimeConfig.public.holo = {
|
|
202
|
+
...opts.runtimeConfig.public.holo || {},
|
|
203
|
+
appName: loaded.app.name
|
|
204
|
+
};
|
|
110
205
|
opts.runtimeConfig.holo = {
|
|
111
206
|
appUrl: loaded.app.url,
|
|
112
207
|
appEnv: loaded.app.env,
|
|
@@ -130,24 +225,17 @@ const module$1 = defineNuxtModule({
|
|
|
130
225
|
opts.runtimeConfig.holoStorage = normalizedStorage;
|
|
131
226
|
}
|
|
132
227
|
if (!opts._holoCoreRuntimeRegistered) {
|
|
133
|
-
const imports = [
|
|
134
|
-
{ name: "holo", as: "holo", from: resolver.resolve("./runtime/composables") },
|
|
135
|
-
{ name: "useHoloDb", as: "useHoloDb", from: resolver.resolve("./runtime/composables") },
|
|
136
|
-
{ name: "useHoloEnv", as: "useHoloEnv", from: resolver.resolve("./runtime/composables") },
|
|
137
|
-
{ name: "useHoloDebug", as: "useHoloDebug", from: resolver.resolve("./runtime/composables") }
|
|
138
|
-
];
|
|
139
|
-
if (storageModule) {
|
|
140
|
-
imports.push(
|
|
141
|
-
{ name: "useStorage", as: "useStorage", from: resolver.resolve("./runtime/composables/storage") },
|
|
142
|
-
{ name: "Storage", as: "Storage", from: resolver.resolve("./runtime/composables/storage") }
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
228
|
addServerPlugin(resolver.resolve("./runtime/plugins/init"));
|
|
146
|
-
|
|
147
|
-
addServerImportsDir(resolver.resolve("./runtime/server/imports"));
|
|
148
|
-
const serverModelImports = await createServerModelImports(
|
|
229
|
+
addServerPlugin(resolver.resolve("./runtime/plugins/forms"));
|
|
230
|
+
addServerImportsDir(resolver.resolve("./runtime/server/auto-imports"));
|
|
231
|
+
const serverModelImports = await createServerModelImports(
|
|
232
|
+
sourceDir,
|
|
233
|
+
loaded.app.paths.models,
|
|
234
|
+
loaded.app.paths.generatedSchema
|
|
235
|
+
);
|
|
149
236
|
if (serverModelImports) {
|
|
150
|
-
addServerImportsDir(serverModelImports);
|
|
237
|
+
addServerImportsDir(serverModelImports.importDir);
|
|
238
|
+
addServerPlugin(serverModelImports.pluginFile);
|
|
151
239
|
}
|
|
152
240
|
opts._holoCoreRuntimeRegistered = true;
|
|
153
241
|
}
|
|
@@ -179,16 +267,25 @@ const module$1 = defineNuxtModule({
|
|
|
179
267
|
}
|
|
180
268
|
if (!opts._holoTypesRegistered) {
|
|
181
269
|
opts._holoTypesRegistered = true;
|
|
270
|
+
await ensureModelRegistryTypesPlaceholder(authTypesPath);
|
|
271
|
+
await ensureModelRegistryTypesPlaceholder(authorizationTypesPath);
|
|
272
|
+
await ensureModelRegistryTypesPlaceholder(modelRegistryTypesPath);
|
|
182
273
|
nuxt.hook("prepare:types", ({ references }) => {
|
|
183
274
|
references.push({ types: "@holo-js/adapter-nuxt" });
|
|
275
|
+
references.push({ path: authTypesPath });
|
|
276
|
+
references.push({ path: authorizationTypesPath });
|
|
277
|
+
references.push({ path: modelRegistryTypesPath });
|
|
184
278
|
});
|
|
185
279
|
}
|
|
186
280
|
}
|
|
187
281
|
});
|
|
188
282
|
const moduleInternals = {
|
|
283
|
+
addViteOptimizeDeps,
|
|
189
284
|
hasModuleNotFoundCode,
|
|
190
285
|
hasLoadedConfigFile,
|
|
191
|
-
importOptionalStorageS3Module
|
|
286
|
+
importOptionalStorageS3Module,
|
|
287
|
+
isModuleResolutionFailure,
|
|
288
|
+
resolveClientOptimizeDeps
|
|
192
289
|
};
|
|
193
290
|
const adapterNuxtInternals = {
|
|
194
291
|
toStorageModuleOptions
|
|
File without changes
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createError } from "h3";
|
|
2
|
+
export function createNuxtAuthorizationError(decision) {
|
|
3
|
+
const status = decision.status === 404 ? 404 : 403;
|
|
4
|
+
return createError({
|
|
5
|
+
statusCode: status,
|
|
6
|
+
statusMessage: decision.message ?? "You are not authorized to perform this action."
|
|
7
|
+
});
|
|
8
|
+
}
|
|
@@ -1,11 +1,27 @@
|
|
|
1
|
+
import type { FormSchema, InferFormData } from '@holo-js/forms'
|
|
2
|
+
import type { ValidationErrorBag } from '@holo-js/forms'
|
|
3
|
+
import type {
|
|
4
|
+
InferFormFieldTree,
|
|
5
|
+
UseFormOptions,
|
|
6
|
+
UseFormResult,
|
|
7
|
+
} from '@holo-js/forms/internal/client'
|
|
8
|
+
|
|
1
9
|
export type {
|
|
2
10
|
ClientSubmitContext,
|
|
3
11
|
ClientSubmitResult,
|
|
4
12
|
FormFieldState,
|
|
5
13
|
FormFieldTree,
|
|
14
|
+
InferFormFieldTree,
|
|
6
15
|
UseFormOptions,
|
|
7
16
|
UseFormResult,
|
|
8
17
|
ValidateOnMode,
|
|
9
|
-
} from '@holo-js/forms/client'
|
|
18
|
+
} from '@holo-js/forms/internal/client'
|
|
19
|
+
|
|
20
|
+
export declare function useForm<TSchema extends FormSchema, TSuccess = unknown>(
|
|
21
|
+
schemaDefinition: TSchema,
|
|
22
|
+
options?: UseFormOptions<InferFormData<TSchema>, TSuccess>,
|
|
23
|
+
): UseFormResult<InferFormData<TSchema>, TSuccess, InferFormFieldTree<TSchema>>
|
|
10
24
|
|
|
11
|
-
export declare
|
|
25
|
+
export declare function useValidationErrors<TData = Record<string, unknown>>(
|
|
26
|
+
bag?: string,
|
|
27
|
+
): ValidationErrorBag<TData>
|
|
@@ -1,69 +1,287 @@
|
|
|
1
|
-
import { onScopeDispose, shallowRef, watchEffect } from "vue";
|
|
1
|
+
import { onScopeDispose, reactive, shallowRef, watchEffect } from "vue";
|
|
2
|
+
import { useCookie } from "#app";
|
|
3
|
+
import { DEFAULT_VALIDATION_BAG, createErrorBag } from "@holo-js/validation";
|
|
2
4
|
import {
|
|
3
|
-
|
|
4
|
-
} from "@holo-js/forms/client";
|
|
5
|
+
createFormClient
|
|
6
|
+
} from "@holo-js/forms/internal/client";
|
|
7
|
+
const FORM_FAILURE_COOKIE = "holo_form_failure";
|
|
8
|
+
const ARRAY_MUTATION_METHODS = /* @__PURE__ */ new Set([
|
|
9
|
+
"copyWithin",
|
|
10
|
+
"fill",
|
|
11
|
+
"pop",
|
|
12
|
+
"push",
|
|
13
|
+
"reverse",
|
|
14
|
+
"shift",
|
|
15
|
+
"sort",
|
|
16
|
+
"splice",
|
|
17
|
+
"unshift"
|
|
18
|
+
]);
|
|
5
19
|
function isPlainObject(value) {
|
|
6
20
|
return !!value && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Blob);
|
|
7
21
|
}
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
22
|
+
function isLeafValue(value) {
|
|
23
|
+
return value instanceof Date || value instanceof Blob || !Array.isArray(value) && !isPlainObject(value);
|
|
24
|
+
}
|
|
25
|
+
function readBrowserCookie(name) {
|
|
26
|
+
const cookie = globalThis.document?.cookie;
|
|
27
|
+
if (!cookie) {
|
|
28
|
+
return void 0;
|
|
29
|
+
}
|
|
30
|
+
for (const segment of cookie.split(";")) {
|
|
31
|
+
const trimmed = segment.trim();
|
|
32
|
+
const separator = trimmed.indexOf("=");
|
|
33
|
+
if (separator <= 0) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (trimmed.slice(0, separator) === name) {
|
|
37
|
+
return trimmed.slice(separator + 1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return void 0;
|
|
41
|
+
}
|
|
42
|
+
function readCookie(name) {
|
|
43
|
+
try {
|
|
44
|
+
const cookie = useCookie(name);
|
|
45
|
+
if (typeof cookie.value === "string" || isPlainObject(cookie.value)) {
|
|
46
|
+
return cookie.value;
|
|
47
|
+
}
|
|
48
|
+
} catch {
|
|
49
|
+
return readBrowserCookie(name);
|
|
50
|
+
}
|
|
51
|
+
return readBrowserCookie(name);
|
|
52
|
+
}
|
|
53
|
+
function parseFlashedValidationPayload() {
|
|
54
|
+
const value = readCookie(FORM_FAILURE_COOKIE);
|
|
55
|
+
if (!value) {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
const decoded = typeof value === "string" ? JSON.parse(decodeURIComponent(value)) : value;
|
|
60
|
+
if (!isPlainObject(decoded) || !isPlainObject(decoded.errors)) {
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
bag: typeof decoded.bag === "string" ? decoded.bag : DEFAULT_VALIDATION_BAG,
|
|
65
|
+
errors: decoded.errors
|
|
66
|
+
};
|
|
67
|
+
} catch {
|
|
68
|
+
return void 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function useValidationErrors(bag = DEFAULT_VALIDATION_BAG) {
|
|
72
|
+
const payload = parseFlashedValidationPayload();
|
|
73
|
+
if (!payload || (payload.bag ?? DEFAULT_VALIDATION_BAG) !== bag) {
|
|
74
|
+
return createErrorBag();
|
|
75
|
+
}
|
|
76
|
+
return createErrorBag(payload.errors ?? {});
|
|
77
|
+
}
|
|
78
|
+
function getValueAtPath(root, path) {
|
|
79
|
+
const parts = path.split(".").map((part) => part.trim()).filter(Boolean);
|
|
80
|
+
let cursor = root;
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
if (!isPlainObject(cursor) && !Array.isArray(cursor)) {
|
|
83
|
+
return void 0;
|
|
84
|
+
}
|
|
85
|
+
cursor = cursor[part];
|
|
86
|
+
}
|
|
87
|
+
return cursor;
|
|
88
|
+
}
|
|
89
|
+
function createArrayMutationView(source, path, getForm, version, cache) {
|
|
90
|
+
const cached = cache.get(source);
|
|
11
91
|
if (cached) {
|
|
12
92
|
return cached;
|
|
13
93
|
}
|
|
14
|
-
const proxy = new Proxy(
|
|
15
|
-
get(
|
|
94
|
+
const proxy = new Proxy([], {
|
|
95
|
+
get(_target, key) {
|
|
16
96
|
void version.value;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return value.bind(currentTarget);
|
|
97
|
+
const current = getValueAtPath(getForm().values, path);
|
|
98
|
+
if (!Array.isArray(current)) {
|
|
99
|
+
return void 0;
|
|
21
100
|
}
|
|
22
|
-
|
|
23
|
-
|
|
101
|
+
const value = Reflect.get(current, key, current);
|
|
102
|
+
if (typeof value !== "function") {
|
|
103
|
+
return value;
|
|
24
104
|
}
|
|
25
|
-
|
|
105
|
+
if (typeof key === "string" && ARRAY_MUTATION_METHODS.has(key)) {
|
|
106
|
+
return (...args) => {
|
|
107
|
+
const latest = getValueAtPath(getForm().values, path);
|
|
108
|
+
if (!Array.isArray(latest)) {
|
|
109
|
+
return void 0;
|
|
110
|
+
}
|
|
111
|
+
const next = latest.slice();
|
|
112
|
+
const result = Reflect.get(next, key, next).apply(next, args);
|
|
113
|
+
void getForm().setValue(path, next);
|
|
114
|
+
return result;
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return value.bind(current);
|
|
26
118
|
},
|
|
27
|
-
set(
|
|
28
|
-
const
|
|
29
|
-
|
|
119
|
+
set(_target, key, value) {
|
|
120
|
+
const current = getValueAtPath(getForm().values, path);
|
|
121
|
+
if (!Array.isArray(current)) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
const next = current.slice();
|
|
125
|
+
const updated = Reflect.set(next, key, value);
|
|
126
|
+
if (updated) {
|
|
127
|
+
void getForm().setValue(path, next);
|
|
128
|
+
}
|
|
30
129
|
return updated;
|
|
31
130
|
},
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
getOwnPropertyDescriptor(_shell, key) {
|
|
37
|
-
void version.value;
|
|
38
|
-
const descriptor = Reflect.getOwnPropertyDescriptor(targetRef.value, key);
|
|
39
|
-
if (!descriptor) {
|
|
40
|
-
return void 0;
|
|
131
|
+
deleteProperty(_target, key) {
|
|
132
|
+
const current = getValueAtPath(getForm().values, path);
|
|
133
|
+
if (!Array.isArray(current)) {
|
|
134
|
+
return false;
|
|
41
135
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
void version.value;
|
|
49
|
-
return Reflect.has(targetRef.value, key);
|
|
136
|
+
const next = current.slice();
|
|
137
|
+
const deleted = Reflect.deleteProperty(next, key);
|
|
138
|
+
if (deleted) {
|
|
139
|
+
void getForm().setValue(path, next);
|
|
140
|
+
}
|
|
141
|
+
return deleted;
|
|
50
142
|
}
|
|
51
143
|
});
|
|
52
|
-
cache.set(
|
|
144
|
+
cache.set(source, proxy);
|
|
53
145
|
return proxy;
|
|
54
146
|
}
|
|
147
|
+
function defineLeafAccessor(target, key, path, getForm, version, arrayCache) {
|
|
148
|
+
const descriptor = Object.getOwnPropertyDescriptor(target, key);
|
|
149
|
+
if (descriptor?.get && descriptor?.set) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
Object.defineProperty(target, key, {
|
|
153
|
+
enumerable: true,
|
|
154
|
+
configurable: true,
|
|
155
|
+
get() {
|
|
156
|
+
void version.value;
|
|
157
|
+
const value = getValueAtPath(getForm().values, path);
|
|
158
|
+
return Array.isArray(value) ? createArrayMutationView(value, path, getForm, version, arrayCache) : value;
|
|
159
|
+
},
|
|
160
|
+
set(nextValue) {
|
|
161
|
+
void getForm().setValue(path, nextValue);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function syncValuesView(target, source, getForm, version, arrayCache, prefix = "") {
|
|
166
|
+
if (!isPlainObject(source)) {
|
|
167
|
+
for (const key of Object.keys(target)) {
|
|
168
|
+
delete target[key];
|
|
169
|
+
}
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
for (const key of Object.keys(target)) {
|
|
173
|
+
if (!(key in source)) {
|
|
174
|
+
delete target[key];
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const [key, item] of Object.entries(source)) {
|
|
178
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
179
|
+
const current = target[key];
|
|
180
|
+
if (Array.isArray(item)) {
|
|
181
|
+
if (isPlainObject(current)) {
|
|
182
|
+
delete target[key];
|
|
183
|
+
}
|
|
184
|
+
defineLeafAccessor(target, key, path, getForm, version, arrayCache);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
if (isLeafValue(item)) {
|
|
188
|
+
if (isPlainObject(current)) {
|
|
189
|
+
delete target[key];
|
|
190
|
+
}
|
|
191
|
+
defineLeafAccessor(target, key, path, getForm, version, arrayCache);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (!isPlainObject(current)) {
|
|
195
|
+
target[key] = {};
|
|
196
|
+
}
|
|
197
|
+
syncValuesView(target[key], item, getForm, version, arrayCache, path);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
55
200
|
export function useForm(schemaDefinition, options = {}) {
|
|
56
|
-
const form = shallowRef(
|
|
201
|
+
const form = shallowRef(void 0);
|
|
57
202
|
const version = shallowRef(0);
|
|
58
|
-
|
|
203
|
+
let versionCounter = 0;
|
|
204
|
+
const rawValues = {};
|
|
205
|
+
const values = reactive(rawValues);
|
|
206
|
+
const arrayCache = /* @__PURE__ */ new WeakMap();
|
|
207
|
+
let activeForm;
|
|
208
|
+
const getActiveForm = () => {
|
|
209
|
+
if (!activeForm) {
|
|
210
|
+
throw new TypeError("Expected form to be initialized.");
|
|
211
|
+
}
|
|
212
|
+
return activeForm;
|
|
213
|
+
};
|
|
214
|
+
const currentForm = () => {
|
|
215
|
+
const current = form.value;
|
|
216
|
+
if (!current) {
|
|
217
|
+
throw new TypeError("Expected form to be initialized.");
|
|
218
|
+
}
|
|
219
|
+
return current;
|
|
220
|
+
};
|
|
221
|
+
const syncValuesFromForm = (localForm) => {
|
|
222
|
+
activeForm = localForm;
|
|
223
|
+
syncValuesView(
|
|
224
|
+
rawValues,
|
|
225
|
+
localForm.values,
|
|
226
|
+
getActiveForm,
|
|
227
|
+
version,
|
|
228
|
+
arrayCache
|
|
229
|
+
);
|
|
230
|
+
};
|
|
59
231
|
const stopWatching = watchEffect((onCleanup) => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
232
|
+
const localForm = createFormClient(schemaDefinition, options);
|
|
233
|
+
syncValuesFromForm(localForm);
|
|
234
|
+
form.value = localForm;
|
|
235
|
+
version.value = ++versionCounter;
|
|
236
|
+
const unsubscribe = localForm.subscribe(() => {
|
|
237
|
+
syncValuesFromForm(localForm);
|
|
238
|
+
version.value = ++versionCounter;
|
|
64
239
|
});
|
|
65
240
|
onCleanup(unsubscribe);
|
|
66
241
|
});
|
|
67
242
|
onScopeDispose(stopWatching);
|
|
68
|
-
return
|
|
243
|
+
return reactive({
|
|
244
|
+
get fields() {
|
|
245
|
+
void version.value;
|
|
246
|
+
return currentForm().fields;
|
|
247
|
+
},
|
|
248
|
+
values,
|
|
249
|
+
get errors() {
|
|
250
|
+
void version.value;
|
|
251
|
+
return currentForm().errors;
|
|
252
|
+
},
|
|
253
|
+
get submitting() {
|
|
254
|
+
void version.value;
|
|
255
|
+
return currentForm().submitting;
|
|
256
|
+
},
|
|
257
|
+
get valid() {
|
|
258
|
+
void version.value;
|
|
259
|
+
return currentForm().valid;
|
|
260
|
+
},
|
|
261
|
+
get lastSubmission() {
|
|
262
|
+
void version.value;
|
|
263
|
+
return currentForm().lastSubmission;
|
|
264
|
+
},
|
|
265
|
+
subscribe(listener) {
|
|
266
|
+
return currentForm().subscribe(listener);
|
|
267
|
+
},
|
|
268
|
+
async validate() {
|
|
269
|
+
return await currentForm().validate();
|
|
270
|
+
},
|
|
271
|
+
async validateField(path) {
|
|
272
|
+
return await currentForm().validateField(path);
|
|
273
|
+
},
|
|
274
|
+
async submit() {
|
|
275
|
+
return await currentForm().submit();
|
|
276
|
+
},
|
|
277
|
+
reset(nextValues) {
|
|
278
|
+
currentForm().reset(nextValues);
|
|
279
|
+
},
|
|
280
|
+
async setValue(path, value) {
|
|
281
|
+
await currentForm().setValue(path, value);
|
|
282
|
+
},
|
|
283
|
+
applyServerState(result) {
|
|
284
|
+
return currentForm().applyServerState(result);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
69
287
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { HoloAdapterProjectAccessors } from '@holo-js/core'
|
|
2
|
+
|
|
1
3
|
export interface HoloRuntimeConnection {
|
|
2
4
|
driver?: 'sqlite' | 'postgres' | 'mysql'
|
|
3
5
|
url?: string
|
|
@@ -23,6 +25,7 @@ export interface HoloRuntimeDefaultConnection extends HoloRuntimeConnection {
|
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
|
|
28
|
+
export declare const holo: HoloAdapterProjectAccessors
|
|
26
29
|
export declare function useHoloDb(): HoloRuntimeDatabaseGroup | HoloRuntimeDefaultConnection
|
|
27
30
|
export declare function useHoloEnv(): 'production' | 'development' | 'test'
|
|
28
31
|
export declare function useHoloDebug(): boolean
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createHoloProjectAccessors,
|
|
3
3
|
initializeHoloAdapterProject
|
|
4
4
|
} from "@holo-js/core";
|
|
5
|
+
import { createNuxtAuthorizationError } from "../authorization-error.js";
|
|
5
6
|
export function configureHoloRuntimeConfig(config) {
|
|
6
7
|
const runtimeGlobals = globalThis;
|
|
7
8
|
runtimeGlobals.__holoRuntimeConfig = config;
|
|
@@ -26,12 +27,115 @@ function resolveRuntimeEnvName(env) {
|
|
|
26
27
|
function resolveRuntimeProjectRoot(config) {
|
|
27
28
|
return config.holo.projectRoot?.trim() || process.cwd();
|
|
28
29
|
}
|
|
30
|
+
async function loadNitroContextModule() {
|
|
31
|
+
return await import("nitropack/runtime/context");
|
|
32
|
+
}
|
|
33
|
+
export function createNuxtAuthRequestAccessors() {
|
|
34
|
+
function safeDecode(value) {
|
|
35
|
+
try {
|
|
36
|
+
return decodeURIComponent(value);
|
|
37
|
+
} catch {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function readHeader(name) {
|
|
42
|
+
const nitroContext = await loadNitroContextModule();
|
|
43
|
+
const event = nitroContext.useEvent();
|
|
44
|
+
if (!event) {
|
|
45
|
+
return void 0;
|
|
46
|
+
}
|
|
47
|
+
const normalizedName = name.toLowerCase();
|
|
48
|
+
if (event.headers instanceof Headers) {
|
|
49
|
+
return event.headers.get(name) ?? void 0;
|
|
50
|
+
}
|
|
51
|
+
if (event.request?.headers instanceof Headers) {
|
|
52
|
+
return event.request.headers.get(name) ?? void 0;
|
|
53
|
+
}
|
|
54
|
+
const value = event.node?.req?.headers?.[normalizedName];
|
|
55
|
+
if (Array.isArray(value)) {
|
|
56
|
+
return value[0];
|
|
57
|
+
}
|
|
58
|
+
return typeof value === "string" ? value : void 0;
|
|
59
|
+
}
|
|
60
|
+
async function readCookie(name) {
|
|
61
|
+
const header = await readHeader("cookie");
|
|
62
|
+
if (!header) {
|
|
63
|
+
return void 0;
|
|
64
|
+
}
|
|
65
|
+
for (const segment of header.split(";")) {
|
|
66
|
+
const trimmed = segment.trim();
|
|
67
|
+
const separator = trimmed.indexOf("=");
|
|
68
|
+
if (separator <= 0) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const key = safeDecode(trimmed.slice(0, separator));
|
|
72
|
+
if (typeof key === "undefined") {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (key !== name) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const value = safeDecode(trimmed.slice(separator + 1));
|
|
79
|
+
if (typeof value === "undefined") {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
return void 0;
|
|
85
|
+
}
|
|
86
|
+
async function appendCookie(cookie) {
|
|
87
|
+
const nitroContext = await loadNitroContextModule();
|
|
88
|
+
const event = nitroContext.useEvent();
|
|
89
|
+
const response = event?.node?.res;
|
|
90
|
+
if (!response) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const current = response.getHeader("set-cookie");
|
|
94
|
+
if (Array.isArray(current)) {
|
|
95
|
+
response.setHeader("set-cookie", [...current, cookie]);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (typeof current === "string") {
|
|
99
|
+
response.setHeader("set-cookie", [current, cookie]);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
response.setHeader("set-cookie", [cookie]);
|
|
103
|
+
}
|
|
104
|
+
const getCookieValue = async (name) => {
|
|
105
|
+
return await readCookie(name);
|
|
106
|
+
};
|
|
107
|
+
const getHeaderValue = async (name) => {
|
|
108
|
+
return await readHeader(name);
|
|
109
|
+
};
|
|
110
|
+
const appendResponseCookie = async (cookie) => {
|
|
111
|
+
await appendCookie(cookie);
|
|
112
|
+
};
|
|
113
|
+
const redirectResponse = async (url, status) => {
|
|
114
|
+
const nitroContext = await loadNitroContextModule();
|
|
115
|
+
const event = nitroContext.useEvent();
|
|
116
|
+
if (!event) {
|
|
117
|
+
throw new TypeError("Holo Nuxt auth redirect requires an active Nitro event.");
|
|
118
|
+
}
|
|
119
|
+
const { sendRedirect } = await import("h3");
|
|
120
|
+
await sendRedirect(event, url, status);
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
getCookie: getCookieValue,
|
|
124
|
+
getHeader: getHeaderValue,
|
|
125
|
+
appendResponseCookie,
|
|
126
|
+
redirectResponse
|
|
127
|
+
};
|
|
128
|
+
}
|
|
29
129
|
export const holo = createHoloProjectAccessors(async () => {
|
|
30
130
|
const config = getRuntimeConfig();
|
|
31
131
|
return initializeHoloAdapterProject(resolveRuntimeProjectRoot(config), {
|
|
32
132
|
envName: resolveRuntimeEnvName(config.holo.appEnv),
|
|
33
133
|
preferCache: process.env.NODE_ENV === "production",
|
|
34
|
-
processEnv: process.env
|
|
134
|
+
processEnv: process.env,
|
|
135
|
+
authRequest: createNuxtAuthRequestAccessors(),
|
|
136
|
+
authorizationError: {
|
|
137
|
+
createError: createNuxtAuthorizationError
|
|
138
|
+
}
|
|
35
139
|
});
|
|
36
140
|
});
|
|
37
141
|
function resolveDefaultConnectionName(group) {
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { defineNitroPlugin } from "nitropack/runtime/plugin";
|
|
2
|
+
import {
|
|
3
|
+
applyFormFailureRedirect,
|
|
4
|
+
getFormFailurePayload,
|
|
5
|
+
shouldRedirectFormFailure
|
|
6
|
+
} from "../server/form-failure.js";
|
|
7
|
+
export default defineNitroPlugin((nitroApp) => {
|
|
8
|
+
nitroApp.hooks.hook("beforeResponse", (event, response) => {
|
|
9
|
+
if (!shouldRedirectFormFailure(event)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const failure = getFormFailurePayload(response.body);
|
|
13
|
+
if (!failure) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
applyFormFailureRedirect(event, response, failure);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createNuxtAuthRequestAccessors,
|
|
2
3
|
configureHoloRuntimeConfig
|
|
3
4
|
} from "../composables/index.js";
|
|
5
|
+
import { createNuxtAuthorizationError } from "../authorization-error.js";
|
|
4
6
|
import { initializeHoloAdapterProject } from "@holo-js/core";
|
|
5
7
|
import { useRuntimeConfig } from "nitropack/runtime/config";
|
|
6
8
|
import { defineNitroPlugin } from "nitropack/runtime/plugin";
|
|
@@ -10,7 +12,11 @@ export default defineNitroPlugin(async (nitroApp) => {
|
|
|
10
12
|
const project = await initializeHoloAdapterProject(config.holo.projectRoot?.trim() || process.cwd(), {
|
|
11
13
|
envName: config.holo.appEnv,
|
|
12
14
|
preferCache: process.env.NODE_ENV === "production",
|
|
13
|
-
processEnv: process.env
|
|
15
|
+
processEnv: process.env,
|
|
16
|
+
authRequest: createNuxtAuthRequestAccessors(),
|
|
17
|
+
authorizationError: {
|
|
18
|
+
createError: createNuxtAuthorizationError
|
|
19
|
+
}
|
|
14
20
|
});
|
|
15
21
|
const driver = project.runtime.manager.connection().getDriver();
|
|
16
22
|
console.log(`\u2705 Holo DB connected (${driver})`);
|
|
File without changes
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
holo as baseHolo,
|
|
3
|
+
useHoloDb as baseUseHoloDb,
|
|
4
|
+
useHoloDebug as baseUseHoloDebug,
|
|
5
|
+
useHoloEnv as baseUseHoloEnv
|
|
6
|
+
} from "../../composables/index.js";
|
|
7
|
+
export const holo = baseHolo;
|
|
8
|
+
export const useHoloDb = baseUseHoloDb;
|
|
9
|
+
export const useHoloDebug = baseUseHoloDebug;
|
|
10
|
+
export const useHoloEnv = baseUseHoloEnv;
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { send, setHeader, setResponseStatus } from "h3";
|
|
2
|
+
import { isValidationException } from "@holo-js/validation";
|
|
3
|
+
import {
|
|
4
|
+
applyFormFailureRedirect,
|
|
5
|
+
shouldRedirectFormFailure
|
|
6
|
+
} from "./form-failure.js";
|
|
7
|
+
function findValidationException(error) {
|
|
8
|
+
if (isValidationException(error)) {
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
if (!error || typeof error !== "object" || !("cause" in error)) {
|
|
12
|
+
return void 0;
|
|
13
|
+
}
|
|
14
|
+
return findValidationException(error.cause);
|
|
15
|
+
}
|
|
16
|
+
export default defineNitroErrorHandler(async (error, event) => {
|
|
17
|
+
const validationError = findValidationException(error);
|
|
18
|
+
if (!validationError) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const payload = validationError.toJSON();
|
|
22
|
+
if (shouldRedirectFormFailure(event)) {
|
|
23
|
+
const failure = { ...payload };
|
|
24
|
+
const response = {};
|
|
25
|
+
applyFormFailureRedirect(event, response, failure);
|
|
26
|
+
await send(event, "", "text/html");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setResponseStatus(event, payload.status);
|
|
30
|
+
setHeader(event, "content-type", "application/json; charset=utf-8");
|
|
31
|
+
await send(event, JSON.stringify(payload), "application/json");
|
|
32
|
+
});
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const FORM_FAILURE_COOKIE = "holo_form_failure";
|
|
2
|
+
function getHeader(event, name) {
|
|
3
|
+
const value = event.node?.req?.headers?.[name.toLowerCase()];
|
|
4
|
+
return Array.isArray(value) ? value[0] : value;
|
|
5
|
+
}
|
|
6
|
+
function isFormFailurePayload(value) {
|
|
7
|
+
if (!value || typeof value !== "object") {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
const payload = value;
|
|
11
|
+
return payload.ok === false && payload.valid === false && typeof payload.status === "number" && !!payload.errors && typeof payload.errors === "object";
|
|
12
|
+
}
|
|
13
|
+
export function getFormFailurePayload(value) {
|
|
14
|
+
if (isFormFailurePayload(value)) {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
if (typeof value !== "string") {
|
|
18
|
+
return void 0;
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = JSON.parse(value);
|
|
22
|
+
return isFormFailurePayload(parsed) ? parsed : void 0;
|
|
23
|
+
} catch {
|
|
24
|
+
return void 0;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function acceptsHtml(event) {
|
|
28
|
+
return getHeader(event, "accept")?.toLowerCase().includes("text/html") === true;
|
|
29
|
+
}
|
|
30
|
+
function isUnsafeMethod(event) {
|
|
31
|
+
const method = event.node?.req?.method?.toUpperCase();
|
|
32
|
+
return method !== "GET" && method !== "HEAD";
|
|
33
|
+
}
|
|
34
|
+
function isApiRequest(event) {
|
|
35
|
+
const path = event.path ?? event.node?.req?.url ?? "";
|
|
36
|
+
return path.startsWith("/api/");
|
|
37
|
+
}
|
|
38
|
+
function getRedirectLocation(event) {
|
|
39
|
+
const referer = getHeader(event, "referer");
|
|
40
|
+
const host = getHeader(event, "host") ?? "localhost";
|
|
41
|
+
if (referer) {
|
|
42
|
+
try {
|
|
43
|
+
const url = new URL(referer);
|
|
44
|
+
if (url.host === host) {
|
|
45
|
+
return `${url.pathname}${url.search}`;
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return event.path ?? event.node?.req?.url ?? "/";
|
|
51
|
+
}
|
|
52
|
+
function appendSetCookie(event, response, cookie) {
|
|
53
|
+
const responseCookie = event.node?.res?.getHeader?.("set-cookie");
|
|
54
|
+
if (Array.isArray(responseCookie)) {
|
|
55
|
+
event.node?.res?.setHeader?.("set-cookie", [...responseCookie, cookie]);
|
|
56
|
+
} else if (responseCookie) {
|
|
57
|
+
event.node?.res?.setHeader?.("set-cookie", [String(responseCookie), cookie]);
|
|
58
|
+
} else {
|
|
59
|
+
event.node?.res?.setHeader?.("set-cookie", cookie);
|
|
60
|
+
}
|
|
61
|
+
if (response.headers instanceof Headers) {
|
|
62
|
+
response.headers.append("set-cookie", cookie);
|
|
63
|
+
} else {
|
|
64
|
+
response.headers ??= {};
|
|
65
|
+
const current = response.headers["set-cookie"];
|
|
66
|
+
if (Array.isArray(current)) {
|
|
67
|
+
response.headers["set-cookie"] = [...current.map(String), cookie];
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
response.headers["set-cookie"] = current ? [String(current), cookie] : cookie;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function setHeader(event, response, name, value) {
|
|
74
|
+
event.node?.res?.setHeader?.(name, value);
|
|
75
|
+
if (response.headers instanceof Headers) {
|
|
76
|
+
response.headers.set(name, value);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
response.headers ??= {};
|
|
80
|
+
response.headers[name] = value;
|
|
81
|
+
}
|
|
82
|
+
function serializeFormFailureCookie(payload) {
|
|
83
|
+
const encoded = encodeURIComponent(JSON.stringify(payload));
|
|
84
|
+
return `${FORM_FAILURE_COOKIE}=${encoded}; Path=/; Max-Age=60; SameSite=Lax`;
|
|
85
|
+
}
|
|
86
|
+
export function shouldRedirectFormFailure(event) {
|
|
87
|
+
return isUnsafeMethod(event) && acceptsHtml(event) && !isApiRequest(event);
|
|
88
|
+
}
|
|
89
|
+
export function applyFormFailureRedirect(event, response, failure) {
|
|
90
|
+
if (event.node?.res) {
|
|
91
|
+
event.node.res.statusCode = 303;
|
|
92
|
+
event.node.res.statusMessage = "See Other";
|
|
93
|
+
}
|
|
94
|
+
response.statusCode = 303;
|
|
95
|
+
response.statusMessage = "See Other";
|
|
96
|
+
response.body = "";
|
|
97
|
+
setHeader(event, response, "location", getRedirectLocation(event));
|
|
98
|
+
setHeader(event, response, "content-type", "text/html; charset=utf-8");
|
|
99
|
+
appendSetCookie(event, response, serializeFormFailureCookie(failure));
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { holo, useHoloDb, useHoloDebug, useHoloEnv } from '../../composables'
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
holo,
|
|
3
|
-
useHoloDb,
|
|
4
|
-
useHoloDebug,
|
|
5
|
-
useHoloEnv
|
|
1
|
+
import {
|
|
2
|
+
holo as baseHolo,
|
|
3
|
+
useHoloDb as baseUseHoloDb,
|
|
4
|
+
useHoloDebug as baseUseHoloDebug,
|
|
5
|
+
useHoloEnv as baseUseHoloEnv
|
|
6
6
|
} from "../../composables/index.js";
|
|
7
|
+
export const holo = baseHolo;
|
|
8
|
+
export const useHoloDb = baseUseHoloDb;
|
|
9
|
+
export const useHoloDebug = baseUseHoloDebug;
|
|
10
|
+
export const useHoloEnv = baseUseHoloEnv;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Storage, useStorage } from '../utils/storage'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { open, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { extname, resolve, sep } from "node:path";
|
|
3
3
|
import { useRuntimeConfig } from "#imports";
|
|
4
4
|
const NAMED_PUBLIC_DISK_ROUTE_SEGMENT = "__holo";
|
|
@@ -70,6 +70,27 @@ function resolveContentType(absolutePath) {
|
|
|
70
70
|
return "application/octet-stream";
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
function isActiveContentPath(absolutePath) {
|
|
74
|
+
switch (extname(absolutePath).toLowerCase()) {
|
|
75
|
+
case ".html":
|
|
76
|
+
case ".js":
|
|
77
|
+
case ".mjs":
|
|
78
|
+
case ".svg":
|
|
79
|
+
return true;
|
|
80
|
+
default:
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function setStorageResponseHeaders(event, absolutePath) {
|
|
85
|
+
if (isActiveContentPath(absolutePath)) {
|
|
86
|
+
setResponseHeader(event, "content-disposition", "attachment");
|
|
87
|
+
setResponseHeader(event, "content-type", "application/octet-stream");
|
|
88
|
+
setResponseHeader(event, "x-content-type-options", "nosniff");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
setResponseHeader(event, "content-type", resolveContentType(absolutePath));
|
|
92
|
+
setResponseHeader(event, "x-content-type-options", "nosniff");
|
|
93
|
+
}
|
|
73
94
|
function createMissingFileError(message) {
|
|
74
95
|
const error = new Error(message);
|
|
75
96
|
error.code = "ENOENT";
|
|
@@ -78,15 +99,28 @@ function createMissingFileError(message) {
|
|
|
78
99
|
function isPathWithinRoot(root, absolutePath) {
|
|
79
100
|
return absolutePath === root || absolutePath.startsWith(`${root}${sep}`);
|
|
80
101
|
}
|
|
102
|
+
function isSameFile(left, right) {
|
|
103
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
104
|
+
}
|
|
81
105
|
async function readPublicFile(event, disk, absolutePath) {
|
|
82
106
|
const resolvedRoot = await realpath(resolve(disk.root));
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
107
|
+
const file = await open(absolutePath, "r");
|
|
108
|
+
try {
|
|
109
|
+
const openedFile = await file.stat();
|
|
110
|
+
const resolvedPath = await realpath(absolutePath);
|
|
111
|
+
if (!isPathWithinRoot(resolvedRoot, resolvedPath)) {
|
|
112
|
+
throw createMissingFileError("Storage file not found.");
|
|
113
|
+
}
|
|
114
|
+
const resolvedFile = await stat(resolvedPath);
|
|
115
|
+
if (!isSameFile(openedFile, resolvedFile)) {
|
|
116
|
+
throw createMissingFileError("Storage file not found.");
|
|
117
|
+
}
|
|
118
|
+
const contents = await file.readFile();
|
|
119
|
+
setStorageResponseHeaders(event, absolutePath);
|
|
120
|
+
return contents;
|
|
121
|
+
} finally {
|
|
122
|
+
await file.close();
|
|
86
123
|
}
|
|
87
|
-
const contents = await readFile(absolutePath);
|
|
88
|
-
setResponseHeader(event, "content-type", resolveContentType(absolutePath));
|
|
89
|
-
return contents;
|
|
90
124
|
}
|
|
91
125
|
function resolveRouteSegments(routePath) {
|
|
92
126
|
const segments = normalizeRequestPath(routePath);
|
package/dist/runtime/shims.d.ts
CHANGED
|
@@ -34,8 +34,15 @@ interface HoloRuntimeConfig extends RuntimeConfigInput {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Minimal Nuxt runtime shims for adapter typechecking.
|
|
39
|
+
*
|
|
40
|
+
* Keep these declarations limited to the fields consumed by adapter runtime code:
|
|
41
|
+
* runtime config, storage access, and Nitro route/plugin globals.
|
|
42
|
+
*/
|
|
37
43
|
declare module '#app' {
|
|
38
44
|
export function useRuntimeConfig(): HoloRuntimeConfig
|
|
45
|
+
export function useCookie<T = string | null>(name: string): { value: T | null | undefined }
|
|
39
46
|
}
|
|
40
47
|
|
|
41
48
|
declare module '#imports' {
|
|
@@ -47,6 +54,10 @@ declare module 'nitropack/runtime/config' {
|
|
|
47
54
|
export function useRuntimeConfig(): HoloRuntimeConfig
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
declare module 'nitropack/runtime/context' {
|
|
58
|
+
export function useEvent(): unknown
|
|
59
|
+
}
|
|
60
|
+
|
|
50
61
|
declare module 'nitropack/runtime/plugin' {
|
|
51
62
|
export function defineNitroPlugin<T>(plugin: T): T
|
|
52
63
|
}
|
|
@@ -57,6 +68,7 @@ declare module 'nitropack/runtime/storage' {
|
|
|
57
68
|
|
|
58
69
|
declare global {
|
|
59
70
|
function createError(input: { statusCode: number, statusMessage: string }): Error
|
|
71
|
+
function defineNitroErrorHandler<T>(handler: T): T
|
|
60
72
|
function defineNitroPlugin<T>(plugin: T): T
|
|
61
73
|
function defineEventHandler<T>(
|
|
62
74
|
handler: (event: unknown) => T | Promise<T>,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@holo-js/adapter-nuxt",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Holo-JS Framework - Nuxt adapter",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,15 +34,17 @@
|
|
|
34
34
|
"test": "vitest --run"
|
|
35
35
|
},
|
|
36
36
|
"dependencies": {
|
|
37
|
-
"@nuxt/kit": "^4.
|
|
38
|
-
"@holo-js/config": "^0.1.
|
|
39
|
-
"@holo-js/core": "^0.1.
|
|
40
|
-
"@holo-js/db": "^0.1.
|
|
37
|
+
"@nuxt/kit": "^4.4.4",
|
|
38
|
+
"@holo-js/config": "^0.1.6",
|
|
39
|
+
"@holo-js/core": "^0.1.6",
|
|
40
|
+
"@holo-js/db": "^0.1.6",
|
|
41
|
+
"@holo-js/validation": "^0.1.6",
|
|
42
|
+
"h3": "^1.15.11"
|
|
41
43
|
},
|
|
42
44
|
"peerDependencies": {
|
|
43
|
-
"@holo-js/forms": "^0.1.
|
|
44
|
-
"@holo-js/storage": "^0.1.
|
|
45
|
-
"@holo-js/storage-s3": "^0.1.
|
|
45
|
+
"@holo-js/forms": "^0.1.6",
|
|
46
|
+
"@holo-js/storage": "^0.1.6",
|
|
47
|
+
"@holo-js/storage-s3": "^0.1.6"
|
|
46
48
|
},
|
|
47
49
|
"peerDependenciesMeta": {
|
|
48
50
|
"@holo-js/forms": {
|
|
@@ -56,11 +58,11 @@
|
|
|
56
58
|
}
|
|
57
59
|
},
|
|
58
60
|
"devDependencies": {
|
|
59
|
-
"@holo-js/storage-s3": "
|
|
61
|
+
"@holo-js/storage-s3": "^0.1.6",
|
|
60
62
|
"@nuxt/module-builder": "^1.0.2",
|
|
61
63
|
"@types/node": "^22.10.2",
|
|
62
|
-
"nuxt": "^4.
|
|
64
|
+
"nuxt": "^4.4.4",
|
|
63
65
|
"typescript": "^5.7.2",
|
|
64
|
-
"vitest": "^
|
|
66
|
+
"vitest": "^4.1.5"
|
|
65
67
|
}
|
|
66
68
|
}
|