- 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
1134 lines
34 KiB
JavaScript
1134 lines
34 KiB
JavaScript
// src/compiler/buffer.ts
|
|
var CompilerBuffer = class _CompilerBuffer {
|
|
#content = "";
|
|
/**
|
|
* The character used to create a new line
|
|
*/
|
|
newLine = "\n";
|
|
/**
|
|
* Write statement ot the output
|
|
*/
|
|
writeStatement(statement) {
|
|
this.#content = `${this.#content}${this.newLine}${statement}`;
|
|
}
|
|
/**
|
|
* Creates a child buffer
|
|
*/
|
|
child() {
|
|
return new _CompilerBuffer();
|
|
}
|
|
/**
|
|
* Returns the buffer contents as string
|
|
*/
|
|
toString() {
|
|
return this.#content;
|
|
}
|
|
/**
|
|
* Flush in-memory string
|
|
*/
|
|
flush() {
|
|
this.#content = "";
|
|
}
|
|
};
|
|
|
|
// src/scripts/field/variables.ts
|
|
function defineFieldVariables({
|
|
parseFnRefId,
|
|
variableName,
|
|
wildCardPath,
|
|
isArrayMember,
|
|
valueExpression,
|
|
parentExpression,
|
|
fieldNameExpression,
|
|
parentValueExpression
|
|
}) {
|
|
const inValueExpression = parseFnRefId ? `refs['${parseFnRefId}'](${valueExpression}, {
|
|
data: root,
|
|
meta: meta,
|
|
parent: ${parentValueExpression}
|
|
})` : valueExpression;
|
|
let fieldPathOutputExpression = "";
|
|
if (parentExpression === "root" || parentExpression === "root_item") {
|
|
fieldPathOutputExpression = fieldNameExpression;
|
|
} else if (fieldNameExpression !== "''") {
|
|
fieldPathOutputExpression = `${parentExpression}.getFieldPath() + '.' + ${fieldNameExpression}`;
|
|
}
|
|
return `const ${variableName} = defineValue(${inValueExpression}, {
|
|
data: root,
|
|
meta: meta,
|
|
name: ${fieldNameExpression},
|
|
wildCardPath: '${wildCardPath}',
|
|
getFieldPath() {
|
|
return ${fieldPathOutputExpression};
|
|
},
|
|
mutate: defineValue,
|
|
report: report,
|
|
isValid: true,
|
|
parent: ${parentValueExpression},
|
|
isArrayMember: ${isArrayMember},
|
|
});`;
|
|
}
|
|
|
|
// src/compiler/nodes/base.ts
|
|
var BaseNode = class {
|
|
#node;
|
|
#parentField;
|
|
field;
|
|
constructor(node, compiler, parent, parentField) {
|
|
this.#parentField = parentField;
|
|
this.#node = node;
|
|
if (this.#parentField) {
|
|
this.field = this.#parentField;
|
|
} else {
|
|
compiler.variablesCounter++;
|
|
this.field = compiler.createFieldFor(node, parent);
|
|
}
|
|
}
|
|
defineField(buffer) {
|
|
if (!this.#parentField) {
|
|
buffer.writeStatement(
|
|
defineFieldVariables({
|
|
fieldNameExpression: this.field.fieldNameExpression,
|
|
isArrayMember: this.field.isArrayMember,
|
|
parentExpression: this.field.parentExpression,
|
|
parentValueExpression: this.field.parentValueExpression,
|
|
valueExpression: this.field.valueExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath,
|
|
parseFnRefId: "parseFnId" in this.#node ? this.#node.parseFnId : void 0
|
|
})
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/scripts/array/guard.ts
|
|
function defineArrayGuard({ variableName, guardedCodeSnippet }) {
|
|
return `if (${variableName}_is_array) {
|
|
${guardedCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/field/is_valid_guard.ts
|
|
function defineIsValidGuard({ variableName, bail, guardedCodeSnippet }) {
|
|
if (!bail) {
|
|
return guardedCodeSnippet;
|
|
}
|
|
return `if (${variableName}.isValid) {
|
|
${guardedCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/field/null_output.ts
|
|
function defineFieldNullOutput({
|
|
allowNull,
|
|
conditional,
|
|
variableName,
|
|
outputExpression,
|
|
transformFnRefId
|
|
}) {
|
|
if (!allowNull) {
|
|
return "";
|
|
}
|
|
return `${conditional || "if"}(${variableName}.value === null) {
|
|
${outputExpression} = ${transformFnRefId ? `refs['${transformFnRefId}'](null, ${variableName});` : "null;"}
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/field/validations.ts
|
|
function wrapInConditional(conditions, wrappingCode) {
|
|
const [first, second] = conditions;
|
|
if (first && second) {
|
|
return `if (${first} && ${second}) {
|
|
${wrappingCode}
|
|
}`;
|
|
}
|
|
if (first) {
|
|
return `if (${first}) {
|
|
${wrappingCode}
|
|
}`;
|
|
}
|
|
if (second) {
|
|
return `if (${second}) {
|
|
${wrappingCode}
|
|
}`;
|
|
}
|
|
return wrappingCode;
|
|
}
|
|
function emitValidationSnippet({ isAsync, implicit, ruleFnId }, variableName, bail, dropMissingCheck, existenceCheckExpression) {
|
|
const rule = `refs['${ruleFnId}']`;
|
|
const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName});`;
|
|
existenceCheckExpression = existenceCheckExpression || `${variableName}.isDefined`;
|
|
const bailCondition = bail ? `${variableName}.isValid` : "";
|
|
const implicitCondition = implicit || dropMissingCheck ? "" : existenceCheckExpression;
|
|
return wrapInConditional(
|
|
[bailCondition, implicitCondition],
|
|
isAsync ? `await ${callable}` : `${callable}`
|
|
);
|
|
}
|
|
function defineFieldValidations({
|
|
bail,
|
|
validations,
|
|
variableName,
|
|
dropMissingCheck,
|
|
existenceCheckExpression
|
|
}) {
|
|
return `${validations.map(
|
|
(one) => emitValidationSnippet(one, variableName, bail, dropMissingCheck, existenceCheckExpression)
|
|
).join("\n")}`;
|
|
}
|
|
|
|
// src/scripts/array/initial_output.ts
|
|
function defineArrayInitialOutput({
|
|
variableName,
|
|
outputExpression,
|
|
outputValueExpression
|
|
}) {
|
|
return `const ${variableName}_out = ${outputValueExpression};
|
|
${outputExpression} = ${variableName}_out;`;
|
|
}
|
|
|
|
// src/scripts/field/existence_validations.ts
|
|
function defineFieldExistenceValidations({
|
|
allowNull,
|
|
isOptional,
|
|
variableName
|
|
}) {
|
|
if (isOptional === false) {
|
|
if (allowNull === false) {
|
|
return `ensureExists(${variableName});`;
|
|
} else {
|
|
return `ensureIsDefined(${variableName});`;
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// src/scripts/array/variables.ts
|
|
function defineArrayVariables({ variableName }) {
|
|
return `const ${variableName}_is_array = ensureIsArray(${variableName});`;
|
|
}
|
|
|
|
// src/compiler/nodes/tuple.ts
|
|
var TupleNodeCompiler = class extends BaseNode {
|
|
#node;
|
|
#buffer;
|
|
#compiler;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
this.#compiler = compiler;
|
|
}
|
|
/**
|
|
* Compiles the tuple children to a JS fragment
|
|
*/
|
|
#compileTupleChildren() {
|
|
const buffer = this.#buffer.child();
|
|
const parent = {
|
|
type: "tuple",
|
|
fieldPathExpression: this.field.fieldPathExpression,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath
|
|
};
|
|
this.#node.properties.forEach((child) => {
|
|
this.#compiler.compileNode(child, buffer, parent);
|
|
});
|
|
return buffer.toString();
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(
|
|
defineFieldExistenceValidations({
|
|
allowNull: this.#node.allowNull,
|
|
isOptional: this.#node.isOptional,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineArrayVariables({
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineFieldValidations({
|
|
variableName: this.field.variableName,
|
|
validations: this.#node.validations,
|
|
bail: this.#node.bail,
|
|
dropMissingCheck: false,
|
|
existenceCheckExpression: `${this.field.variableName}_is_array`
|
|
})
|
|
);
|
|
const isArrayValidBlock = defineArrayGuard({
|
|
variableName: this.field.variableName,
|
|
guardedCodeSnippet: `${this.#buffer.newLine}${defineIsValidGuard({
|
|
variableName: this.field.variableName,
|
|
bail: this.#node.bail,
|
|
guardedCodeSnippet: `${defineArrayInitialOutput({
|
|
variableName: this.field.variableName,
|
|
outputExpression: this.field.outputExpression,
|
|
outputValueExpression: this.#node.allowUnknownProperties ? `copyProperties(${this.field.variableName}.value)` : `[]`
|
|
})}${this.#buffer.newLine}${this.#compileTupleChildren()}`
|
|
})}`
|
|
});
|
|
this.#buffer.writeStatement(
|
|
`${isArrayValidBlock}${this.#buffer.newLine}${defineFieldNullOutput({
|
|
allowNull: this.#node.allowNull,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
conditional: "else if"
|
|
})}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/scripts/array/loop.ts
|
|
function defineArrayLoop({
|
|
variableName,
|
|
loopCodeSnippet,
|
|
startingIndex
|
|
}) {
|
|
startingIndex = startingIndex || 0;
|
|
return `const ${variableName}_items_size = ${variableName}.value.length;
|
|
for (let ${variableName}_i = ${startingIndex}; ${variableName}_i < ${variableName}_items_size; ${variableName}_i++) {
|
|
${loopCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/compiler/nodes/array.ts
|
|
var ArrayNodeCompiler = class extends BaseNode {
|
|
#node;
|
|
#buffer;
|
|
#compiler;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
this.#compiler = compiler;
|
|
}
|
|
/**
|
|
* Compiles the array elements to a JS fragment
|
|
*/
|
|
#compileArrayElements() {
|
|
const arrayElementsBuffer = this.#buffer.child();
|
|
this.#compiler.compileNode(this.#node.each, arrayElementsBuffer, {
|
|
type: "array",
|
|
fieldPathExpression: this.field.fieldPathExpression,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath
|
|
});
|
|
const buffer = this.#buffer.child();
|
|
buffer.writeStatement(
|
|
defineArrayLoop({
|
|
variableName: this.field.variableName,
|
|
startingIndex: 0,
|
|
loopCodeSnippet: arrayElementsBuffer.toString()
|
|
})
|
|
);
|
|
arrayElementsBuffer.flush();
|
|
return buffer.toString();
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(
|
|
defineFieldExistenceValidations({
|
|
allowNull: this.#node.allowNull,
|
|
isOptional: this.#node.isOptional,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineArrayVariables({
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineFieldValidations({
|
|
variableName: this.field.variableName,
|
|
validations: this.#node.validations,
|
|
bail: this.#node.bail,
|
|
dropMissingCheck: false,
|
|
existenceCheckExpression: `${this.field.variableName}_is_array`
|
|
})
|
|
);
|
|
const isArrayValidBlock = defineArrayGuard({
|
|
variableName: this.field.variableName,
|
|
guardedCodeSnippet: `${this.#buffer.newLine}${defineIsValidGuard({
|
|
variableName: this.field.variableName,
|
|
bail: this.#node.bail,
|
|
guardedCodeSnippet: `${defineArrayInitialOutput({
|
|
variableName: this.field.variableName,
|
|
outputExpression: this.field.outputExpression,
|
|
outputValueExpression: `[]`
|
|
})}${this.#buffer.newLine}${this.#compileArrayElements()}`
|
|
})}`
|
|
});
|
|
this.#buffer.writeStatement(
|
|
`${isArrayValidBlock}${this.#buffer.newLine}${defineFieldNullOutput({
|
|
allowNull: this.#node.allowNull,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
conditional: "else if"
|
|
})}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/scripts/union/parse.ts
|
|
function callParseFunction({ parseFnRefId, variableName }) {
|
|
if (parseFnRefId) {
|
|
return `${variableName}.value = refs['${parseFnRefId}'](${variableName}.value);`;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
// src/scripts/define_else_conditon.ts
|
|
function defineElseCondition({ variableName, conditionalFnRefId }) {
|
|
return `else {
|
|
refs['${conditionalFnRefId}'](${variableName}.value, ${variableName});
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/define_conditional_guard.ts
|
|
function defineConditionalGuard({
|
|
conditional,
|
|
variableName,
|
|
conditionalFnRefId,
|
|
guardedCodeSnippet
|
|
}) {
|
|
return `${conditional}(refs['${conditionalFnRefId}'](${variableName}.value, ${variableName})) {
|
|
${guardedCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/compiler/nodes/union.ts
|
|
var UnionNodeCompiler = class extends BaseNode {
|
|
#compiler;
|
|
#node;
|
|
#buffer;
|
|
#parent;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
this.#parent = parent;
|
|
this.#compiler = compiler;
|
|
}
|
|
/**
|
|
* Compiles union children by wrapping each conditon inside a conditional
|
|
* guard block
|
|
*/
|
|
#compileUnionChildren() {
|
|
const childrenBuffer = this.#buffer.child();
|
|
this.#node.conditions.forEach((child, index) => {
|
|
const conditionalBuffer = this.#buffer.child();
|
|
if ("parseFnId" in child.schema) {
|
|
conditionalBuffer.writeStatement(
|
|
callParseFunction({
|
|
parseFnRefId: child.schema.parseFnId,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
}
|
|
this.#compiler.compileNode(child.schema, conditionalBuffer, this.#parent, this.field);
|
|
childrenBuffer.writeStatement(
|
|
defineConditionalGuard({
|
|
conditional: index === 0 ? "if" : "else if",
|
|
variableName: this.field.variableName,
|
|
conditionalFnRefId: child.conditionalFnRefId,
|
|
guardedCodeSnippet: conditionalBuffer.toString()
|
|
})
|
|
);
|
|
conditionalBuffer.flush();
|
|
});
|
|
if (this.#node.elseConditionalFnRefId && this.#node.conditions.length) {
|
|
childrenBuffer.writeStatement(
|
|
defineElseCondition({
|
|
variableName: this.field.variableName,
|
|
conditionalFnRefId: this.#node.elseConditionalFnRefId
|
|
})
|
|
);
|
|
}
|
|
return childrenBuffer.toString();
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(this.#compileUnionChildren());
|
|
}
|
|
};
|
|
|
|
// src/scripts/record/loop.ts
|
|
function defineRecordLoop({ variableName, loopCodeSnippet }) {
|
|
return `const ${variableName}_keys = Object.keys(${variableName}.value);
|
|
const ${variableName}_keys_size = ${variableName}_keys.length;
|
|
for (let ${variableName}_key_i = 0; ${variableName}_key_i < ${variableName}_keys_size; ${variableName}_key_i++) {
|
|
const ${variableName}_i = ${variableName}_keys[${variableName}_key_i];
|
|
${loopCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/object/guard.ts
|
|
function defineObjectGuard({ variableName, guardedCodeSnippet }) {
|
|
return `if (${variableName}_is_object) {
|
|
${guardedCodeSnippet}
|
|
}`;
|
|
}
|
|
|
|
// src/scripts/object/initial_output.ts
|
|
function defineObjectInitialOutput({
|
|
variableName,
|
|
outputExpression,
|
|
outputValueExpression
|
|
}) {
|
|
return `const ${variableName}_out = ${outputValueExpression};
|
|
${outputExpression} = ${variableName}_out;`;
|
|
}
|
|
|
|
// src/scripts/object/variables.ts
|
|
function defineObjectVariables({ variableName }) {
|
|
return `const ${variableName}_is_object = ensureIsObject(${variableName});`;
|
|
}
|
|
|
|
// src/compiler/nodes/record.ts
|
|
var RecordNodeCompiler = class extends BaseNode {
|
|
#node;
|
|
#buffer;
|
|
#compiler;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
this.#compiler = compiler;
|
|
}
|
|
/**
|
|
* Compiles the record elements to a JS fragment
|
|
*/
|
|
#compileRecordElements() {
|
|
const buffer = this.#buffer.child();
|
|
const recordElementsBuffer = this.#buffer.child();
|
|
this.#compiler.compileNode(this.#node.each, recordElementsBuffer, {
|
|
type: "record",
|
|
fieldPathExpression: this.field.fieldPathExpression,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath
|
|
});
|
|
buffer.writeStatement(
|
|
defineRecordLoop({
|
|
variableName: this.field.variableName,
|
|
loopCodeSnippet: recordElementsBuffer.toString()
|
|
})
|
|
);
|
|
recordElementsBuffer.flush();
|
|
return buffer.toString();
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(
|
|
defineFieldExistenceValidations({
|
|
allowNull: this.#node.allowNull,
|
|
isOptional: this.#node.isOptional,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineObjectVariables({
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineFieldValidations({
|
|
variableName: this.field.variableName,
|
|
validations: this.#node.validations,
|
|
bail: this.#node.bail,
|
|
dropMissingCheck: false,
|
|
existenceCheckExpression: `${this.field.variableName}_is_object`
|
|
})
|
|
);
|
|
const isObjectValidBlock = defineIsValidGuard({
|
|
variableName: this.field.variableName,
|
|
bail: this.#node.bail,
|
|
guardedCodeSnippet: `${defineObjectInitialOutput({
|
|
variableName: this.field.variableName,
|
|
outputExpression: this.field.outputExpression,
|
|
outputValueExpression: `{}`
|
|
})}${this.#compileRecordElements()}`
|
|
});
|
|
const isValueAnObjectBlock = defineObjectGuard({
|
|
variableName: this.field.variableName,
|
|
guardedCodeSnippet: `${this.#buffer.newLine}${isObjectValidBlock}`
|
|
});
|
|
this.#buffer.writeStatement(
|
|
`${isValueAnObjectBlock}${this.#buffer.newLine}${defineFieldNullOutput({
|
|
allowNull: this.#node.allowNull,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
conditional: "else if"
|
|
})}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/scripts/object/move_unknown_properties.ts
|
|
function arrayToString(arr) {
|
|
return `[${arr.map((str) => `"${str}"`).join(", ")}]`;
|
|
}
|
|
function defineMoveProperties({
|
|
variableName,
|
|
fieldsToIgnore,
|
|
allowUnknownProperties
|
|
}) {
|
|
if (!allowUnknownProperties) {
|
|
return "";
|
|
}
|
|
const serializedFieldsToIgnore = arrayToString(fieldsToIgnore);
|
|
return `moveProperties(${variableName}.value, ${variableName}_out, ${serializedFieldsToIgnore});`;
|
|
}
|
|
|
|
// src/compiler/nodes/object.ts
|
|
var ObjectNodeCompiler = class extends BaseNode {
|
|
#node;
|
|
#buffer;
|
|
#compiler;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
this.#compiler = compiler;
|
|
}
|
|
/**
|
|
* Returns known field names for the object
|
|
*/
|
|
#getFieldNames(node) {
|
|
let fieldNames = node.properties.map((child) => child.fieldName);
|
|
const groupsFieldNames = node.groups.flatMap((group) => this.#getGroupFieldNames(group));
|
|
return fieldNames.concat(groupsFieldNames);
|
|
}
|
|
/**
|
|
* Returns field names of a group.
|
|
*/
|
|
#getGroupFieldNames(group) {
|
|
return group.conditions.flatMap((condition) => {
|
|
return this.#getFieldNames(condition.schema);
|
|
});
|
|
}
|
|
/**
|
|
* Compiles object children to JS output
|
|
*/
|
|
#compileObjectChildren() {
|
|
const buffer = this.#buffer.child();
|
|
const parent = {
|
|
type: "object",
|
|
fieldPathExpression: this.field.fieldPathExpression,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath
|
|
};
|
|
this.#node.properties.forEach((child) => this.#compiler.compileNode(child, buffer, parent));
|
|
return buffer.toString();
|
|
}
|
|
/**
|
|
* Compiles object groups with conditions to JS output.
|
|
*/
|
|
#compileObjectGroups() {
|
|
const buffer = this.#buffer.child();
|
|
const parent = {
|
|
type: "object",
|
|
fieldPathExpression: this.field.fieldPathExpression,
|
|
outputExpression: this.field.outputExpression,
|
|
variableName: this.field.variableName,
|
|
wildCardPath: this.field.wildCardPath
|
|
};
|
|
this.#node.groups.forEach((group) => this.#compileObjectGroup(group, buffer, parent));
|
|
return buffer.toString();
|
|
}
|
|
/**
|
|
* Compiles an object groups recursively
|
|
*/
|
|
#compileObjectGroup(group, buffer, parent) {
|
|
group.conditions.forEach((condition, index) => {
|
|
const guardBuffer = buffer.child();
|
|
condition.schema.properties.forEach((child) => {
|
|
this.#compiler.compileNode(child, guardBuffer, parent);
|
|
});
|
|
condition.schema.groups.forEach((child) => {
|
|
this.#compileObjectGroup(child, guardBuffer, parent);
|
|
});
|
|
buffer.writeStatement(
|
|
defineConditionalGuard({
|
|
variableName: this.field.variableName,
|
|
conditional: index === 0 ? "if" : "else if",
|
|
conditionalFnRefId: condition.conditionalFnRefId,
|
|
guardedCodeSnippet: guardBuffer.toString()
|
|
})
|
|
);
|
|
});
|
|
if (group.elseConditionalFnRefId && group.conditions.length) {
|
|
buffer.writeStatement(
|
|
defineElseCondition({
|
|
variableName: this.field.variableName,
|
|
conditionalFnRefId: group.elseConditionalFnRefId
|
|
})
|
|
);
|
|
}
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(
|
|
defineFieldExistenceValidations({
|
|
allowNull: this.#node.allowNull,
|
|
isOptional: this.#node.isOptional,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineObjectVariables({
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineFieldValidations({
|
|
variableName: this.field.variableName,
|
|
validations: this.#node.validations,
|
|
bail: this.#node.bail,
|
|
dropMissingCheck: false,
|
|
existenceCheckExpression: `${this.field.variableName}_is_object`
|
|
})
|
|
);
|
|
const isObjectValidBlock = defineIsValidGuard({
|
|
variableName: this.field.variableName,
|
|
bail: this.#node.bail,
|
|
guardedCodeSnippet: `${defineObjectInitialOutput({
|
|
variableName: this.field.variableName,
|
|
outputExpression: this.field.outputExpression,
|
|
outputValueExpression: "{}"
|
|
})}${this.#buffer.newLine}${this.#compileObjectChildren()}${this.#buffer.newLine}${this.#compileObjectGroups()}${this.#buffer.newLine}${defineMoveProperties({
|
|
variableName: this.field.variableName,
|
|
allowUnknownProperties: this.#node.allowUnknownProperties,
|
|
fieldsToIgnore: this.#node.allowUnknownProperties ? this.#getFieldNames(this.#node) : []
|
|
})}`
|
|
});
|
|
const isValueAnObject = defineObjectGuard({
|
|
variableName: this.field.variableName,
|
|
guardedCodeSnippet: `${isObjectValidBlock}`
|
|
});
|
|
this.#buffer.writeStatement(
|
|
`${isValueAnObject}${this.#buffer.newLine}${defineFieldNullOutput({
|
|
variableName: this.field.variableName,
|
|
allowNull: this.#node.allowNull,
|
|
outputExpression: this.field.outputExpression,
|
|
conditional: "else if"
|
|
})}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/compiler/fields/root_field.ts
|
|
function createRootField(parent) {
|
|
return {
|
|
parentExpression: parent.variableName,
|
|
parentValueExpression: parent.variableName,
|
|
fieldNameExpression: `''`,
|
|
fieldPathExpression: `''`,
|
|
wildCardPath: "",
|
|
variableName: `${parent.variableName}_item`,
|
|
valueExpression: "root",
|
|
outputExpression: parent.outputExpression,
|
|
isArrayMember: false
|
|
};
|
|
}
|
|
|
|
// src/scripts/field/value_output.ts
|
|
function defineFieldValueOutput({
|
|
variableName,
|
|
outputExpression,
|
|
transformFnRefId
|
|
}) {
|
|
const outputValueExpression = transformFnRefId ? `refs['${transformFnRefId}'](${variableName}.value, ${variableName})` : `${variableName}.value`;
|
|
return `if (${variableName}.isDefined && ${variableName}.isValid) {
|
|
${outputExpression} = ${outputValueExpression};
|
|
}`;
|
|
}
|
|
|
|
// src/compiler/nodes/literal.ts
|
|
var LiteralNodeCompiler = class extends BaseNode {
|
|
#node;
|
|
#buffer;
|
|
constructor(node, buffer, compiler, parent, parentField) {
|
|
super(node, compiler, parent, parentField);
|
|
this.#node = node;
|
|
this.#buffer = buffer;
|
|
}
|
|
compile() {
|
|
this.defineField(this.#buffer);
|
|
this.#buffer.writeStatement(
|
|
defineFieldExistenceValidations({
|
|
allowNull: this.#node.allowNull,
|
|
isOptional: this.#node.isOptional,
|
|
variableName: this.field.variableName
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
defineFieldValidations({
|
|
variableName: this.field.variableName,
|
|
validations: this.#node.validations,
|
|
bail: this.#node.bail,
|
|
dropMissingCheck: false
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(
|
|
`${defineFieldValueOutput({
|
|
variableName: this.field.variableName,
|
|
outputExpression: this.field.outputExpression,
|
|
transformFnRefId: this.#node.transformFnId
|
|
})}${this.#buffer.newLine}${defineFieldNullOutput({
|
|
variableName: this.field.variableName,
|
|
allowNull: this.#node.allowNull,
|
|
outputExpression: this.field.outputExpression,
|
|
transformFnRefId: this.#node.transformFnId,
|
|
conditional: "else if"
|
|
})}`
|
|
);
|
|
}
|
|
};
|
|
|
|
// src/compiler/fields/array_field.ts
|
|
function createArrayField(parent) {
|
|
const wildCardPath = parent.wildCardPath !== "" ? `${parent.wildCardPath}.*` : `*`;
|
|
return {
|
|
parentExpression: parent.variableName,
|
|
parentValueExpression: `${parent.variableName}.value`,
|
|
fieldNameExpression: `${parent.variableName}_i`,
|
|
fieldPathExpression: wildCardPath,
|
|
wildCardPath,
|
|
variableName: `${parent.variableName}_item`,
|
|
valueExpression: `${parent.variableName}.value[${parent.variableName}_i]`,
|
|
outputExpression: `${parent.variableName}_out[${parent.variableName}_i]`,
|
|
isArrayMember: true
|
|
};
|
|
}
|
|
|
|
// src/compiler/fields/tuple_field.ts
|
|
function createTupleField(node, parent) {
|
|
const wildCardPath = parent.wildCardPath !== "" ? `${parent.wildCardPath}.${node.fieldName}` : node.fieldName;
|
|
return {
|
|
parentExpression: parent.variableName,
|
|
parentValueExpression: `${parent.variableName}.value`,
|
|
fieldNameExpression: `${node.fieldName}`,
|
|
fieldPathExpression: wildCardPath,
|
|
wildCardPath,
|
|
variableName: `${parent.variableName}_item_${node.fieldName}`,
|
|
valueExpression: `${parent.variableName}.value[${node.fieldName}]`,
|
|
outputExpression: `${parent.variableName}_out[${node.propertyName}]`,
|
|
isArrayMember: true
|
|
};
|
|
}
|
|
|
|
// src/scripts/report_errors.ts
|
|
function reportErrors() {
|
|
return `if(errorReporter.hasErrors) {
|
|
throw errorReporter.createError();
|
|
}`;
|
|
}
|
|
|
|
// src/helpers.ts
|
|
var NUMBER_CHAR_RE = /\d/;
|
|
var VALID_CHARS = /[A-Za-z0-9]+/;
|
|
function isUppercase(char = "") {
|
|
if (NUMBER_CHAR_RE.test(char)) {
|
|
return void 0;
|
|
}
|
|
return char !== char.toLowerCase();
|
|
}
|
|
function upperFirst(value) {
|
|
return value ? value[0].toUpperCase() + value.slice(1) : "";
|
|
}
|
|
function lowerFirst(value) {
|
|
return value ? value[0].toLowerCase() + value.slice(1) : "";
|
|
}
|
|
function splitByCase(value) {
|
|
const parts = [];
|
|
if (!value || typeof value !== "string") {
|
|
return parts;
|
|
}
|
|
let buff = "";
|
|
let previousUpper;
|
|
let previousSplitter;
|
|
for (const char of value) {
|
|
const isSplitter = !VALID_CHARS.test(char);
|
|
if (isSplitter === true) {
|
|
parts.push(buff);
|
|
buff = "";
|
|
previousUpper = void 0;
|
|
continue;
|
|
}
|
|
const isUpper = isUppercase(char);
|
|
if (previousSplitter === false) {
|
|
if (previousUpper === false && isUpper === true) {
|
|
parts.push(buff);
|
|
buff = char;
|
|
previousUpper = isUpper;
|
|
continue;
|
|
}
|
|
if (previousUpper === true && isUpper === false && buff.length > 1) {
|
|
const lastChar = buff.at(-1);
|
|
parts.push(buff.slice(0, Math.max(0, buff.length - 1)));
|
|
buff = lastChar + char;
|
|
previousUpper = isUpper;
|
|
continue;
|
|
}
|
|
}
|
|
buff += char;
|
|
previousUpper = isUpper;
|
|
previousSplitter = isSplitter;
|
|
}
|
|
parts.push(buff);
|
|
return parts;
|
|
}
|
|
function toVariableName(value) {
|
|
const pascalCase = splitByCase(value).map((p) => upperFirst(p.toLowerCase())).join("");
|
|
return /^[0-9]+/.test(pascalCase) ? `var_${pascalCase}` : lowerFirst(pascalCase);
|
|
}
|
|
|
|
// src/compiler/fields/object_field.ts
|
|
function createObjectField(node, variablesCounter, parent) {
|
|
const wildCardPath = parent.wildCardPath !== "" ? `${parent.wildCardPath}.${node.fieldName}` : node.fieldName;
|
|
return {
|
|
parentExpression: parent.variableName,
|
|
parentValueExpression: `${parent.variableName}.value`,
|
|
fieldNameExpression: `'${node.fieldName}'`,
|
|
fieldPathExpression: wildCardPath,
|
|
wildCardPath,
|
|
variableName: `${toVariableName(node.propertyName)}_${variablesCounter}`,
|
|
valueExpression: `${parent.variableName}.value['${node.fieldName}']`,
|
|
outputExpression: `${parent.variableName}_out['${node.propertyName}']`,
|
|
isArrayMember: false
|
|
};
|
|
}
|
|
|
|
// src/compiler/fields/record_field.ts
|
|
function createRecordField(parent) {
|
|
const wildCardPath = parent.wildCardPath !== "" ? `${parent.wildCardPath}.*` : `*`;
|
|
return {
|
|
parentExpression: parent.variableName,
|
|
parentValueExpression: `${parent.variableName}.value`,
|
|
fieldNameExpression: `${parent.variableName}_i`,
|
|
fieldPathExpression: wildCardPath,
|
|
wildCardPath,
|
|
variableName: `${parent.variableName}_item`,
|
|
valueExpression: `${parent.variableName}.value[${parent.variableName}_i]`,
|
|
outputExpression: `${parent.variableName}_out[${parent.variableName}_i]`,
|
|
isArrayMember: false
|
|
};
|
|
}
|
|
|
|
// src/scripts/define_inline_functions.ts
|
|
function defineInlineFunctions(options) {
|
|
return `function report(message, rule, field, args) {
|
|
field.isValid = false;
|
|
errorReporter.report(messagesProvider.getMessage(message, rule, field, args), rule, field, args);
|
|
};
|
|
function defineValue(value, field) {
|
|
${options.convertEmptyStringsToNull ? `if (value === '') { value = null; }` : ""}
|
|
field.value = value;
|
|
field.isDefined = value !== undefined && value !== null;
|
|
return field;
|
|
};
|
|
function ensureExists(field) {
|
|
if (field.value === undefined || field.value === null) {
|
|
field.report(REQUIRED, 'required', field);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
function ensureIsDefined(field) {
|
|
if (field.value === undefined) {
|
|
field.report(REQUIRED, 'required', field);
|
|
return false;
|
|
}
|
|
return true;
|
|
};
|
|
function ensureIsObject(field) {
|
|
if (!field.isDefined) {
|
|
return false;
|
|
}
|
|
if (typeof field.value == 'object' && !Array.isArray(field.value)) {
|
|
return true;
|
|
}
|
|
field.report(NOT_AN_OBJECT, 'object', field);
|
|
return false;
|
|
};
|
|
function ensureIsArray(field) {
|
|
if (!field.isDefined) {
|
|
return false;
|
|
}
|
|
if (Array.isArray(field.value)) {
|
|
return true;
|
|
}
|
|
field.report(NOT_AN_ARRAY, 'array', field);
|
|
return false;
|
|
};
|
|
function copyProperties(val) {
|
|
let k, out, tmp;
|
|
|
|
if (Array.isArray(val)) {
|
|
out = Array((k = val.length))
|
|
while (k--) out[k] = (tmp = val[k]) && typeof tmp == 'object' ? copyProperties(tmp) : tmp
|
|
return out
|
|
}
|
|
|
|
if (Object.prototype.toString.call(val) === '[object Object]') {
|
|
out = {} // null
|
|
for (k in val) {
|
|
out[k] = (tmp = val[k]) && typeof tmp == 'object' ? copyProperties(tmp) : tmp
|
|
}
|
|
return out
|
|
}
|
|
return val
|
|
};
|
|
function moveProperties(source, destination, ignoreKeys) {
|
|
for (let key in source) {
|
|
if (!ignoreKeys.includes(key)) {
|
|
const value = source[key]
|
|
destination[key] = copyProperties(value)
|
|
}
|
|
}
|
|
};`;
|
|
}
|
|
|
|
// src/scripts/define_error_messages.ts
|
|
function defineInlineErrorMessages(messages) {
|
|
return `const REQUIRED = '${messages.required}';
|
|
const NOT_AN_OBJECT = '${messages.object}';
|
|
const NOT_AN_ARRAY = '${messages.array}';`;
|
|
}
|
|
|
|
// src/compiler/main.ts
|
|
var AsyncFunction = Object.getPrototypeOf(async function() {
|
|
}).constructor;
|
|
var Compiler = class {
|
|
/**
|
|
* Variables counter is used to generate unique variable
|
|
* names with a counter suffix.
|
|
*/
|
|
variablesCounter = 0;
|
|
/**
|
|
* An array of nodes to process
|
|
*/
|
|
#rootNode;
|
|
/**
|
|
* Options to configure the compiler behavior
|
|
*/
|
|
#options;
|
|
/**
|
|
* Buffer for collection the JS output string
|
|
*/
|
|
#buffer = new CompilerBuffer();
|
|
constructor(rootNode, options) {
|
|
this.#rootNode = rootNode;
|
|
this.#options = options || { convertEmptyStringsToNull: false };
|
|
}
|
|
/**
|
|
* Initiates the JS output
|
|
*/
|
|
#initiateJSOutput() {
|
|
this.#buffer.writeStatement(
|
|
defineInlineErrorMessages({
|
|
required: "value is required",
|
|
object: "value is not a valid object",
|
|
array: "value is not a valid array",
|
|
...this.#options.messages
|
|
})
|
|
);
|
|
this.#buffer.writeStatement(defineInlineFunctions(this.#options));
|
|
this.#buffer.writeStatement("let out;");
|
|
}
|
|
/**
|
|
* Finished the JS output
|
|
*/
|
|
#finishJSOutput() {
|
|
this.#buffer.writeStatement(reportErrors());
|
|
this.#buffer.writeStatement("return out;");
|
|
}
|
|
/**
|
|
* Compiles all the nodes
|
|
*/
|
|
#compileNodes() {
|
|
this.compileNode(this.#rootNode.schema, this.#buffer, {
|
|
type: "root",
|
|
variableName: "root",
|
|
outputExpression: "out",
|
|
fieldPathExpression: "out",
|
|
wildCardPath: ""
|
|
});
|
|
}
|
|
/**
|
|
* Returns compiled output as a function
|
|
*/
|
|
#toAsyncFunction() {
|
|
return new AsyncFunction(
|
|
"root",
|
|
"meta",
|
|
"refs",
|
|
"messagesProvider",
|
|
"errorReporter",
|
|
this.#buffer.toString()
|
|
);
|
|
}
|
|
/**
|
|
* Converts a node to a field. Optionally accepts a parent node to create
|
|
* a field for a specific parent type.
|
|
*/
|
|
createFieldFor(node, parent) {
|
|
switch (parent.type) {
|
|
case "array":
|
|
return createArrayField(parent);
|
|
case "root":
|
|
return createRootField(parent);
|
|
case "object":
|
|
return createObjectField(node, this.variablesCounter, parent);
|
|
case "tuple":
|
|
return createTupleField(node, parent);
|
|
case "record":
|
|
return createRecordField(parent);
|
|
}
|
|
}
|
|
/**
|
|
* Compiles a given compiler node
|
|
*/
|
|
compileNode(node, buffer, parent, parentField) {
|
|
switch (node.type) {
|
|
case "literal":
|
|
return new LiteralNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
case "array":
|
|
return new ArrayNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
case "record":
|
|
return new RecordNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
case "object":
|
|
return new ObjectNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
case "tuple":
|
|
return new TupleNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
case "union":
|
|
return new UnionNodeCompiler(node, buffer, this, parent, parentField).compile();
|
|
}
|
|
}
|
|
/**
|
|
* Compile schema nodes to an async function
|
|
*/
|
|
compile() {
|
|
this.#initiateJSOutput();
|
|
this.#compileNodes();
|
|
this.#finishJSOutput();
|
|
const outputFunction = this.#toAsyncFunction();
|
|
this.variablesCounter = 0;
|
|
this.#buffer.flush();
|
|
return outputFunction;
|
|
}
|
|
};
|
|
|
|
export {
|
|
Compiler
|
|
};
|