@cyberskill/shared 3.13.0 → 3.15.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/config/env/env.util.d.ts +5 -0
- package/dist/config/env/env.util.js +20 -16
- package/dist/config/env/env.util.js.map +1 -1
- package/dist/config/env/index.js +2 -2
- package/dist/config/vitest/vitest.e2e.js +4 -4
- package/dist/config/vitest/vitest.e2e.js.map +1 -1
- package/dist/config/vitest/vitest.unit.js +5 -5
- package/dist/config/vitest/vitest.unit.js.map +1 -1
- package/dist/config/vitest/vitest.unit.setup.js +10 -0
- package/dist/config/vitest/vitest.unit.setup.js.map +1 -0
- package/dist/node/apollo-server/apollo-server.type.d.ts +13 -1
- package/dist/node/apollo-server/apollo-server.util.d.ts +1 -0
- package/dist/node/apollo-server/apollo-server.util.js +40 -16
- package/dist/node/apollo-server/apollo-server.util.js.map +1 -1
- package/dist/node/cli/index.js +26 -28
- package/dist/node/cli/index.js.map +1 -1
- package/dist/node/command/command.util.d.ts +5 -0
- package/dist/node/command/command.util.js +49 -48
- package/dist/node/command/command.util.js.map +1 -1
- package/dist/node/command/index.js +2 -2
- package/dist/node/express/express.type.d.ts +11 -0
- package/dist/node/express/express.util.d.ts +34 -6
- package/dist/node/express/express.util.js +81 -56
- package/dist/node/express/express.util.js.map +1 -1
- package/dist/node/express/index.js +2 -2
- package/dist/node/log/log.type.d.ts +17 -0
- package/dist/node/log/log.type.js.map +1 -1
- package/dist/node/log/log.util.js +25 -11
- package/dist/node/log/log.util.js.map +1 -1
- package/dist/node/mongo/index.d.ts +2 -1
- package/dist/node/mongo/index.js +7 -8
- package/dist/node/mongo/mongo.constant.d.ts +5 -0
- package/dist/node/mongo/mongo.constant.js +2 -2
- package/dist/node/mongo/mongo.constant.js.map +1 -1
- package/dist/node/mongo/mongo.controller.mongoose.d.ts +3 -0
- package/dist/node/mongo/mongo.controller.mongoose.js +41 -55
- package/dist/node/mongo/mongo.controller.mongoose.js.map +1 -1
- package/dist/node/mongo/mongo.controller.native.d.ts +29 -2
- package/dist/node/mongo/mongo.controller.native.js +31 -14
- package/dist/node/mongo/mongo.controller.native.js.map +1 -1
- package/dist/node/mongo/mongo.type.d.ts +3 -1
- package/dist/node/mongo/mongo.util.d.ts +1 -0
- package/dist/node/mongo/mongo.util.js +38 -17
- package/dist/node/mongo/mongo.util.js.map +1 -1
- package/dist/node/package/package.util.js +47 -47
- package/dist/node/path/index.js +2 -2
- package/dist/node/path/path.constant.d.ts +4 -0
- package/dist/node/path/path.constant.js +75 -72
- package/dist/node/path/path.constant.js.map +1 -1
- package/dist/node/storage/storage.util.d.ts +50 -1
- package/dist/node/storage/storage.util.js +79 -54
- package/dist/node/storage/storage.util.js.map +1 -1
- package/dist/node/upload/upload.type.d.ts +2 -0
- package/dist/node/upload/upload.type.js.map +1 -1
- package/dist/node/upload/upload.util.d.ts +1 -0
- package/dist/node/upload/upload.util.js +62 -52
- package/dist/node/upload/upload.util.js.map +1 -1
- package/dist/node/ws/ws.util.d.ts +7 -0
- package/dist/node/ws/ws.util.js +20 -19
- package/dist/node/ws/ws.util.js.map +1 -1
- package/dist/react/apollo-client/apollo-client.component.js.map +1 -1
- package/dist/react/apollo-client/apollo-client.type.d.ts +2 -0
- package/dist/react/apollo-client/apollo-client.util.js +6 -6
- package/dist/react/apollo-client/apollo-client.util.js.map +1 -1
- package/dist/react/apollo-error/apollo-error.component.js +1 -1
- package/dist/react/apollo-error/apollo-error.component.js.map +1 -1
- package/dist/react/apollo-error/apollo-error.util.js.map +1 -1
- package/dist/react/i18next/i18next.server.d.ts +17 -0
- package/dist/react/i18next/i18next.server.js +9 -0
- package/dist/react/i18next/i18next.server.js.map +1 -0
- package/dist/react/next-intl/next-intl.hoc.d.ts +4 -8
- package/dist/react/next-intl/next-intl.hoc.js +14 -10
- package/dist/react/next-intl/next-intl.hoc.js.map +1 -1
- package/dist/react/next-intl/next-intl.server.d.ts +10 -0
- package/dist/react/next-intl/next-intl.server.js +7 -0
- package/dist/react/next-intl/next-intl.server.js.map +1 -0
- package/dist/react/storage/storage.util.d.ts +34 -1
- package/dist/react/storage/storage.util.js +30 -5
- package/dist/react/storage/storage.util.js.map +1 -1
- package/dist/react/userback/userback.component.js.map +1 -1
- package/dist/typescript/common.type.d.ts +4 -0
- package/dist/typescript/common.type.js +2 -2
- package/dist/typescript/common.type.js.map +1 -1
- package/dist/typescript/index.js +2 -2
- package/dist/util/object/object.util.js +29 -18
- package/dist/util/object/object.util.js.map +1 -1
- package/dist/util/serializer/serializer.util.d.ts +8 -0
- package/dist/util/serializer/serializer.util.js +51 -64
- package/dist/util/serializer/serializer.util.js.map +1 -1
- package/dist/util/storage/storage-envelope.d.ts +25 -0
- package/dist/util/storage/storage-envelope.js +18 -0
- package/dist/util/storage/storage-envelope.js.map +1 -0
- package/package.json +33 -12
- package/dist/node/mongo/mongo.type.js +0 -8
- package/dist/node/mongo/mongo.type.js.map +0 -1
- package/dist/node_modules/.pnpm/vitest@4.1.2_@types_node@25.5.0_jsdom@29.0.1_@noble_hashes@1.8.0__vite@8.0.3_@types_nod_0827261ede788764a5d99ac6bdf44bde/node_modules/vitest/dist/config.js +0 -8
- package/dist/node_modules/.pnpm/vitest@4.1.2_@types_node@25.5.0_jsdom@29.0.1_@noble_hashes@1.8.0__vite@8.0.3_@types_nod_0827261ede788764a5d99ac6bdf44bde/node_modules/vitest/dist/config.js.map +0 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { RESPONSE_STATUS as e } from "../../constant/response-status.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { log as t } from "../log/log.util.js";
|
|
3
|
+
import { E_UploadType as n } from "./upload.type.js";
|
|
4
|
+
import { BYTES_PER_MB as r, DEFAULT_UPLOAD_CONFIG as i } from "./upload.constant.js";
|
|
5
|
+
import { createWriteStream as a, mkdirSync as o, pathExistsSync as s } from "../fs/fs.util.js";
|
|
6
|
+
import { dirname as c } from "../path/path.util.js";
|
|
7
7
|
import l from "node:path";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { Buffer as u } from "node:buffer";
|
|
9
|
+
import { Transform as d } from "node:stream";
|
|
10
|
+
import { ReadableStream as f } from "node:stream/web";
|
|
10
11
|
//#region src/node/upload/upload.util.ts
|
|
11
|
-
async function
|
|
12
|
+
async function p(e) {
|
|
12
13
|
return new Promise((t, n) => {
|
|
13
14
|
let r = 0;
|
|
14
15
|
e.on("data", (e) => {
|
|
@@ -16,10 +17,11 @@ async function f(e) {
|
|
|
16
17
|
}), e.on("end", () => t(r)), e.on("error", n);
|
|
17
18
|
});
|
|
18
19
|
}
|
|
19
|
-
async function
|
|
20
|
-
let i = await (await n).file, a = await
|
|
20
|
+
async function m(t, n, r) {
|
|
21
|
+
let i = await (await n).file, a = await p(i.createReadStream()), o = r ?? v(), s = _({
|
|
21
22
|
filename: i.filename,
|
|
22
|
-
fileSize: a
|
|
23
|
+
fileSize: a,
|
|
24
|
+
mimetype: i.mimetype
|
|
23
25
|
}, o, t);
|
|
24
26
|
return s.isValid ? {
|
|
25
27
|
success: !0,
|
|
@@ -31,59 +33,63 @@ async function p(t, n, r) {
|
|
|
31
33
|
code: e.BAD_REQUEST.CODE
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
|
-
async function
|
|
35
|
-
let i = (
|
|
36
|
+
async function h(e, t, n) {
|
|
37
|
+
let i = (n ?? v())[e], a = await m(e, t, n);
|
|
36
38
|
if (!a.success) return a;
|
|
37
|
-
let { createReadStream: o } = a.result, s = i.sizeLimit,
|
|
38
|
-
s -= e.length, s < 0 ?
|
|
39
|
-
} }),
|
|
39
|
+
let { createReadStream: o } = a.result, s = i.sizeLimit, c = new d({ transform(e, t, n) {
|
|
40
|
+
s -= e.length, s < 0 ? n(/* @__PURE__ */ Error(`File size exceeds limit of ${i.sizeLimit / r}MB`)) : n(null, e);
|
|
41
|
+
} }), l = o().pipe(c);
|
|
40
42
|
return {
|
|
41
43
|
success: !0,
|
|
42
|
-
result: new
|
|
43
|
-
|
|
44
|
-
e.enqueue(typeof t == "string" ?
|
|
45
|
-
}),
|
|
44
|
+
result: new f({ start(e) {
|
|
45
|
+
l.on("data", (t) => {
|
|
46
|
+
e.enqueue(typeof t == "string" ? u.from(t) : t);
|
|
47
|
+
}), l.on("end", () => e.close()), l.on("error", (t) => e.error(t));
|
|
46
48
|
} })
|
|
47
49
|
};
|
|
48
50
|
}
|
|
49
|
-
function
|
|
51
|
+
function g(e, t) {
|
|
50
52
|
let n = e.lastIndexOf(".");
|
|
51
53
|
if (n === -1) return !1;
|
|
52
54
|
let r = e.substring(n + 1).toLowerCase();
|
|
53
55
|
return t.includes(r);
|
|
54
56
|
}
|
|
55
|
-
function
|
|
56
|
-
let { filename:
|
|
57
|
-
if (!
|
|
57
|
+
function _(e, r, i) {
|
|
58
|
+
let { filename: a, fileSize: o, mimetype: s } = e, { allowedExtensions: c, sizeLimit: l } = r[i];
|
|
59
|
+
if (!g(a, c)) return {
|
|
58
60
|
isValid: !1,
|
|
59
|
-
error: `File extension not allowed for ${
|
|
61
|
+
error: `File extension not allowed for ${i.toLowerCase()} files. Allowed extensions: ${c.join(", ")}`
|
|
60
62
|
};
|
|
61
|
-
if (
|
|
62
|
-
let e = Math.round(
|
|
63
|
+
if (o !== void 0 && o > l) {
|
|
64
|
+
let e = Math.round(l / (1024 * 1024));
|
|
63
65
|
return {
|
|
64
66
|
isValid: !1,
|
|
65
|
-
error: `File size exceeds limit for ${
|
|
67
|
+
error: `File size exceeds limit for ${i.toLowerCase()} files. Maximum size: ${e}MB`
|
|
66
68
|
};
|
|
67
69
|
}
|
|
70
|
+
if (s) {
|
|
71
|
+
let e = "";
|
|
72
|
+
i === n.IMAGE ? e = "image/" : i === n.VIDEO ? e = "video/" : i === n.AUDIO && (e = "audio/"), e && !s.startsWith(e) && t.warn(`Advisory Mimetype Warning: File '${a}' (type: ${i}) has unexpected mimetype '${s}'. Expected prefix: '${e}'`);
|
|
73
|
+
}
|
|
68
74
|
return { isValid: !0 };
|
|
69
75
|
}
|
|
70
|
-
function
|
|
76
|
+
function v(e) {
|
|
71
77
|
return {
|
|
72
|
-
...
|
|
78
|
+
...i,
|
|
73
79
|
...e
|
|
74
80
|
};
|
|
75
81
|
}
|
|
76
|
-
async function
|
|
77
|
-
let { path: r, file:
|
|
82
|
+
async function y(t) {
|
|
83
|
+
let { path: r, file: i, config: u, type: d, baseDir: f } = t;
|
|
78
84
|
if (!r || typeof r != "string") return {
|
|
79
85
|
success: !1,
|
|
80
86
|
message: "Invalid path provided",
|
|
81
87
|
code: e.BAD_REQUEST.CODE
|
|
82
88
|
};
|
|
83
|
-
let
|
|
89
|
+
let p = l.resolve(r);
|
|
84
90
|
if (f) {
|
|
85
91
|
let t = l.resolve(f) + l.sep;
|
|
86
|
-
if (!
|
|
92
|
+
if (!p.startsWith(t) && p !== l.resolve(f)) return {
|
|
87
93
|
success: !1,
|
|
88
94
|
message: "Path traversal detected: path resolves outside the allowed base directory",
|
|
89
95
|
code: e.BAD_REQUEST.CODE
|
|
@@ -93,39 +99,43 @@ async function v(n) {
|
|
|
93
99
|
message: "Path traversal detected: \"..\" segments are not allowed",
|
|
94
100
|
code: e.BAD_REQUEST.CODE
|
|
95
101
|
};
|
|
96
|
-
if (!
|
|
102
|
+
if (!i || typeof i != "object") return {
|
|
97
103
|
success: !1,
|
|
98
104
|
message: "Invalid file provided",
|
|
99
105
|
code: e.BAD_REQUEST.CODE
|
|
100
106
|
};
|
|
101
107
|
if (u) {
|
|
102
|
-
let
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
let t = [
|
|
109
|
+
n.IMAGE,
|
|
110
|
+
n.VIDEO,
|
|
111
|
+
n.DOCUMENT,
|
|
112
|
+
n.OTHER
|
|
107
113
|
];
|
|
108
|
-
for (let
|
|
109
|
-
if (!u[
|
|
114
|
+
for (let n of t) {
|
|
115
|
+
if (!u[n] || !Array.isArray(u[n].allowedExtensions) || u[n].allowedExtensions.length === 0) return {
|
|
110
116
|
success: !1,
|
|
111
|
-
message: `Invalid config for ${
|
|
117
|
+
message: `Invalid config for ${n.toLowerCase()} files`,
|
|
112
118
|
code: e.BAD_REQUEST.CODE
|
|
113
119
|
};
|
|
114
|
-
if (typeof u[
|
|
120
|
+
if (typeof u[n].sizeLimit != "number" || u[n].sizeLimit <= 0) return {
|
|
115
121
|
success: !1,
|
|
116
|
-
message: `Invalid size limit for ${
|
|
122
|
+
message: `Invalid size limit for ${n.toLowerCase()} files`,
|
|
117
123
|
code: e.BAD_REQUEST.CODE
|
|
118
124
|
};
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
try {
|
|
122
|
-
let t = await
|
|
128
|
+
let t = await m(d, await i, u);
|
|
123
129
|
if (!t.success) return t;
|
|
124
|
-
let { createReadStream: n } = t.result, l =
|
|
125
|
-
|
|
126
|
-
let f = n(),
|
|
127
|
-
return f.pipe(
|
|
128
|
-
|
|
130
|
+
let { createReadStream: n } = t.result, l = c(r);
|
|
131
|
+
s(l) || o(l, { recursive: !0 });
|
|
132
|
+
let f = n(), p = a(r);
|
|
133
|
+
return f.pipe(p), await new Promise((e, t) => {
|
|
134
|
+
p.on("finish", () => e()), p.on("error", (e) => {
|
|
135
|
+
"destroy" in f && typeof f.destroy == "function" && f.destroy(), t(e);
|
|
136
|
+
}), f.on("error", (e) => {
|
|
137
|
+
p.destroy(), t(e);
|
|
138
|
+
});
|
|
129
139
|
}), {
|
|
130
140
|
success: !0,
|
|
131
141
|
result: r,
|
|
@@ -141,6 +151,6 @@ async function v(n) {
|
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
//#endregion
|
|
144
|
-
export {
|
|
154
|
+
export { v as createUploadConfig, m as getAndValidateFile, p as getFileSizeFromStream, h as getFileWebStream, y as upload, g as validateFileExtension, _ as validateUpload };
|
|
145
155
|
|
|
146
156
|
//# sourceMappingURL=upload.util.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport nodePath from 'node:path';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type, baseDir } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n // Security: Validate path is within allowed base directory to prevent path traversal.\n // When baseDir is provided, the resolved path must start with it.\n // When baseDir is not provided, reject any path containing \"..\" segments.\n const resolvedPath = nodePath.resolve(path);\n\n if (baseDir) {\n const resolvedBase = nodePath.resolve(baseDir) + nodePath.sep;\n if (!resolvedPath.startsWith(resolvedBase) && resolvedPath !== nodePath.resolve(baseDir)) {\n return {\n success: false,\n message: 'Path traversal detected: path resolves outside the allowed base directory',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n else if (path.includes('..')) {\n return {\n success: false,\n message: 'Path traversal detected: \"..\" segments are not allowed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const writeStream = createReadStream();\n const out = createWriteStream(path);\n writeStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', reject);\n writeStream.on('error', reject);\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"upload.util.js","names":[],"sources":["../../../src/node/upload/upload.util.ts"],"sourcesContent":["import { Buffer } from 'node:buffer';\nimport nodePath from 'node:path';\nimport { Transform } from 'node:stream';\nimport { ReadableStream } from 'node:stream/web';\n\nimport type { I_Return } from '#typescript/index.js';\n\nimport { RESPONSE_STATUS } from '#constant/index.js';\n\nimport type { I_UploadConfig, I_UploadFile, I_UploadFileData, I_UploadOptions, I_UploadTypeConfig, I_UploadValidationConfig } from './upload.type.js';\n\nimport { createWriteStream, mkdirSync, pathExistsSync } from '../fs/index.js';\nimport { log } from '../log/index.js';\nimport { dirname } from '../path/index.js';\nimport { BYTES_PER_MB, DEFAULT_UPLOAD_CONFIG } from './upload.constant.js';\nimport { E_UploadType } from './upload.type.js';\n\n/**\n * Calculates the size of a file from a readable stream.\n * This function reads through the entire stream to determine the total byte size\n * by accumulating the length of each data chunk.\n *\n * @param stream - The readable stream to calculate the size for.\n * @returns A promise that resolves to the total size of the stream in bytes.\n */\nexport async function getFileSizeFromStream(stream: NodeJS.ReadableStream): Promise<number> {\n return new Promise((resolve, reject) => {\n let size = 0;\n stream.on('data', (chunk) => {\n size += chunk.length;\n });\n stream.on('end', () => resolve(size));\n stream.on('error', reject);\n });\n}\n\n/**\n * Extracts and validates file data from an upload file.\n * This function processes upload files by:\n * - Extracting file metadata and creating a readable stream\n * - Calculating the file size from the stream\n * - Validating file size and extension against upload configuration\n * - Returning a standardized response with success status and error codes\n * - Providing validated file data for further processing\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing validated file data or error information.\n */\nexport async function getAndValidateFile(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<I_UploadFileData>> {\n const fileData = await (await file).file;\n const stream = fileData.createReadStream();\n // Stream is consumed here for validation; callers use createReadStream() again for the actual write.\n // This is intentional — createReadStream() is a factory that yields a new stream per call.\n const fileSize = await getFileSizeFromStream(stream);\n const uploadConfig = config ?? createUploadConfig();\n\n const validationResult = validateUpload(\n { filename: fileData.filename, fileSize, mimetype: fileData.mimetype },\n uploadConfig,\n type,\n );\n\n if (!validationResult.isValid) {\n return {\n success: false,\n message: validationResult.error || 'File validation failed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n return {\n success: true,\n result: fileData,\n message: 'File validated successfully',\n };\n}\n\n/**\n * Creates a validated web-readable stream from an upload file with size validation.\n * This function processes file uploads for web environments by:\n * - Validating file data using getAndValidateFile function\n * - Creating a size validation transform stream to monitor upload progress\n * - Returning a web-compatible ReadableStream with real-time validation\n * - Providing standardized error responses for validation failures\n * - Wrapping the stream in a standardized response format\n *\n * @param type - The type of upload being processed (IMAGE, VIDEO, DOCUMENT, OTHER).\n * @param file - The upload file object containing file metadata and stream creation method.\n * @param config - Optional upload configuration. If not provided, uses default configuration.\n * @returns A promise that resolves to a standardized response containing either a web ReadableStream or error information.\n */\nexport async function getFileWebStream(type: E_UploadType, file: I_UploadFile, config?: I_UploadConfig): Promise<I_Return<ReadableStream<Uint8Array>>> {\n const uploadConfig = config ?? createUploadConfig();\n const typeConfig = uploadConfig[type];\n\n const fileData = await getAndValidateFile(type, file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n let remainingBytes = typeConfig.sizeLimit;\n\n const sizeValidationStream = new Transform({\n transform(chunk: Buffer, _enc: BufferEncoding, cb) {\n remainingBytes -= chunk.length;\n\n if (remainingBytes < 0) {\n cb(new Error(`File size exceeds limit of ${typeConfig.sizeLimit / BYTES_PER_MB}MB`));\n }\n else {\n cb(null, chunk);\n }\n },\n });\n const originalStream = createReadStream();\n const validatedStream = originalStream.pipe(sizeValidationStream);\n\n return {\n success: true,\n result: new ReadableStream<Uint8Array>({\n start(controller) {\n validatedStream.on('data', (chunk: Buffer | string) => {\n controller.enqueue(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);\n });\n validatedStream.on('end', () => controller.close());\n validatedStream.on('error', (err: unknown) => controller.error(err));\n },\n }),\n };\n}\n\n/**\n * Validates if a file has an allowed extension.\n * This function extracts the file extension from the filename and checks if it's\n * included in the list of allowed extensions (case-insensitive comparison).\n *\n * @param filename - The filename to check for valid extension.\n * @param allowedExtensions - An array of allowed file extensions (without dots).\n * @returns True if the file extension is allowed, false otherwise.\n */\nexport function validateFileExtension(filename: string, allowedExtensions: string[]): boolean {\n const lastDotIndex = filename.lastIndexOf('.');\n\n if (lastDotIndex === -1) {\n return false;\n }\n\n const extension = filename.substring(lastDotIndex + 1).toLowerCase();\n\n return allowedExtensions.includes(extension);\n}\n\n/**\n * Validates an upload against the specified configuration.\n * This function performs comprehensive validation including:\n * - File extension validation against allowed extensions\n * - File size validation against size limits\n * - Returns detailed error messages for validation failures\n *\n * @param config - The validation configuration including filename and optional file size.\n * @param uploadConfig - The upload configuration containing allowed extensions and size limits.\n * @param uploadType - The type of upload being validated.\n * @returns An object indicating validation success and optional error message.\n */\nexport function validateUpload(\n config: I_UploadValidationConfig,\n uploadConfig: I_UploadConfig,\n uploadType: E_UploadType,\n): { isValid: boolean; error?: string } {\n const { filename, fileSize, mimetype } = config;\n const typeConfig: I_UploadTypeConfig = uploadConfig[uploadType];\n\n const { allowedExtensions, sizeLimit } = typeConfig;\n\n if (!validateFileExtension(filename, allowedExtensions)) {\n return {\n isValid: false,\n error: `File extension not allowed for ${uploadType.toLowerCase()} files. Allowed extensions: ${allowedExtensions.join(', ')}`,\n };\n }\n\n if (fileSize !== undefined && fileSize > sizeLimit) {\n const maxSizeMB = Math.round(sizeLimit / (1024 * 1024));\n\n return {\n isValid: false,\n error: `File size exceeds limit for ${uploadType.toLowerCase()} files. Maximum size: ${maxSizeMB}MB`,\n };\n }\n\n if (mimetype) {\n let expectedPrefix = '';\n\n if (uploadType === E_UploadType.IMAGE) {\n expectedPrefix = 'image/';\n }\n else if (uploadType === E_UploadType.VIDEO) {\n expectedPrefix = 'video/';\n }\n else if (uploadType === E_UploadType.AUDIO) {\n expectedPrefix = 'audio/';\n }\n\n if (expectedPrefix && !mimetype.startsWith(expectedPrefix)) {\n // Advisory MIME validation - log warning but DO NOT reject\n log.warn(`Advisory Mimetype Warning: File '${filename}' (type: ${uploadType}) has unexpected mimetype '${mimetype}'. Expected prefix: '${expectedPrefix}'`);\n }\n }\n\n return { isValid: true };\n}\n\n/**\n * Creates a default upload configuration with predefined settings for different file types.\n * This function provides sensible defaults for image, video, document, and other file types,\n * including allowed extensions and size limits. The configuration can be customized with overrides.\n *\n * @param overrides - Optional configuration overrides to merge with the default configuration.\n * @returns A complete upload configuration object with defaults and any provided overrides.\n * @since 3.13.0\n */\nexport function createUploadConfig(overrides?: Partial<I_UploadConfig>): I_UploadConfig {\n return { ...DEFAULT_UPLOAD_CONFIG, ...overrides };\n}\n\n/**\n * Uploads a file with comprehensive validation and error handling.\n * This function processes file uploads with the following features:\n * - Input validation for path and file parameters\n * - Configuration validation for all upload types\n * - File validation using getAndValidateFile function\n * - Automatic directory creation\n * - Stream-based file writing\n * - Comprehensive error handling with standardized response codes\n *\n * @param options - Upload configuration including file, path, type, and optional validation config.\n * @returns A promise that resolves to a standardized response with success status, message, file path, and response codes.\n */\nexport async function upload(options: I_UploadOptions): Promise<I_Return<string>> {\n const { path, file, config, type, baseDir } = options;\n\n if (!path || typeof path !== 'string') {\n return {\n success: false,\n message: 'Invalid path provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n // Security: Validate path is within allowed base directory to prevent path traversal.\n // When baseDir is provided, the resolved path must start with it.\n // When baseDir is not provided, reject any path containing \"..\" segments.\n const resolvedPath = nodePath.resolve(path);\n\n if (baseDir) {\n const resolvedBase = nodePath.resolve(baseDir) + nodePath.sep;\n if (!resolvedPath.startsWith(resolvedBase) && resolvedPath !== nodePath.resolve(baseDir)) {\n return {\n success: false,\n message: 'Path traversal detected: path resolves outside the allowed base directory',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n else if (path.includes('..')) {\n return {\n success: false,\n message: 'Path traversal detected: \"..\" segments are not allowed',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (!file || typeof file !== 'object') {\n return {\n success: false,\n message: 'Invalid file provided',\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n\n if (config) {\n const requiredTypes = [E_UploadType.IMAGE, E_UploadType.VIDEO, E_UploadType.DOCUMENT, E_UploadType.OTHER];\n\n for (const requiredType of requiredTypes) {\n if (!config[requiredType] || !Array.isArray(config[requiredType].allowedExtensions) || config[requiredType].allowedExtensions.length === 0) {\n return {\n success: false,\n message: `Invalid config for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n if (typeof config[requiredType].sizeLimit !== 'number' || config[requiredType].sizeLimit <= 0) {\n return {\n success: false,\n message: `Invalid size limit for ${requiredType.toLowerCase()} files`,\n code: RESPONSE_STATUS.BAD_REQUEST.CODE,\n };\n }\n }\n }\n\n try {\n const fileData = await getAndValidateFile(type, await file, config);\n\n if (!fileData.success) {\n return fileData;\n }\n\n const { createReadStream } = fileData.result;\n\n const dir = dirname(path);\n\n if (!pathExistsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const readStream = createReadStream();\n const out = createWriteStream(path);\n readStream.pipe(out);\n\n await new Promise<void>((resolve, reject) => {\n out.on('finish', () => resolve());\n out.on('error', (err) => {\n // Destroy the read stream to release resources if write fails\n if ('destroy' in readStream && typeof readStream.destroy === 'function') {\n readStream.destroy();\n }\n reject(err);\n });\n readStream.on('error', (err) => {\n out.destroy();\n reject(err);\n });\n });\n\n return {\n success: true,\n result: path,\n message: 'File uploaded successfully',\n code: RESPONSE_STATUS.OK.CODE,\n };\n }\n catch (error) {\n return {\n success: false,\n message: error instanceof Error ? error.message : 'File upload failed',\n code: RESPONSE_STATUS.INTERNAL_SERVER_ERROR.CODE,\n };\n }\n}\n"],"mappings":"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"}
|
|
@@ -5,6 +5,13 @@ import { I_GraphqlWSOptions, I_WSOptions } from './ws.type.js';
|
|
|
5
5
|
* This function creates a WebSocket server instance that can be attached to an HTTP server
|
|
6
6
|
* and configured with a specific path for WebSocket connections.
|
|
7
7
|
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* **Authentication Warning:** When `sessionParser` is not provided, the WebSocket server
|
|
10
|
+
* has **no session-based authentication**. Any client that can reach the endpoint can
|
|
11
|
+
* establish a connection. Only omit `sessionParser` for truly public WebSocket endpoints
|
|
12
|
+
* (e.g., health-check or anonymous broadcast channels). For authenticated subscriptions,
|
|
13
|
+
* always provide a `sessionParser` to validate sessions on upgrade.
|
|
14
|
+
*
|
|
8
15
|
* @param options - Configuration options including the HTTP server instance and WebSocket path.
|
|
9
16
|
* @returns A configured WebSocket server instance ready to handle connections.
|
|
10
17
|
*/
|
package/dist/node/ws/ws.util.js
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { log as e } from "../log/log.util.js";
|
|
2
|
+
import { useServer as t } from "graphql-ws/use/ws";
|
|
3
|
+
import { WebSocketServer as n } from "ws";
|
|
3
4
|
//#region src/node/ws/ws.util.ts
|
|
4
|
-
function
|
|
5
|
-
let { server:
|
|
6
|
-
if (!
|
|
7
|
-
if (!
|
|
8
|
-
if (
|
|
9
|
-
let e = new
|
|
10
|
-
return
|
|
5
|
+
function r(t) {
|
|
6
|
+
let { server: r, path: i, sessionParser: a } = t;
|
|
7
|
+
if (!r) throw Error("[WS] HTTP server instance is required to create a WebSocket server.");
|
|
8
|
+
if (!i || !i.startsWith("/")) throw Error("[WS] WebSocket path must be a non-empty string starting with \"/\".");
|
|
9
|
+
if (a) {
|
|
10
|
+
let e = new n({ noServer: !0 });
|
|
11
|
+
return r.on("upgrade", (t, n, r) => {
|
|
11
12
|
try {
|
|
12
|
-
if (new URL(t.url || "", "http://localhost").pathname !==
|
|
13
|
-
|
|
14
|
-
e.handleUpgrade(t, n,
|
|
13
|
+
if (new URL(t.url || "", "http://localhost").pathname !== i) return;
|
|
14
|
+
a(t, {}, () => {
|
|
15
|
+
e.handleUpgrade(t, n, r, (n) => {
|
|
15
16
|
e.emit("connection", n, t);
|
|
16
17
|
});
|
|
17
18
|
});
|
|
@@ -20,14 +21,14 @@ function n(e) {
|
|
|
20
21
|
}
|
|
21
22
|
}), e;
|
|
22
23
|
}
|
|
23
|
-
return new
|
|
24
|
-
server:
|
|
25
|
-
path:
|
|
24
|
+
return e.warn(`[WS] Creating unauthenticated WebSocket at "${i}". No sessionParser provided — any client can connect. Provide a sessionParser for authenticated endpoints.`), new n({
|
|
25
|
+
server: r,
|
|
26
|
+
path: i
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
function
|
|
29
|
-
let { schema: n, server: r, context: i, onConnect: a } =
|
|
30
|
-
return
|
|
29
|
+
function i(e) {
|
|
30
|
+
let { schema: n, server: r, context: i, onConnect: a } = e;
|
|
31
|
+
return t({
|
|
31
32
|
schema: n,
|
|
32
33
|
context: async (e) => {
|
|
33
34
|
let t = e.extra.request;
|
|
@@ -45,6 +46,6 @@ function r(t) {
|
|
|
45
46
|
}, r);
|
|
46
47
|
}
|
|
47
48
|
//#endregion
|
|
48
|
-
export {
|
|
49
|
+
export { r as createWSServer, i as initGraphQLWS };
|
|
49
50
|
|
|
50
51
|
//# sourceMappingURL=ws.util.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ws.util.js","names":[],"sources":["../../../src/node/ws/ws.util.ts"],"sourcesContent":["import type { Request, Response } from 'express';\nimport type { Buffer } from 'node:buffer';\nimport type { IncomingMessage } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\nimport { useServer as createGraphQLWSServer } from 'graphql-ws/use/ws';\nimport { WebSocketServer } from 'ws';\n\nimport type { I_AuthenticatedRequest, I_GraphqlWSOptions, I_WSOptions } from './ws.type.js';\n\n/**\n * Creates a WebSocket server with the specified configuration.\n * This function creates a WebSocket server instance that can be attached to an HTTP server\n * and configured with a specific path for WebSocket connections.\n *\n * @param options - Configuration options including the HTTP server instance and WebSocket path.\n * @returns A configured WebSocket server instance ready to handle connections.\n */\nexport function createWSServer(options: I_WSOptions): WebSocketServer {\n const { server, path, sessionParser } = options;\n\n if (!server) {\n throw new Error('[WS] HTTP server instance is required to create a WebSocket server.');\n }\n\n if (!path || !path.startsWith('/')) {\n throw new Error('[WS] WebSocket path must be a non-empty string starting with \"/\".');\n }\n\n if (sessionParser) {\n const wss = new WebSocketServer({ noServer: true });\n\n server.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {\n try {\n const url = new URL(req.url || '', 'http://localhost');\n if (url.pathname !== path)\n return;\n\n sessionParser(req as Request, {} as Response, () => {\n wss.handleUpgrade(req, socket, head, (ws) => {\n wss.emit('connection', ws, req);\n });\n });\n }\n catch {\n socket.destroy();\n }\n });\n\n return wss;\n }\n\n return new WebSocketServer({ server, path });\n}\n\n/**\n * Initializes GraphQL WebSocket server with schema and WebSocket server.\n * This function sets up GraphQL subscriptions over WebSocket by creating a GraphQL WebSocket server\n * that can handle GraphQL operations including queries, mutations, and subscriptions.\n *\n * @param options - Configuration options including the GraphQL schema and WebSocket server instance.\n * @returns A configured GraphQL WebSocket server ready to handle GraphQL operations over WebSocket.\n */\nexport function initGraphQLWS(options: I_GraphqlWSOptions) {\n const { schema, server, context: makeExtraContext, onConnect } = options;\n\n return createGraphQLWSServer(\n {\n schema,\n context: async (ctx) => {\n const req = ctx.extra.request as I_AuthenticatedRequest;\n\n const extra = makeExtraContext ? await makeExtraContext(req) : {};\n return { req, ...extra };\n },\n onConnect: async (ctx) => {\n if (onConnect) {\n const req = ctx.extra.request as I_AuthenticatedRequest;\n await onConnect(req);\n }\n },\n },\n server,\n );\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"ws.util.js","names":[],"sources":["../../../src/node/ws/ws.util.ts"],"sourcesContent":["import type { Request, Response } from 'express';\nimport type { Buffer } from 'node:buffer';\nimport type { IncomingMessage } from 'node:http';\nimport type { Duplex } from 'node:stream';\n\nimport { useServer as createGraphQLWSServer } from 'graphql-ws/use/ws';\nimport { WebSocketServer } from 'ws';\n\nimport type { I_AuthenticatedRequest, I_GraphqlWSOptions, I_WSOptions } from './ws.type.js';\n\nimport { log } from '../log/index.js';\n\n/**\n * Creates a WebSocket server with the specified configuration.\n * This function creates a WebSocket server instance that can be attached to an HTTP server\n * and configured with a specific path for WebSocket connections.\n *\n * @remarks\n * **Authentication Warning:** When `sessionParser` is not provided, the WebSocket server\n * has **no session-based authentication**. Any client that can reach the endpoint can\n * establish a connection. Only omit `sessionParser` for truly public WebSocket endpoints\n * (e.g., health-check or anonymous broadcast channels). For authenticated subscriptions,\n * always provide a `sessionParser` to validate sessions on upgrade.\n *\n * @param options - Configuration options including the HTTP server instance and WebSocket path.\n * @returns A configured WebSocket server instance ready to handle connections.\n */\nexport function createWSServer(options: I_WSOptions): WebSocketServer {\n const { server, path, sessionParser } = options;\n\n if (!server) {\n throw new Error('[WS] HTTP server instance is required to create a WebSocket server.');\n }\n\n if (!path || !path.startsWith('/')) {\n throw new Error('[WS] WebSocket path must be a non-empty string starting with \"/\".');\n }\n\n if (sessionParser) {\n const wss = new WebSocketServer({ noServer: true });\n\n server.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {\n try {\n const url = new URL(req.url || '', 'http://localhost');\n if (url.pathname !== path)\n return;\n\n sessionParser(req as Request, {} as Response, () => {\n wss.handleUpgrade(req, socket, head, (ws) => {\n wss.emit('connection', ws, req);\n });\n });\n }\n catch {\n /* Intentionally empty — destroy socket on any URL parsing or session error */\n socket.destroy();\n }\n });\n\n return wss;\n }\n\n log.warn(`[WS] Creating unauthenticated WebSocket at \"${path}\". No sessionParser provided — any client can connect. Provide a sessionParser for authenticated endpoints.`);\n\n return new WebSocketServer({ server, path });\n}\n\n/**\n * Initializes GraphQL WebSocket server with schema and WebSocket server.\n * This function sets up GraphQL subscriptions over WebSocket by creating a GraphQL WebSocket server\n * that can handle GraphQL operations including queries, mutations, and subscriptions.\n *\n * @param options - Configuration options including the GraphQL schema and WebSocket server instance.\n * @returns A configured GraphQL WebSocket server ready to handle GraphQL operations over WebSocket.\n */\nexport function initGraphQLWS(options: I_GraphqlWSOptions) {\n const { schema, server, context: makeExtraContext, onConnect } = options;\n\n return createGraphQLWSServer(\n {\n schema,\n context: async (ctx) => {\n const req = ctx.extra.request as I_AuthenticatedRequest;\n\n const extra = makeExtraContext ? await makeExtraContext(req) : {};\n return { req, ...extra };\n },\n onConnect: async (ctx) => {\n if (onConnect) {\n const req = ctx.extra.request as I_AuthenticatedRequest;\n await onConnect(req);\n }\n },\n },\n server,\n );\n}\n"],"mappings":";;;;AA2BA,SAAgB,EAAe,GAAuC;CAClE,IAAM,EAAE,WAAQ,SAAM,qBAAkB;AAExC,KAAI,CAAC,EACD,OAAU,MAAM,sEAAsE;AAG1F,KAAI,CAAC,KAAQ,CAAC,EAAK,WAAW,IAAI,CAC9B,OAAU,MAAM,sEAAoE;AAGxF,KAAI,GAAe;EACf,IAAM,IAAM,IAAI,EAAgB,EAAE,UAAU,IAAM,CAAC;AAoBnD,SAlBA,EAAO,GAAG,YAAY,GAAsB,GAAgB,MAAiB;AACzE,OAAI;AAEA,QADY,IAAI,IAAI,EAAI,OAAO,IAAI,mBAAmB,CAC9C,aAAa,EACjB;AAEJ,MAAc,GAAgB,EAAE,QAAoB;AAChD,OAAI,cAAc,GAAK,GAAQ,IAAO,MAAO;AACzC,QAAI,KAAK,cAAc,GAAI,EAAI;OACjC;MACJ;WAEA;AAEF,MAAO,SAAS;;IAEtB,EAEK;;AAKX,QAFA,EAAI,KAAK,+CAA+C,EAAK,6GAA6G,EAEnK,IAAI,EAAgB;EAAE;EAAQ;EAAM,CAAC;;AAWhD,SAAgB,EAAc,GAA6B;CACvD,IAAM,EAAE,WAAQ,WAAQ,SAAS,GAAkB,iBAAc;AAEjE,QAAO,EACH;EACI;EACA,SAAS,OAAO,MAAQ;GACpB,IAAM,IAAM,EAAI,MAAM;AAGtB,UAAO;IAAE;IAAK,GADA,IAAmB,MAAM,EAAiB,EAAI,GAAG,EAAE;IACzC;;EAE5B,WAAW,OAAO,MAAQ;AACtB,OAAI,GAAW;IACX,IAAM,IAAM,EAAI,MAAM;AACtB,UAAM,EAAU,EAAI;;;EAG/B,EACD,EACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apollo-client.component.js","names":[],"sources":["../../../src/react/apollo-client/apollo-client.component.tsx"],"sourcesContent":["import { ApolloProvider as ApolloProviderDefault } from '@apollo/client/react';\nimport * as React from 'react';\nimport { useMemo } from 'react';\n\nimport type { I_ApolloProviderProps } from './apollo-client.type.js';\n\nimport { ApolloErrorComponent, ApolloErrorProvider } from '../apollo-error/index.js';\nimport { Toaster } from '../toast/index.js';\nimport { ApolloClientProvider } from './apollo-client.context.js';\nimport { getClient } from './apollo-client.util.js';\n\n/**\n * Apollo Provider component that wraps the application with Apollo Client context.\n * This component provides the Apollo Client instance to all child components,\n * enabling GraphQL operations throughout the component tree. It includes\n * error handling and toast integration for a complete GraphQL experience.\n *\n * Features:\n * - Apollo Client context provision\n * - Error boundary integration\n * - Toast notification support\n * - Automatic error handling\n * - Development and production optimizations\n *\n * @param props - Component props containing options, children, and optional error override.\n * @param props.options - Apollo Client configuration options.\n * @param props.children - React children that will have access to Apollo Client context.\n * @param props.onError - Optional callback to override the default modal/toast error handling.\n * @returns A React component that provides Apollo Client context to its children.\n */\nexport function ApolloProvider({ options, children, onError }: I_ApolloProviderProps) {\n const client = useMemo(\n () => getClient(options ?? {}),\n //
|
|
1
|
+
{"version":3,"file":"apollo-client.component.js","names":[],"sources":["../../../src/react/apollo-client/apollo-client.component.tsx"],"sourcesContent":["import { ApolloProvider as ApolloProviderDefault } from '@apollo/client/react';\nimport * as React from 'react';\nimport { useMemo } from 'react';\n\nimport type { I_ApolloProviderProps } from './apollo-client.type.js';\n\nimport { ApolloErrorComponent, ApolloErrorProvider } from '../apollo-error/index.js';\nimport { Toaster } from '../toast/index.js';\nimport { ApolloClientProvider } from './apollo-client.context.js';\nimport { getClient } from './apollo-client.util.js';\n\n/**\n * Apollo Provider component that wraps the application with Apollo Client context.\n * This component provides the Apollo Client instance to all child components,\n * enabling GraphQL operations throughout the component tree. It includes\n * error handling and toast integration for a complete GraphQL experience.\n *\n * Features:\n * - Apollo Client context provision\n * - Error boundary integration\n * - Toast notification support\n * - Automatic error handling\n * - Development and production optimizations\n *\n * @param props - Component props containing options, children, and optional error override.\n * @param props.options - Apollo Client configuration options.\n * @param props.children - React children that will have access to Apollo Client context.\n * @param props.onError - Optional callback to override the default modal/toast error handling.\n * @returns A React component that provides Apollo Client context to its children.\n */\nexport function ApolloProvider({ options, children, onError }: I_ApolloProviderProps) {\n const client = useMemo(\n () => getClient(options ?? {}),\n // eslint-disable-next-line react/exhaustive-deps -- intended to only recreate on uri/wsUrl change\n [options?.uri, options?.wsUrl],\n );\n\n return (\n <>\n <ApolloErrorProvider onError={onError}>\n <ApolloClientProvider client={client}>\n <ApolloProviderDefault client={client}>{children}</ApolloProviderDefault>\n </ApolloClientProvider>\n {!onError && <ApolloErrorComponent />}\n </ApolloErrorProvider>\n <Toaster position=\"top-right\" />\n </>\n );\n}\n"],"mappings":";;;;;;;;;AA8BA,SAAgB,EAAe,EAAE,YAAS,aAAU,cAAkC;CAClF,IAAM,IAAS,QACL,EAAU,KAAW,EAAE,CAAC,EAE9B,CAAC,GAAS,KAAK,GAAS,MAAM,CACjC;AAED,QACI,kBAAA,cAAA,EAAA,UAAA,MACI,kBAAA,cAAC,GAAD,EAA8B,YAKR,EAJlB,kBAAA,cAAC,GAAD,EAA8B,WAEP,EADnB,kBAAA,cAAC,GAAD,EAA+B,WAA0C,EAAjC,EAAiC,CACtD,EACtB,CAAC,KAAW,kBAAA,cAAC,GAAA,KAAuB,CACnB,EACtB,kBAAA,cAAC,GAAD,EAAS,UAAS,aAAc,CAAA,CACjC"}
|
|
@@ -6,6 +6,8 @@ export interface I_ApolloOptions extends Omit<ApolloClient.Options, 'link' | 'ca
|
|
|
6
6
|
uri?: string;
|
|
7
7
|
wsUrl?: string;
|
|
8
8
|
customLinks?: ApolloLink[];
|
|
9
|
+
/** Enable round-trip timing logs for every GraphQL operation. Defaults to false. */
|
|
10
|
+
debug?: boolean;
|
|
9
11
|
}
|
|
10
12
|
export interface I_ApolloProviderProps extends I_Children {
|
|
11
13
|
isNextJS?: boolean;
|
|
@@ -33,19 +33,19 @@ var x = new l((e, t) => (e.setContext({ start: performance.now() }), t(e).pipe(b
|
|
|
33
33
|
}, "Error Details"))));
|
|
34
34
|
});
|
|
35
35
|
function C(e) {
|
|
36
|
-
let { uri: t, wsUrl: r, customLinks: i } = e,
|
|
36
|
+
let { uri: t, wsUrl: r, customLinks: i, debug: o } = e, c = new g();
|
|
37
37
|
t || n.warn(`[Apollo] No GraphQL URI provided — using "${a}" as default`);
|
|
38
|
-
let
|
|
38
|
+
let u = s({
|
|
39
39
|
uri: t ?? "/graphql",
|
|
40
40
|
credentials: "include",
|
|
41
41
|
headers: { "apollo-require-preflight": "true" }
|
|
42
|
-
}),
|
|
42
|
+
}), d = r ? new _(y({ url: r })) : l.empty(), f = r ? l.split(({ operationType: e }) => e === v.SUBSCRIPTION, d, u) : u;
|
|
43
43
|
return [
|
|
44
|
-
x,
|
|
44
|
+
...o ? [x] : [],
|
|
45
45
|
S,
|
|
46
|
-
|
|
46
|
+
c,
|
|
47
47
|
...i ?? [],
|
|
48
|
-
|
|
48
|
+
f
|
|
49
49
|
];
|
|
50
50
|
}
|
|
51
51
|
function w(e) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apollo-client.util.js","names":[],"sources":["../../../src/react/apollo-client/apollo-client.util.tsx"],"sourcesContent":["import { ApolloClient, CombinedGraphQLErrors, CombinedProtocolErrors, InMemoryCache, ServerError } from '@apollo/client/core';\nimport { ApolloLink } from '@apollo/client/link';\nimport { ErrorLink } from '@apollo/client/link/error';\nimport { RemoveTypenameFromVariablesLink } from '@apollo/client/link/remove-typename';\nimport { GraphQLWsLink } from '@apollo/client/link/subscriptions';\nimport { OperationTypeNode } from 'graphql';\nimport { createClient } from 'graphql-ws';\nimport * as React from 'react';\nimport { tap } from 'rxjs';\n\nimport { IS_BROWSER } from '#constant/index.js';\n\nimport type { I_ApolloOptions } from './apollo-client.type.js';\n\nimport { hasCustomApolloErrorHandler, showGlobalApolloError } from '../apollo-error/index.js';\nimport { log } from '../log/index.js';\nimport { toast } from '../toast/index.js';\nimport { GRAPHQL_URI_DEFAULT } from './apollo-client.constant.js';\nimport styles from './apollo-client.module.scss';\nimport { createUploadLink } from './links/index.js';\n\nconst roundTripLink = new ApolloLink((operation, forward) => {\n operation.setContext({ start: performance.now() });\n\n return forward(operation).pipe(\n tap(() => {\n const time = Math.round(performance.now() - operation.getContext()['start']);\n\n log.info(`Operation ${operation.operationName} took ${time}ms to complete`);\n }),\n );\n});\n\nconst errorLink = new ErrorLink(({ error, operation }) => {\n const opName = operation?.operationName || 'Unknown';\n let errorMessage = '';\n\n if (CombinedGraphQLErrors.is(error)) {\n error.errors.forEach(({ message, locations, path }, index) => {\n if (index === 0) {\n errorMessage = message;\n }\n\n log.error(\n `[GraphQL error] ${opName}: ${message}, Location: ${JSON.stringify(locations, null, 4)}, Path: ${path}`,\n );\n });\n }\n else if (CombinedProtocolErrors.is(error)) {\n error.errors.forEach(({ message, extensions }, index) => {\n if (index === 0) {\n errorMessage = message;\n }\n\n log.error(\n `[Protocol error]: ${message}, Extensions: ${JSON.stringify(extensions, null, 4)}`,\n );\n });\n }\n else if (ServerError.is(error)) {\n errorMessage = error.message;\n\n log.error(`[Server error]: ${error.message}`);\n }\n else {\n errorMessage = error.message;\n\n log.error(`[Network error]: ${error.message}`);\n }\n\n if (error && errorMessage && IS_BROWSER) {\n if (hasCustomApolloErrorHandler()) {\n showGlobalApolloError(error);\n }\n else {\n toast.error((t: { id: string }) => (\n <div className={styles['error-container']}>\n {errorMessage}\n <button\n type=\"button\"\n className={styles['error-details-button']}\n onClick={() => {\n showGlobalApolloError(error);\n\n toast.dismiss(t.id);\n }}\n >\n Error Details\n </button>\n </div>\n ));\n }\n }\n});\n\n/**\n * Creates a comprehensive Apollo Link chain with all necessary middleware.\n * This function sets up a complete Apollo Link chain including error handling,\n * logging, file uploads, WebSocket subscriptions, and custom links. The chain\n * is configured to handle both HTTP and WebSocket operations with proper routing.\n *\n * The link chain includes:\n * - Development logging for operation tracking\n * - Error handling with user-friendly notifications\n * - Type name removal for cleaner requests\n * - File upload support\n * - WebSocket subscription support\n * - Custom link integration\n *\n * @param options - Configuration options for the Apollo Client including URI, WebSocket URL, and custom links.\n * @returns An array of Apollo Links configured for the specified options.\n */\nexport function createApolloLinks(options: I_ApolloOptions): ApolloLink[] {\n const { uri, wsUrl, customLinks } = options;\n\n const removeTypenameLink = new RemoveTypenameFromVariablesLink();\n\n if (!uri) {\n log.warn(`[Apollo] No GraphQL URI provided — using \"${GRAPHQL_URI_DEFAULT}\" as default`);\n }\n\n const uploadLink = createUploadLink({\n uri: uri ?? GRAPHQL_URI_DEFAULT,\n credentials: 'include',\n headers: {\n 'apollo-require-preflight': 'true',\n },\n });\n\n const wsLink = wsUrl\n ? new GraphQLWsLink(createClient({ url: wsUrl }))\n : ApolloLink.empty();\n\n const splitLink = wsUrl\n ? ApolloLink.split(\n ({ operationType }) => {\n return operationType === OperationTypeNode.SUBSCRIPTION;\n },\n wsLink,\n uploadLink as unknown as ApolloLink,\n )\n : uploadLink;\n\n return [\n roundTripLink,\n errorLink,\n removeTypenameLink,\n ...(customLinks ?? []),\n splitLink as unknown as ApolloLink,\n ];\n}\n\n/**\n * Creates a fully configured Apollo Client instance.\n * This function creates an Apollo Client with all necessary configuration including\n * the link chain, cache, and any additional options provided. The client is ready\n * for immediate use in React applications with comprehensive error handling and\n * development tooling.\n *\n * @param options - Configuration options for the Apollo Client including links, cache, and other settings.\n * @returns A fully configured Apollo Client instance ready for use.\n */\nexport function getClient(options: I_ApolloOptions) {\n const link = ApolloLink.from(createApolloLinks(options));\n\n return new ApolloClient({\n link,\n cache: new InMemoryCache(),\n ...options,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqBA,IAAM,IAAgB,IAAI,GAAY,GAAW,OAC7C,EAAU,WAAW,EAAE,OAAO,YAAY,KAAK,EAAE,CAAC,EAE3C,EAAQ,EAAU,CAAC,KACtB,QAAU;CACN,IAAM,IAAO,KAAK,MAAM,YAAY,KAAK,GAAG,EAAU,YAAY,CAAC,MAAS;AAE5E,GAAI,KAAK,aAAa,EAAU,cAAc,QAAQ,EAAK,gBAAgB;EAC7E,CACL,EACH,EAEI,IAAY,IAAI,GAAW,EAAE,UAAO,mBAAgB;CACtD,IAAM,IAAS,GAAW,iBAAiB,WACvC,IAAe;AAmCnB,CAjCI,EAAsB,GAAG,EAAM,GAC/B,EAAM,OAAO,SAAS,EAAE,YAAS,cAAW,WAAQ,MAAU;AAK1D,EAJI,MAAU,MACV,IAAe,IAGnB,EAAI,MACA,mBAAmB,EAAO,IAAI,EAAQ,cAAc,KAAK,UAAU,GAAW,MAAM,EAAE,CAAC,UAAU,IACpG;GACH,GAEG,EAAuB,GAAG,EAAM,GACrC,EAAM,OAAO,SAAS,EAAE,YAAS,iBAAc,MAAU;AAKrD,EAJI,MAAU,MACV,IAAe,IAGnB,EAAI,MACA,qBAAqB,EAAQ,gBAAgB,KAAK,UAAU,GAAY,MAAM,EAAE,GACnF;GACH,GAEG,EAAY,GAAG,EAAM,IAC1B,IAAe,EAAM,SAErB,EAAI,MAAM,mBAAmB,EAAM,UAAU,KAG7C,IAAe,EAAM,SAErB,EAAI,MAAM,oBAAoB,EAAM,UAAU,GAG9C,KAAS,KAAgB,MACrB,GAA6B,GAC7B,EAAsB,EAAM,GAG5B,EAAM,OAAO,MACT,kBAAA,cAAC,OAAD,EAAK,WAAW,EAAO,oBAajB,EAZD,GACD,kBAAA,cAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,eAAe;AAGX,GAFA,EAAsB,EAAM,EAE5B,EAAM,QAAQ,EAAE,GAAG;;EAIlB,EAFR,gBAEQ,CACP,CACR;EAGZ;AAmBF,SAAgB,EAAkB,GAAwC;CACtE,IAAM,EAAE,QAAK,UAAO,
|
|
1
|
+
{"version":3,"file":"apollo-client.util.js","names":[],"sources":["../../../src/react/apollo-client/apollo-client.util.tsx"],"sourcesContent":["import { ApolloClient, CombinedGraphQLErrors, CombinedProtocolErrors, InMemoryCache, ServerError } from '@apollo/client/core';\nimport { ApolloLink } from '@apollo/client/link';\nimport { ErrorLink } from '@apollo/client/link/error';\nimport { RemoveTypenameFromVariablesLink } from '@apollo/client/link/remove-typename';\nimport { GraphQLWsLink } from '@apollo/client/link/subscriptions';\nimport { OperationTypeNode } from 'graphql';\nimport { createClient } from 'graphql-ws';\nimport * as React from 'react';\nimport { tap } from 'rxjs';\n\nimport { IS_BROWSER } from '#constant/index.js';\n\nimport type { I_ApolloOptions } from './apollo-client.type.js';\n\nimport { hasCustomApolloErrorHandler, showGlobalApolloError } from '../apollo-error/index.js';\nimport { log } from '../log/index.js';\nimport { toast } from '../toast/index.js';\nimport { GRAPHQL_URI_DEFAULT } from './apollo-client.constant.js';\nimport styles from './apollo-client.module.scss';\nimport { createUploadLink } from './links/index.js';\n\nconst roundTripLink = new ApolloLink((operation, forward) => {\n operation.setContext({ start: performance.now() });\n\n return forward(operation).pipe(\n tap(() => {\n const time = Math.round(performance.now() - operation.getContext()['start']);\n\n log.info(`Operation ${operation.operationName} took ${time}ms to complete`);\n }),\n );\n});\n\nconst errorLink = new ErrorLink(({ error, operation }) => {\n const opName = operation?.operationName || 'Unknown';\n let errorMessage = '';\n\n if (CombinedGraphQLErrors.is(error)) {\n error.errors.forEach(({ message, locations, path }, index) => {\n if (index === 0) {\n errorMessage = message;\n }\n\n log.error(\n `[GraphQL error] ${opName}: ${message}, Location: ${JSON.stringify(locations, null, 4)}, Path: ${path}`,\n );\n });\n }\n else if (CombinedProtocolErrors.is(error)) {\n error.errors.forEach(({ message, extensions }, index) => {\n if (index === 0) {\n errorMessage = message;\n }\n\n log.error(\n `[Protocol error]: ${message}, Extensions: ${JSON.stringify(extensions, null, 4)}`,\n );\n });\n }\n else if (ServerError.is(error)) {\n errorMessage = error.message;\n\n log.error(`[Server error]: ${error.message}`);\n }\n else {\n errorMessage = error.message;\n\n log.error(`[Network error]: ${error.message}`);\n }\n\n if (error && errorMessage && IS_BROWSER) {\n if (hasCustomApolloErrorHandler()) {\n showGlobalApolloError(error);\n }\n else {\n toast.error((t: { id: string }) => (\n <div className={styles['error-container']}>\n {errorMessage}\n <button\n type=\"button\"\n className={styles['error-details-button']}\n onClick={() => {\n showGlobalApolloError(error);\n\n toast.dismiss(t.id);\n }}\n >\n Error Details\n </button>\n </div>\n ));\n }\n }\n});\n\n/**\n * Creates a comprehensive Apollo Link chain with all necessary middleware.\n * This function sets up a complete Apollo Link chain including error handling,\n * logging, file uploads, WebSocket subscriptions, and custom links. The chain\n * is configured to handle both HTTP and WebSocket operations with proper routing.\n *\n * The link chain includes:\n * - Development logging for operation tracking\n * - Error handling with user-friendly notifications\n * - Type name removal for cleaner requests\n * - File upload support\n * - WebSocket subscription support\n * - Custom link integration\n *\n * @param options - Configuration options for the Apollo Client including URI, WebSocket URL, and custom links.\n * @returns An array of Apollo Links configured for the specified options.\n */\nexport function createApolloLinks(options: I_ApolloOptions): ApolloLink[] {\n const { uri, wsUrl, customLinks, debug } = options;\n\n const removeTypenameLink = new RemoveTypenameFromVariablesLink();\n\n if (!uri) {\n log.warn(`[Apollo] No GraphQL URI provided — using \"${GRAPHQL_URI_DEFAULT}\" as default`);\n }\n\n const uploadLink = createUploadLink({\n uri: uri ?? GRAPHQL_URI_DEFAULT,\n credentials: 'include',\n headers: {\n 'apollo-require-preflight': 'true',\n },\n });\n\n const wsLink = wsUrl\n ? new GraphQLWsLink(createClient({ url: wsUrl }))\n : ApolloLink.empty();\n\n const splitLink = wsUrl\n ? ApolloLink.split(\n ({ operationType }) => {\n return operationType === OperationTypeNode.SUBSCRIPTION;\n },\n wsLink,\n uploadLink as unknown as ApolloLink,\n )\n : uploadLink;\n\n return [\n ...(debug ? [roundTripLink] : []),\n errorLink,\n removeTypenameLink,\n ...(customLinks ?? []),\n splitLink as unknown as ApolloLink,\n ];\n}\n\n/**\n * Creates a fully configured Apollo Client instance.\n * This function creates an Apollo Client with all necessary configuration including\n * the link chain, cache, and any additional options provided. The client is ready\n * for immediate use in React applications with comprehensive error handling and\n * development tooling.\n *\n * @param options - Configuration options for the Apollo Client including links, cache, and other settings.\n * @returns A fully configured Apollo Client instance ready for use.\n */\nexport function getClient(options: I_ApolloOptions) {\n const link = ApolloLink.from(createApolloLinks(options));\n\n return new ApolloClient({\n link,\n cache: new InMemoryCache(),\n ...options,\n });\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAqBA,IAAM,IAAgB,IAAI,GAAY,GAAW,OAC7C,EAAU,WAAW,EAAE,OAAO,YAAY,KAAK,EAAE,CAAC,EAE3C,EAAQ,EAAU,CAAC,KACtB,QAAU;CACN,IAAM,IAAO,KAAK,MAAM,YAAY,KAAK,GAAG,EAAU,YAAY,CAAC,MAAS;AAE5E,GAAI,KAAK,aAAa,EAAU,cAAc,QAAQ,EAAK,gBAAgB;EAC7E,CACL,EACH,EAEI,IAAY,IAAI,GAAW,EAAE,UAAO,mBAAgB;CACtD,IAAM,IAAS,GAAW,iBAAiB,WACvC,IAAe;AAmCnB,CAjCI,EAAsB,GAAG,EAAM,GAC/B,EAAM,OAAO,SAAS,EAAE,YAAS,cAAW,WAAQ,MAAU;AAK1D,EAJI,MAAU,MACV,IAAe,IAGnB,EAAI,MACA,mBAAmB,EAAO,IAAI,EAAQ,cAAc,KAAK,UAAU,GAAW,MAAM,EAAE,CAAC,UAAU,IACpG;GACH,GAEG,EAAuB,GAAG,EAAM,GACrC,EAAM,OAAO,SAAS,EAAE,YAAS,iBAAc,MAAU;AAKrD,EAJI,MAAU,MACV,IAAe,IAGnB,EAAI,MACA,qBAAqB,EAAQ,gBAAgB,KAAK,UAAU,GAAY,MAAM,EAAE,GACnF;GACH,GAEG,EAAY,GAAG,EAAM,IAC1B,IAAe,EAAM,SAErB,EAAI,MAAM,mBAAmB,EAAM,UAAU,KAG7C,IAAe,EAAM,SAErB,EAAI,MAAM,oBAAoB,EAAM,UAAU,GAG9C,KAAS,KAAgB,MACrB,GAA6B,GAC7B,EAAsB,EAAM,GAG5B,EAAM,OAAO,MACT,kBAAA,cAAC,OAAD,EAAK,WAAW,EAAO,oBAajB,EAZD,GACD,kBAAA,cAAC,UAAD;EACI,MAAK;EACL,WAAW,EAAO;EAClB,eAAe;AAGX,GAFA,EAAsB,EAAM,EAE5B,EAAM,QAAQ,EAAE,GAAG;;EAIlB,EAFR,gBAEQ,CACP,CACR;EAGZ;AAmBF,SAAgB,EAAkB,GAAwC;CACtE,IAAM,EAAE,QAAK,UAAO,gBAAa,aAAU,GAErC,IAAqB,IAAI,GAAiC;AAEhE,CAAK,KACD,EAAI,KAAK,6CAA6C,EAAoB,cAAc;CAG5F,IAAM,IAAa,EAAiB;EAChC,KAAK,KAAA;EACL,aAAa;EACb,SAAS,EACL,4BAA4B,QAC/B;EACJ,CAAC,EAEI,IAAS,IACT,IAAI,EAAc,EAAa,EAAE,KAAK,GAAO,CAAC,CAAC,GAC/C,EAAW,OAAO,EAElB,IAAY,IACZ,EAAW,OACJ,EAAE,uBACQ,MAAkB,EAAkB,cAE/C,GACA,EACH,GACH;AAEN,QAAO;EACH,GAAI,IAAQ,CAAC,EAAc,GAAG,EAAE;EAChC;EACA;EACA,GAAI,KAAe,EAAE;EACrB;EACH;;AAaL,SAAgB,EAAU,GAA0B;AAGhD,QAAO,IAAI,EAAa;EACpB,MAHS,EAAW,KAAK,EAAkB,EAAQ,CAAC;EAIpD,OAAO,IAAI,GAAe;EAC1B,GAAG;EACN,CAAC"}
|
|
@@ -23,7 +23,7 @@ function c() {
|
|
|
23
23
|
let t = [...u.current.querySelectorAll(s)].filter((e) => !e.hasAttribute("disabled"));
|
|
24
24
|
if (t.length === 0) return;
|
|
25
25
|
let n = t[0], r = t.at(-1);
|
|
26
|
-
!n || !r || (e.shiftKey ? document.activeElement === n && (e.preventDefault(), r.focus()) : document.activeElement === r && (e.preventDefault(), n.focus()));
|
|
26
|
+
!n || !r || (e.shiftKey ? (document.activeElement === n || document.activeElement === u.current) && (e.preventDefault(), r.focus()) : document.activeElement === r && (e.preventDefault(), n.focus()));
|
|
27
27
|
};
|
|
28
28
|
if (!c) return null;
|
|
29
29
|
let p = "locations" in c || "path" in c || "extensions" in c, m = "message" in c ? c.message : "Unknown error occurred";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apollo-error.component.js","names":[],"sources":["../../../src/react/apollo-error/apollo-error.component.tsx"],"sourcesContent":["import * as React from 'react';\nimport { use, useEffect, useRef } from 'react';\n\nimport { validate } from '#util/validate/validate.util.js';\n\nimport { ApolloErrorContext } from './apollo-error.context.js';\nimport style from './apollo-error.module.scss';\n\nconst FOCUSABLE_SELECTORS = 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Apollo Error Component that displays detailed error information in a modal.\n * This component provides a comprehensive error display interface that shows\n * GraphQL errors and other error types with detailed information for debugging purposes.\n *\n * Features:\n * - Modal overlay with backdrop for focus\n * - Detailed error categorization and display\n * - Reload functionality for error recovery\n * - Close button to dismiss the error modal\n * - Structured display of different error types\n * - Extra information display for debugging\n *\n * @returns A modal component displaying Apollo error details, or null if no error is present.\n */\nexport function ApolloErrorComponent() {\n const context = use(ApolloErrorContext);\n const { error, hideError } = context ?? {};\n const dialogRef = useRef<HTMLDivElement>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n if (!error || !hideError)\n return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n hideError();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [error, hideError]);\n\n useEffect(() => {\n if (error && dialogRef.current) {\n previousFocusRef.current = document.activeElement as HTMLElement;\n dialogRef.current.focus();\n }\n else if (!error && previousFocusRef.current) {\n previousFocusRef.current.focus();\n previousFocusRef.current = null;\n }\n }, [error]);\n\n const handleFocusTrap = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Tab' || !dialogRef.current)\n return;\n\n const focusableElements = [...dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)].filter(el => !el.hasAttribute('disabled'));\n\n if (focusableElements.length === 0)\n return;\n\n const firstElement = focusableElements[0];\n const lastElement = focusableElements.at(-1);\n\n if (!firstElement || !lastElement)\n return;\n\n if (event.shiftKey) {\n if (document.activeElement === firstElement) {\n event.preventDefault();\n lastElement.focus();\n }\n }\n else if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n };\n\n if (!error) {\n return null;\n }\n\n const isGraphQLError = 'locations' in error || 'path' in error || 'extensions' in error;\n const errorMessage = 'message' in error ? error.message : 'Unknown error occurred';\n\n return (\n <div className={style['modal-backdrop']} onClick={hideError}>\n <div\n ref={dialogRef}\n className={style['modal-content']}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"apollo-error-title\"\n aria-describedby=\"apollo-error-details\"\n tabIndex={-1}\n onKeyDown={handleFocusTrap}\n onClick={e => e.stopPropagation()}\n >\n <button\n type=\"button\"\n className={style['btn-close']}\n onClick={hideError}\n aria-label=\"Close error details (Esc)\"\n aria-keyshortcuts=\"Escape\"\n title=\"Close error details (Esc)\"\n >\n ✕\n </button>\n <div className={style['error-title']}>\n <button\n type=\"button\"\n className={style['btn-retry']}\n onClick={() => window.location.reload()}\n aria-label=\"Reload page\"\n >\n Reload\n </button>\n {' '}\n <span id=\"apollo-error-title\">\n {errorMessage || 'Error details'}\n </span>\n </div>\n <div id=\"apollo-error-details\" className={style['error-details']}>\n {isGraphQLError && 'locations' in error && error.locations && (\n <pre className=\"locations\">\n <strong>Locations:</strong>\n {' '}\n {JSON.stringify(error.locations, null, 4)}\n </pre>\n )}\n {isGraphQLError && 'path' in error && error.path && (\n <pre className=\"path\">\n <strong>Path:</strong>\n {' '}\n {JSON.stringify(error.path, null, 4)}\n </pre>\n )}\n {isGraphQLError && 'extensions' in error && error.extensions && (\n <pre className=\"extensions\">\n <strong>Extensions:</strong>\n {' '}\n {JSON.stringify(error.extensions, null, 4)}\n </pre>\n )}\n {!isGraphQLError && (\n <pre className=\"error-details\">\n <strong>Error Details:</strong>\n {' '}\n {validate.isEmpty(error) ? errorMessage : JSON.stringify(error, null, 4)}\n </pre>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":";;;;;;AAQA,IAAM,IAAsB;AAiB5B,SAAgB,IAAuB;CAEnC,IAAM,EAAE,UAAO,iBADC,EAAI,EAAmB,IACC,EAAE,EACpC,IAAY,EAAuB,KAAK,EACxC,IAAmB,EAA2B,KAAK;AAmBzD,CAjBA,QAAgB;AACZ,MAAI,CAAC,KAAS,CAAC,EACX;EAEJ,IAAM,KAAiB,MAAyB;AAC5C,GAAI,EAAM,QAAQ,YACd,GAAW;;AAMnB,SAFA,OAAO,iBAAiB,WAAW,EAAc,QAEpC;AACT,UAAO,oBAAoB,WAAW,EAAc;;IAEzD,CAAC,GAAO,EAAU,CAAC,EAEtB,QAAgB;AACZ,EAAI,KAAS,EAAU,WACnB,EAAiB,UAAU,SAAS,eACpC,EAAU,QAAQ,OAAO,IAEpB,CAAC,KAAS,EAAiB,YAChC,EAAiB,QAAQ,OAAO,EAChC,EAAiB,UAAU;IAEhC,CAAC,EAAM,CAAC;CAEX,IAAM,KAAmB,MAA+C;AACpE,MAAI,EAAM,QAAQ,SAAS,CAAC,EAAU,QAClC;EAEJ,IAAM,IAAoB,CAAC,GAAG,EAAU,QAAQ,iBAA8B,EAAoB,CAAC,CAAC,QAAO,MAAM,CAAC,EAAG,aAAa,WAAW,CAAC;AAE9I,MAAI,EAAkB,WAAW,EAC7B;EAEJ,IAAM,IAAe,EAAkB,IACjC,IAAc,EAAkB,GAAG,GAAG;AAExC,GAAC,KAAgB,CAAC,MAGlB,EAAM,
|
|
1
|
+
{"version":3,"file":"apollo-error.component.js","names":[],"sources":["../../../src/react/apollo-error/apollo-error.component.tsx"],"sourcesContent":["import * as React from 'react';\nimport { use, useEffect, useRef } from 'react';\n\nimport { validate } from '#util/validate/validate.util.js';\n\nimport { ApolloErrorContext } from './apollo-error.context.js';\nimport style from './apollo-error.module.scss';\n\nconst FOCUSABLE_SELECTORS = 'button, [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])';\n\n/**\n * Apollo Error Component that displays detailed error information in a modal.\n * This component provides a comprehensive error display interface that shows\n * GraphQL errors and other error types with detailed information for debugging purposes.\n *\n * Features:\n * - Modal overlay with backdrop for focus\n * - Detailed error categorization and display\n * - Reload functionality for error recovery\n * - Close button to dismiss the error modal\n * - Structured display of different error types\n * - Extra information display for debugging\n *\n * @returns A modal component displaying Apollo error details, or null if no error is present.\n */\nexport function ApolloErrorComponent() {\n const context = use(ApolloErrorContext);\n const { error, hideError } = context ?? {};\n const dialogRef = useRef<HTMLDivElement>(null);\n const previousFocusRef = useRef<HTMLElement | null>(null);\n\n useEffect(() => {\n if (!error || !hideError)\n return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n hideError();\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n\n return () => {\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [error, hideError]);\n\n useEffect(() => {\n if (error && dialogRef.current) {\n previousFocusRef.current = document.activeElement as HTMLElement;\n dialogRef.current.focus();\n }\n else if (!error && previousFocusRef.current) {\n previousFocusRef.current.focus();\n previousFocusRef.current = null;\n }\n }, [error]);\n\n const handleFocusTrap = (event: React.KeyboardEvent<HTMLDivElement>) => {\n if (event.key !== 'Tab' || !dialogRef.current)\n return;\n\n const focusableElements = [...dialogRef.current.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTORS)].filter(el => !el.hasAttribute('disabled'));\n\n if (focusableElements.length === 0)\n return;\n\n const firstElement = focusableElements[0];\n const lastElement = focusableElements.at(-1);\n\n if (!firstElement || !lastElement)\n return;\n\n if (event.shiftKey) {\n if (document.activeElement === firstElement || document.activeElement === dialogRef.current) {\n event.preventDefault();\n lastElement.focus();\n }\n }\n else if (document.activeElement === lastElement) {\n event.preventDefault();\n firstElement.focus();\n }\n };\n\n if (!error) {\n return null;\n }\n\n const isGraphQLError = 'locations' in error || 'path' in error || 'extensions' in error;\n const errorMessage = 'message' in error ? error.message : 'Unknown error occurred';\n\n return (\n <div className={style['modal-backdrop']} onClick={hideError}>\n <div\n ref={dialogRef}\n className={style['modal-content']}\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby=\"apollo-error-title\"\n aria-describedby=\"apollo-error-details\"\n tabIndex={-1}\n onKeyDown={handleFocusTrap}\n onClick={e => e.stopPropagation()}\n >\n <button\n type=\"button\"\n className={style['btn-close']}\n onClick={hideError}\n aria-label=\"Close error details (Esc)\"\n aria-keyshortcuts=\"Escape\"\n title=\"Close error details (Esc)\"\n >\n ✕\n </button>\n <div className={style['error-title']}>\n <button\n type=\"button\"\n className={style['btn-retry']}\n onClick={() => window.location.reload()}\n aria-label=\"Reload page\"\n >\n Reload\n </button>\n {' '}\n <span id=\"apollo-error-title\">\n {errorMessage || 'Error details'}\n </span>\n </div>\n <div id=\"apollo-error-details\" className={style['error-details']}>\n {isGraphQLError && 'locations' in error && error.locations && (\n <pre className=\"locations\">\n <strong>Locations:</strong>\n {' '}\n {JSON.stringify(error.locations, null, 4)}\n </pre>\n )}\n {isGraphQLError && 'path' in error && error.path && (\n <pre className=\"path\">\n <strong>Path:</strong>\n {' '}\n {JSON.stringify(error.path, null, 4)}\n </pre>\n )}\n {isGraphQLError && 'extensions' in error && error.extensions && (\n <pre className=\"extensions\">\n <strong>Extensions:</strong>\n {' '}\n {JSON.stringify(error.extensions, null, 4)}\n </pre>\n )}\n {!isGraphQLError && (\n <pre className=\"error-details\">\n <strong>Error Details:</strong>\n {' '}\n {validate.isEmpty(error) ? errorMessage : JSON.stringify(error, null, 4)}\n </pre>\n )}\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"apollo-error.util.js","names":[],"sources":["../../../src/react/apollo-error/apollo-error.util.tsx"],"sourcesContent":["import type { GraphQLError } from 'graphql';\n\n/**\n * Global callback function for displaying Apollo errors.\n * This variable holds the callback function that will be called when a global\n * Apollo error needs to be displayed. It is set by the ApolloErrorProvider\n * and used throughout the application for consistent error handling.\n *\n * @remarks\n * **SSR Warning:** These module-level singletons are shared across all requests
|
|
1
|
+
{"version":3,"file":"apollo-error.util.js","names":[],"sources":["../../../src/react/apollo-error/apollo-error.util.tsx"],"sourcesContent":["import type { GraphQLError } from 'graphql';\n\n/**\n * Global callback function for displaying Apollo errors.\n * This variable holds the callback function that will be called when a global\n * Apollo error needs to be displayed. It is set by the ApolloErrorProvider\n * and used throughout the application for consistent error handling.\n *\n * @remarks\n * **SSR Warning — Known Limitation:** These module-level singletons are shared\n * across all requests in SSR environments (e.g., Next.js). In server contexts,\n * Request A's error handler could be invoked for Request B's error, causing\n * intermittent wrong-user error display. This module is designed for **client-side\n * use only**. For SSR, ensure the provider is properly scoped per-request\n * or use a request-scoped error handling strategy.\n */\nlet showErrorCallback: ((err: GraphQLError | Error) => void) | null = null;\nlet hasCustomHandler = false;\n\n/**\n * Sets the global callback function for Apollo error handling.\n * This function registers a callback that will be called whenever a global\n * Apollo error needs to be displayed. The callback is typically set by the\n * ApolloErrorProvider component and used throughout the application for\n * consistent error handling.\n *\n * @param callback - The function to be called when a global Apollo error occurs.\n * @param isCustomHandler - Flag that identifies whether the handler was provided by the consumer.\n */\nexport function setGlobalApolloErrorCallback(\n callback: (err: GraphQLError | Error) => void,\n isCustomHandler = false,\n) {\n showErrorCallback = callback;\n hasCustomHandler = isCustomHandler;\n}\n\n/**\n * Clears the global callback function for Apollo error handling.\n * Useful when the ApolloErrorProvider unmounts to avoid stale references.\n */\nexport function clearGlobalApolloErrorCallback() {\n showErrorCallback = null;\n hasCustomHandler = false;\n}\n\n/**\n * Indicates whether a custom Apollo error handler is registered.\n *\n * @returns True if a custom handler is registered, otherwise false.\n */\nexport function hasCustomApolloErrorHandler() {\n return hasCustomHandler;\n}\n\n/**\n * Displays a global Apollo error using the registered callback.\n * This function triggers the global error display system by calling the\n * registered callback function. If no callback is registered, the function\n * does nothing, providing safe error handling even when the provider is not set up.\n *\n * @param error - The Apollo error to display globally.\n */\nexport function showGlobalApolloError(error: GraphQLError | Error) {\n showErrorCallback?.(error);\n}\n"],"mappings":";AAgBA,IAAI,IAAkE,MAClE,IAAmB;AAYvB,SAAgB,EACZ,GACA,IAAkB,IACpB;AAEE,CADA,IAAoB,GACpB,IAAmB;;AAOvB,SAAgB,IAAiC;AAE7C,CADA,IAAoB,MACpB,IAAmB;;AAQvB,SAAgB,IAA8B;AAC1C,QAAO;;AAWX,SAAgB,EAAsB,GAA6B;AAC/D,KAAoB,EAAM"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { i18n as I18nInstance } from 'i18next';
|
|
2
|
+
export interface I_GetTranslationsI18nextOptions {
|
|
3
|
+
/**
|
|
4
|
+
* Optional i18n instance. If not provided, it falls back to the global i18next instance.
|
|
5
|
+
*/
|
|
6
|
+
i18n?: I18nInstance;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Retrieves the i18next translation function outside of React components.
|
|
10
|
+
* This provides access to translation functions for use in server components,
|
|
11
|
+
* non-React environments, or normal JS functions.
|
|
12
|
+
*
|
|
13
|
+
* @param namespace - Optional namespace to load translations from.
|
|
14
|
+
* @param options - Options object allowing injection of a specific i18n instance.
|
|
15
|
+
* @returns The translation function (`t`) bound to the specified namespace and instance.
|
|
16
|
+
*/
|
|
17
|
+
export declare function getTranslationsI18next(namespace?: string | null, options?: I_GetTranslationsI18nextOptions): import('i18next').TFunction<string, undefined>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"i18next.server.js","names":[],"sources":["../../../src/react/i18next/i18next.server.ts"],"sourcesContent":["import type { i18n as I18nInstance } from 'i18next';\n\nimport globalI18nextInstance from 'i18next';\n\nexport interface I_GetTranslationsI18nextOptions {\n /**\n * Optional i18n instance. If not provided, it falls back to the global i18next instance.\n */\n i18n?: I18nInstance;\n}\n\n/**\n * Retrieves the i18next translation function outside of React components.\n * This provides access to translation functions for use in server components,\n * non-React environments, or normal JS functions.\n *\n * @param namespace - Optional namespace to load translations from.\n * @param options - Options object allowing injection of a specific i18n instance.\n * @returns The translation function (`t`) bound to the specified namespace and instance.\n */\nexport function getTranslationsI18next(\n namespace?: string | null,\n options?: I_GetTranslationsI18nextOptions,\n) {\n const instance = options?.i18n || globalI18nextInstance;\n\n // We bind using getFixedT to ensure the namespace is locked to this t function\n return instance.getFixedT(null, namespace ?? null);\n}\n"],"mappings":";;AAoBA,SAAgB,EACZ,GACA,GACF;AAIE,SAHiB,GAAS,QAAQ,GAGlB,UAAU,MAAM,KAAa,KAAK"}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { ComponentType } from 'react';
|
|
2
2
|
import { I_Children } from '../../typescript/index.js';
|
|
3
3
|
import { I_NextIntlLanguage, T_NextIntlMessageList } from './next-intl.type.js';
|
|
4
|
-
import * as React from 'react';
|
|
5
4
|
/**
|
|
6
5
|
* Higher-Order Component (HOC) that wraps components with Next.js internationalization support.
|
|
7
6
|
* This HOC provides internationalization capabilities to React components by wrapping them
|
|
@@ -18,10 +17,7 @@ import * as React from 'react';
|
|
|
18
17
|
* @param Component - The React component to wrap with internationalization support.
|
|
19
18
|
* @returns A new component with internationalization capabilities and additional props for languages and messages.
|
|
20
19
|
*/
|
|
21
|
-
export declare function withNextIntl<T extends I_Children>(Component: ComponentType<T>): {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}): React.JSX.Element | null;
|
|
26
|
-
displayName: string;
|
|
27
|
-
};
|
|
20
|
+
export declare function withNextIntl<T extends I_Children>(Component: ComponentType<T>): ComponentType<T & {
|
|
21
|
+
languages: I_NextIntlLanguage[];
|
|
22
|
+
messages: T_NextIntlMessageList;
|
|
23
|
+
}>;
|