- Delete old Vite+Svelte frontend - Initialize new SvelteKit project with TypeScript - Configure Tailwind CSS v4 + DaisyUI - Implement JWT authentication with auto-refresh - Create login page with form validation (Zod) - Add protected route guards - Update Docker configuration for single-stage build - Add E2E tests with Playwright (6/11 passing) - Fix Svelte 5 reactivity with $state() runes Known issues: - 5 E2E tests failing (timing/async issues) - Token refresh implementation needs debugging - Validation error display timing
156 lines
5.5 KiB
JavaScript
156 lines
5.5 KiB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
function setPath(parent, key, value) {
|
|
// Prevent prototype injection
|
|
if (key === '__proto__' || key === 'prototype') {
|
|
throw new Error("Cannot set an object's `__proto__` or `prototype` property");
|
|
}
|
|
parent[key] = value;
|
|
return 'skip';
|
|
}
|
|
function isInvalidPath(originalPath, pathData) {
|
|
return (pathData.value !== undefined &&
|
|
typeof pathData.value !== 'object' &&
|
|
pathData.path.length < originalPath.length);
|
|
}
|
|
export function pathExists(obj, path, options = {}) {
|
|
if (!options.modifier) {
|
|
options.modifier = (pathData) => (isInvalidPath(path, pathData) ? undefined : pathData.value);
|
|
}
|
|
const exists = traversePath(obj, path, options.modifier);
|
|
if (!exists)
|
|
return undefined;
|
|
if (options.value === undefined)
|
|
return exists;
|
|
return options.value(exists.value) ? exists : undefined;
|
|
}
|
|
export function traversePath(obj, realPath, modifier) {
|
|
if (!realPath.length)
|
|
return undefined;
|
|
// Prevent prototype injection
|
|
if (realPath.includes('__proto__') || realPath.includes('prototype')) {
|
|
throw new Error("Cannot set an object's `__proto__` or `prototype` property");
|
|
}
|
|
const path = [realPath[0]];
|
|
let parent = obj;
|
|
while (parent && path.length < realPath.length) {
|
|
const key = path[path.length - 1];
|
|
const value = modifier
|
|
? modifier({
|
|
parent,
|
|
key: String(key),
|
|
value: parent[key],
|
|
path: path.map((p) => String(p)),
|
|
isLeaf: false,
|
|
set: (v) => setPath(parent, key, v)
|
|
})
|
|
: parent[key];
|
|
if (value === undefined)
|
|
return undefined;
|
|
else
|
|
parent = value;
|
|
path.push(realPath[path.length]);
|
|
}
|
|
if (!parent)
|
|
return undefined;
|
|
const key = realPath[realPath.length - 1];
|
|
return {
|
|
parent,
|
|
key: String(key),
|
|
value: parent[key],
|
|
path: realPath.map((p) => String(p)),
|
|
isLeaf: true,
|
|
set: (v) => setPath(parent, key, v)
|
|
};
|
|
}
|
|
export function traversePaths(parent, modifier, path = []) {
|
|
for (const key in parent) {
|
|
const value = parent[key];
|
|
const isLeaf = value === null || typeof value !== 'object';
|
|
const pathData = {
|
|
parent,
|
|
key,
|
|
value,
|
|
path: path.concat([key]), // path.map(String).concat([key])
|
|
isLeaf,
|
|
set: (v) => setPath(parent, key, v)
|
|
};
|
|
const status = modifier(pathData);
|
|
if (status === 'abort')
|
|
return status;
|
|
else if (status === 'skip')
|
|
continue;
|
|
else if (!isLeaf) {
|
|
const status = traversePaths(value, modifier, pathData.path);
|
|
if (status === 'abort')
|
|
return status;
|
|
}
|
|
}
|
|
}
|
|
// Thanks to https://stackoverflow.com/a/31129384/70894
|
|
function eqSet(xs, ys) {
|
|
return xs === ys || (xs.size === ys.size && [...xs].every((x) => ys.has(x)));
|
|
}
|
|
/**
|
|
* Compare two objects and return the differences as paths.
|
|
*/
|
|
export function comparePaths(newObj, oldObj) {
|
|
const diffPaths = new Map();
|
|
function builtInDiff(one, other) {
|
|
if (one instanceof Date && other instanceof Date && one.getTime() !== other.getTime())
|
|
return true;
|
|
if (one instanceof Set && other instanceof Set && !eqSet(one, other))
|
|
return true;
|
|
if (one instanceof File && other instanceof File && one !== other)
|
|
return true;
|
|
return false;
|
|
}
|
|
function isBuiltin(data) {
|
|
return data instanceof Date || data instanceof Set || data instanceof File;
|
|
}
|
|
function checkPath(data, compareTo) {
|
|
const otherData = compareTo ? traversePath(compareTo, data.path) : undefined;
|
|
//console.log('Compare', data.path, data.value, 'to', otherData?.path, otherData?.value);
|
|
function addDiff() {
|
|
//console.log('Diff', data.path);
|
|
diffPaths.set(data.path.join(' '), data.path);
|
|
return 'skip';
|
|
}
|
|
if (isBuiltin(data.value)) {
|
|
if (!isBuiltin(otherData?.value) || builtInDiff(data.value, otherData.value)) {
|
|
return addDiff();
|
|
}
|
|
}
|
|
if (data.isLeaf) {
|
|
if (!otherData || data.value !== otherData.value) {
|
|
addDiff();
|
|
}
|
|
}
|
|
}
|
|
traversePaths(newObj, (data) => checkPath(data, oldObj));
|
|
traversePaths(oldObj, (data) => checkPath(data, newObj));
|
|
// Need to sort the list so the shortest paths comes first
|
|
const output = Array.from(diffPaths.values());
|
|
output.sort((a, b) => a.length - b.length);
|
|
return output;
|
|
}
|
|
export function setPaths(obj, paths, value) {
|
|
const isFunction = typeof value === 'function';
|
|
for (const path of paths) {
|
|
const leaf = traversePath(obj, path, ({ parent, key, value }) => {
|
|
if (value === undefined || typeof value !== 'object') {
|
|
// If a previous check tainted the node, but the search goes deeper,
|
|
// so it needs to be replaced with a (parent) node
|
|
parent[key] = {};
|
|
}
|
|
return parent[key];
|
|
});
|
|
if (leaf) {
|
|
// Prevent prototype injection
|
|
if (leaf.key === '__proto__' || leaf.key === 'prototype') {
|
|
throw new Error("Cannot set an object's `__proto__` or `prototype` property");
|
|
}
|
|
leaf.parent[leaf.key] = isFunction ? value(path, leaf) : value;
|
|
}
|
|
}
|
|
}
|