@astro-utils/forms
Version:
Server component for Astro (call server functions from client side with validation and state management)
193 lines (192 loc) • 5.99 kB
JavaScript
import { ZodIssueCode, z } from 'zod';
import { getFormMultiValue } from '../../form-tools/post.js';
import { FORM_OPTIONS } from '../../settings.js';
import path from 'path';
import { BigFile } from '../../components/form/UploadBigFile/BigFile.js';
import getContext from '@astro-utils/context';
import fs from 'fs/promises';
import fsExtra from 'fs-extra/esm';
const HEX_COLOR_REGEX = /^#?([0-9a-f]{6}|[0-9a-f]{3})$/i;
const BIG_FILE_START = 'big-file:';
export function parseCheckbox(about, originalValue) {
if (originalValue == null) {
about.formValue = about.formValue === 'on';
}
}
export function parseNumber(about, type, min, max) {
let num = z.number();
if (type === 'int') {
num = num.int();
}
if (min != null) {
num = num.min(min);
}
if (max != null) {
num = num.max(max);
}
about.formValue = Number(about.formValue);
about.catchParse(num);
}
function parseFormDate(date, type) {
if (date instanceof Date) {
return date;
}
if (type === 'date' || type === 'datetime-local') {
date = new Date(date);
}
else if (type === 'time') {
date = new Date(`1970-01-01T${date}`);
}
else if (type === 'month') {
date = new Date(`${date}-01`);
}
else if (type === 'week') {
const year = parseInt(date.substring(0, 4), 10);
const week = parseInt(date.substring(6, 8), 10) - 1;
const janFirst = new Date(year, 0, 1);
const days = (week * 7) - janFirst.getDay() + 1;
date = new Date(year, 0, days);
}
else {
date = new Date(date);
}
return date;
}
export function parseDate(about, type, min, max) {
let date = z.date();
if (min != null) {
date = date.min(parseFormDate(min, type));
}
if (max != null) {
date = date.max(parseFormDate(max, type));
}
about.formValue = parseFormDate(about.formValue, type);
about.catchParse(date);
}
export function parseJSON(about) {
const EMPTY_OBJECT = {};
about.catchParse(z.string()
.transform((str, ctx) => {
try {
return JSON.parse(str, (key, value) => {
if (EMPTY_OBJECT[key] !== undefined) {
return;
}
return value;
});
}
catch (e) {
ctx.addIssue({ code: ZodIssueCode.custom, message: 'Invalid JSON' });
return z.NEVER;
}
}));
}
export function parseEmail(about) {
about.catchParse(z.string().email());
}
export function parseURL(about) {
about.catchParse(z.string().url());
}
export function parseColor(about) {
about.catchParse(z.string().regex(HEX_COLOR_REGEX), 'Invalid hex color');
}
const zodValidationInfo = z.preprocess((str, ctx) => {
try {
return JSON.parse(str);
}
catch {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid JSON",
});
return z.NEVER;
}
}, z.object({
id: z.string().uuid(),
name: z.string().min(1),
failed: z.boolean().optional(),
}));
async function isBigFile(value) {
if (typeof value !== 'string' || !value.startsWith(BIG_FILE_START)) {
return;
}
const tempDirectory = FORM_OPTIONS.forms.bigFilesUpload.bigFileServerOptions.tempDirectory;
const bigFileInfo = value.substring(BIG_FILE_START.length);
const { success, data } = zodValidationInfo.safeParse(bigFileInfo);
if (!success) {
return;
}
if (data.failed) {
const chunksDir = path.join(tempDirectory, "chunks_" + data.id);
const errorMessage = path.join(chunksDir, "error.txt");
try {
return await fs.readFile(errorMessage, 'utf8');
}
catch {
return "upload failed";
}
finally {
await fsExtra.remove(chunksDir);
}
}
const filePath = path.join(tempDirectory, data.id);
try {
const fileSize = await BigFile.loadFileSize(filePath);
return new BigFile(data.name, filePath, fileSize);
}
catch { }
}
export async function parseFiles(about, astro, multiple, readonly) {
if (readonly)
return;
const { disposeFiles, bindId = '' } = getContext(astro, '@astro-utils/forms');
let values = [about.formValue];
let hasFailed = false;
if (multiple) {
values = about.formValue = await getFormMultiValue(astro.request, bindId + about.originalName);
const promises = [];
for (let i = 0; i < values.length; i++) {
const promise = isBigFile(values[i]).then((bigFile) => {
if (!bigFile || hasFailed) {
return;
}
if (typeof bigFile === "string") {
hasFailed = true;
about.pushErrorManually('upload-failed', bigFile);
return;
}
values[i] = bigFile;
disposeFiles.push(bigFile.path);
});
promises.push(promise);
}
await Promise.all(promises);
}
else {
const bigFile = await isBigFile(about.formValue);
if (bigFile) {
if (typeof bigFile === "string") {
about.pushErrorManually('upload-failed', bigFile);
return;
}
values = [about.formValue = bigFile];
disposeFiles.push(bigFile.path);
}
}
for (const value of values) {
if (value instanceof File === false && value instanceof BigFile === false) {
about.pushErrorManually('upload-not-file', 'The upload value is not a file');
break;
}
}
}
export function parseEmptyFiles(about, astro) {
if (astro.props.readonly)
return;
if (!about.formValue || about.formValue.size === 0) {
about.formValue = null;
}
if (astro.props.multiple) {
about.formValue = [];
}
}