Squashed commit of the following:
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 40s
commit 579f0878e5240dc17db69be1e0b0c0f5af7ef9fe
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Tue Jun 9 09:25:44 2026 +0200
feat: Refactor error handling in Dataset Edit form and improve validation messages
- Updated error handling in the Dataset Edit form to use a centralized formatError function for displaying validation messages.
- Enhanced user feedback by ensuring that error messages are displayed consistently across various fields.
- Modified the validation rule for arrayContainsTypes to provide clearer error messages for missing main and translated titles/abstracts.
- Introduced a new ValidationService to manage manual construction of validation errors.
- Updated Vite configuration to streamline asset loading and improve performance.
- Adjusted Inertia setup to utilize dynamic imports for page-specific assets.
- Cleaned up unnecessary comments and code in various files for better readability.
commit 5efddc2a58c0e164fef585cc7344c06155dbc2c1
Author: Arno Kaimbacher <arno.kaimbacher@geosphere.at>
Date: Mon Jan 12 17:02:47 2026 +0100
feat: add dataset change detection and form submission composables
- Implemented `useDatasetChangeDetection` for tracking unsaved changes in dataset forms, including comparisons for licenses, basic properties, files, coverage, and more.
- Added `useDatasetFormSubmission` for handling dataset form submissions with validation, success/error handling, and auto-save functionality.
This commit is contained in:
parent
0680879e2f
commit
9368a0dd8d
38 changed files with 5588 additions and 6181 deletions
|
|
@ -42,7 +42,7 @@ export default class RoleController {
|
|||
can: {
|
||||
create: await auth.user?.can(['user-create']),
|
||||
edit: await auth.user?.can(['user-edit']),
|
||||
delete: await auth.user?.can(['user-delete']),
|
||||
delete: false, //await auth.user?.can(['user-delete']),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
@ -124,7 +124,7 @@ export default class RoleController {
|
|||
|
||||
// password is optional
|
||||
|
||||
const input = request.only(['name', 'description']);
|
||||
const input = request.only(['name', 'display_name', 'description']);
|
||||
await role.merge(input).save();
|
||||
// await user.save();
|
||||
|
||||
|
|
|
|||
|
|
@ -821,6 +821,10 @@ export default class DatasetsController {
|
|||
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||
doctypes: DatasetTypes,
|
||||
can: {
|
||||
edit: await auth.user?.can(['dataset-editor-update']),
|
||||
// delete: await auth.user?.can(['dataset-delete']),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,9 +14,7 @@ import Person from '#models/person';
|
|||
import db from '@adonisjs/lucid/services/db';
|
||||
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
|
||||
import Subject from '#models/subject';
|
||||
// import CreateDatasetValidator from '#validators/create_dataset_validator';
|
||||
import { createDatasetValidator, updateDatasetValidator } from '#validators/dataset';
|
||||
// import UpdateDatasetValidator from '#validators/update_dataset_validator';
|
||||
import {
|
||||
TitleTypes,
|
||||
DescriptionTypes,
|
||||
|
|
@ -40,7 +38,8 @@ import { pipeline } from 'node:stream/promises';
|
|||
import { createWriteStream } from 'node:fs';
|
||||
import type { Multipart } from '@adonisjs/bodyparser';
|
||||
import * as fs from 'fs';
|
||||
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes } from '#app/utils/utility-functions';
|
||||
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes, errorMessage } from '#app/utils/utility-functions';
|
||||
import validation from '#services/validation_service';
|
||||
|
||||
interface Dictionary {
|
||||
[index: string]: string;
|
||||
|
|
@ -207,6 +206,7 @@ export default class DatasetController {
|
|||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||
}),
|
||||
)
|
||||
.minLength(1) // Ensure at least the main title exists
|
||||
// .minLength(2)
|
||||
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||
descriptions: vine
|
||||
|
|
@ -426,37 +426,31 @@ export default class DatasetController {
|
|||
}
|
||||
|
||||
public async store({ auth, request, response, session }: HttpContext) {
|
||||
// At the top of the store() method, declare an array to hold temporary file paths
|
||||
const uploadedTmpFiles: string[] = [];
|
||||
// Aggregated limit example (adjust as needed)
|
||||
const multipartConfig = getConfigFor('multipart');
|
||||
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||
// const aggregatedLimit = 200 * 1024 * 1024;
|
||||
let totalUploadedSize = 0;
|
||||
|
||||
// // Helper function to format bytes as human-readable text
|
||||
// function formatBytes(bytes: number): string {
|
||||
// if (bytes === 0) return '0 Bytes';
|
||||
// const k = 1024;
|
||||
// const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||
// const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
// return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
// }
|
||||
// const enabledExtensions = await this.getEnabledExtensions();
|
||||
const multipart: Multipart = request.multipart;
|
||||
|
||||
// NULL GUARD: kein multipart-Body → sauberer Validierungsfehler statt Crash
|
||||
if (!multipart) {
|
||||
validation.throw('files', 'Please select at least one file.', 'required');
|
||||
}
|
||||
|
||||
// Configuration for limits
|
||||
const multipartConfig = getConfigFor('multipart');
|
||||
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||
|
||||
let totalUploadedSize = 0;
|
||||
let filesCount = 0; // Tracker: ensure at least one file is processed
|
||||
|
||||
multipart.onFile('files', { deferValidations: true }, async (part) => {
|
||||
// Attach an individual file size accumulator if needed
|
||||
filesCount++;
|
||||
let fileUploadedSize = 0;
|
||||
|
||||
// Simply accumulate the size in on('data') without performing the expensive check per chunk
|
||||
// Accumulate the size per chunk (cheap), defer the limit check to 'end'
|
||||
part.on('data', (chunk) => {
|
||||
// reporter(chunk);
|
||||
// Increase counters using the chunk length
|
||||
fileUploadedSize += chunk.length;
|
||||
});
|
||||
|
||||
// After the file is completely read, update the global counter and check aggregated limit
|
||||
// After the file is fully read, update the global counter and check the aggregated limit
|
||||
part.on('end', () => {
|
||||
totalUploadedSize += fileUploadedSize;
|
||||
part.file.size = fileUploadedSize;
|
||||
|
|
@ -465,6 +459,7 @@ export default class DatasetController {
|
|||
uploadedTmpFiles.push(part.file.tmpPath);
|
||||
}
|
||||
|
||||
// AGGREGATED LIMIT CHECK: abort immediately if too big
|
||||
if (totalUploadedSize > aggregatedLimit) {
|
||||
// Clean up all temporary files if aggregate limit is exceeded
|
||||
uploadedTmpFiles.forEach((tmpPath) => {
|
||||
|
|
@ -474,48 +469,44 @@ export default class DatasetController {
|
|||
console.error('Error cleaning up temporary file:', cleanupError);
|
||||
}
|
||||
});
|
||||
const error = new errors.E_VALIDATION_ERROR({
|
||||
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
});
|
||||
request.multipart.abort(error);
|
||||
request.multipart.abort(
|
||||
validation.make('files', `Upload limit of ${formatBytes(aggregatedLimit)} exceeded.`, 'limit')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
part.on('error', (error) => {
|
||||
// fileUploadError = error;
|
||||
request.multipart.abort(error);
|
||||
});
|
||||
|
||||
// await pipeline(part, createWriteStream(filePath));
|
||||
// return { filePath };
|
||||
// Process file with error handling
|
||||
try {
|
||||
// Extract extension from the client file name, e.g. "Tethys 5 - Ampflwang_dataset.zip"
|
||||
const ext = path.extname(part.file.clientName).replace('.', '');
|
||||
// Attach the extracted extension to the file object for later use
|
||||
part.file.extname = ext;
|
||||
|
||||
// part.file.sortOrder = part.file.sortOrder;
|
||||
|
||||
const tmpPath = getTmpPath(multipartConfig);
|
||||
(part.file as any).tmpPath = tmpPath;
|
||||
|
||||
const writeStream = createWriteStream(tmpPath);
|
||||
await pipeline(part, writeStream);
|
||||
} catch (error) {
|
||||
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
|
||||
request.multipart.abort(validation.make('files', error.message, 'upload'));
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await multipart.process();
|
||||
// // Instead of letting an error abort the controller, check if any error occurred
|
||||
|
||||
// EMPTY FIELD CHECK: triggered if process finishes but onFile never ran
|
||||
if (filesCount === 0) {
|
||||
validation.throw('files', 'Please select at least one file.', 'required');
|
||||
}
|
||||
} catch (error) {
|
||||
// This is where you'd expect to catch any errors.
|
||||
session.flash('errors', error.messages);
|
||||
return response.redirect().back();
|
||||
// If it's already a validation error, let it bubble up unchanged
|
||||
if (error instanceof errors.E_VALIDATION_ERROR) throw error;
|
||||
|
||||
// Wrap any other stream/size errors
|
||||
validation.throw('files', errorMessage(error) || 'Upload failed.');
|
||||
}
|
||||
|
||||
// Proceed to transaction and createDatasetAndAssociations
|
||||
let trx: TransactionClientContract | null = null;
|
||||
try {
|
||||
await request.validateUsing(createDatasetValidator);
|
||||
|
|
@ -539,13 +530,11 @@ export default class DatasetController {
|
|||
await trx.rollback();
|
||||
}
|
||||
console.error('Failed to create dataset and related models:', error);
|
||||
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||
throw error;
|
||||
}
|
||||
|
||||
session.flash('message', 'Dataset has been created successfully');
|
||||
return response.redirect().toRoute('dataset.list');
|
||||
// return response.redirect().back();
|
||||
}
|
||||
private async createDatasetAndAssociations(
|
||||
user: User,
|
||||
|
|
@ -1106,7 +1095,7 @@ export default class DatasetController {
|
|||
}
|
||||
});
|
||||
const error = new errors.E_VALIDATION_ERROR({
|
||||
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
'files': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||
});
|
||||
request.multipart.abort(error);
|
||||
}
|
||||
|
|
@ -1132,7 +1121,7 @@ export default class DatasetController {
|
|||
try {
|
||||
await multipart.process();
|
||||
} catch (error) {
|
||||
session.flash('errors', error.messages);
|
||||
// session.flash('errors', error.messages);
|
||||
return response.redirect().back();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import ResumptionToken from './ResumptionToken.js';
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
import InternalServerErrorException from '#app/exceptions/InternalServerException';
|
||||
import { sprintf } from 'sprintf-js';
|
||||
import dayjs from 'dayjs';
|
||||
// import { sprintf } from 'sprintf-js';
|
||||
// import dayjs from 'dayjs';
|
||||
import TokenWorkerContract from './TokenWorkerContract.js';
|
||||
|
||||
export default class TokenWorkerService implements TokenWorkerContract {
|
||||
|
|
@ -98,21 +98,21 @@ export default class TokenWorkerService implements TokenWorkerContract {
|
|||
// return uniqueName;
|
||||
// }
|
||||
|
||||
private async generateUniqueName(): Promise<string> {
|
||||
let fc = 0;
|
||||
const uniqueId = dayjs().unix().toString();
|
||||
let uniqueName: string;
|
||||
let cacheKeyExists: boolean;
|
||||
do {
|
||||
// format values
|
||||
// %s - String
|
||||
// %d - Signed decimal number (negative, zero or positive)
|
||||
// [0-9] (Specifies the minimum width held of to the variable value)
|
||||
uniqueName = sprintf('%s%05d', uniqueId, fc++);
|
||||
cacheKeyExists = await this.has(uniqueName);
|
||||
} while (cacheKeyExists);
|
||||
return uniqueName;
|
||||
}
|
||||
// private async generateUniqueName(): Promise<string> {
|
||||
// let fc = 0;
|
||||
// const uniqueId = dayjs().unix().toString();
|
||||
// let uniqueName: string;
|
||||
// let cacheKeyExists: boolean;
|
||||
// do {
|
||||
// // format values
|
||||
// // %s - String
|
||||
// // %d - Signed decimal number (negative, zero or positive)
|
||||
// // [0-9] (Specifies the minimum width held of to the variable value)
|
||||
// uniqueName = sprintf('%s%05d', uniqueId, fc++);
|
||||
// cacheKeyExists = await this.has(uniqueName);
|
||||
// } while (cacheKeyExists);
|
||||
// return uniqueName;
|
||||
// }
|
||||
|
||||
public async get(key: string): Promise<ResumptionToken | null> {
|
||||
if (!this.cache) {
|
||||
|
|
|
|||
|
|
@ -1,214 +1,53 @@
|
|||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Http Exception Handler
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| AdonisJs will forward all exceptions occurred during an HTTP request to
|
||||
| the following class. You can learn more about exception handling by
|
||||
| reading docs.
|
||||
|
|
||||
| The exception handler extends a base `HttpExceptionHandler` which is not
|
||||
| mandatory, however it can do lot of heavy lifting to handle the errors
|
||||
| properly.
|
||||
|
|
||||
*/
|
||||
import app from '@adonisjs/core/services/app';
|
||||
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http';
|
||||
// import logger from '@adonisjs/core/services/logger';
|
||||
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http';
|
||||
import app from '@adonisjs/core/services/app'
|
||||
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http'
|
||||
import type { StatusPageRange, StatusPageRenderer } from '@adonisjs/core/types/http'
|
||||
|
||||
export default class HttpExceptionHandler extends ExceptionHandler {
|
||||
/**
|
||||
* In debug mode, the exception handler will display verbose errors
|
||||
* with pretty printed stack traces.
|
||||
*/
|
||||
protected debug = !app.inProduction;
|
||||
protected debug = !app.inProduction
|
||||
protected renderStatusPages = true
|
||||
|
||||
/**
|
||||
* Status pages are used to display a custom HTML pages for certain error
|
||||
* codes. You might want to enable them in production only, but feel
|
||||
* free to enable them in development as well.
|
||||
*/
|
||||
protected renderStatusPages = true; //app.inProduction;
|
||||
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||
'404': (error, ctx) =>
|
||||
ctx.inertia
|
||||
? ctx.inertia.render('Errors/ServerError', { error: error.message, code: error.status })
|
||||
: ctx.response.status(error.status).send(error.message),
|
||||
'401..403': (error, ctx) => {
|
||||
if (ctx.inertia) {
|
||||
return ctx.inertia.render('Errors/ServerError', { error: error.message, code: error.status });
|
||||
}
|
||||
return ctx.response.status(error.status).send(error.message);
|
||||
},
|
||||
'500..599': (error, ctx) => {
|
||||
const isDbError =
|
||||
error.code === 'ECONNREFUSED' &&
|
||||
(error.errors?.some((e: any) => e.port === 5432) ?? error.message?.includes('5432'));
|
||||
|
||||
/**
|
||||
* Status pages is a collection of error code range and a callback
|
||||
* to return the HTML contents to send as a response.
|
||||
*/
|
||||
// protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||
// '401..403': (error, { view }) => {
|
||||
// return view.render('./errors/unauthorized', { error });
|
||||
// },
|
||||
// '404': (error, { view }) => {
|
||||
// return view.render('./errors/not-found', { error });
|
||||
// },
|
||||
// '500..599': (error, { view }) => {
|
||||
// return view.render('./errors/server-error', { error });
|
||||
// },
|
||||
// };
|
||||
if (isDbError && ctx.inertia) {
|
||||
return ctx.inertia.render('Errors/postgres_error', {
|
||||
status: 'error',
|
||||
message: 'PostgreSQL database connection failed.',
|
||||
details: {
|
||||
code: error.code,
|
||||
type: error.status
|
||||
// Entferne das .map() auf error.errors, da es oft undefined ist
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||
'404': (error, { inertia }) => {
|
||||
return inertia.render('Errors/ServerError', {
|
||||
error: error.message,
|
||||
code: error.status,
|
||||
});
|
||||
},
|
||||
'401..403': async (error, { inertia }) => {
|
||||
// session.flash('errors', error.message);
|
||||
return inertia.render('Errors/ServerError', {
|
||||
error: error.message,
|
||||
code: error.status,
|
||||
});
|
||||
},
|
||||
// '500': (error, { inertia }) => {
|
||||
// return inertia.render('Errors/postgres_error', {
|
||||
// status: 'error',
|
||||
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||
// details: {
|
||||
// code: error.code,
|
||||
// type: error.status,
|
||||
// ports: error.errors.map((err: any) => ({
|
||||
// port: err.port,
|
||||
// address: err.address,
|
||||
// })),
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
'500..599': (error, { inertia }) => {
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
|
||||
|
||||
if (dbErrors) {
|
||||
return inertia.render('Errors/postgres_error', {
|
||||
status: 'error',
|
||||
message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||
details: {
|
||||
code: error.code,
|
||||
type: error.status,
|
||||
ports: error.errors.map((err: any) => ({
|
||||
port: err.port,
|
||||
address: err.address,
|
||||
})),
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
return inertia.render('Errors/ServerError', {
|
||||
error: error.message,
|
||||
code: error.status,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// constructor() {
|
||||
// super(logger);
|
||||
// }
|
||||
|
||||
public async handle(error: any, ctx: HttpContext) {
|
||||
const { response, request, session, inertia } = ctx;
|
||||
|
||||
/**
|
||||
* Handle failed authentication attempt
|
||||
*/
|
||||
// if (['E_INVALID_AUTH_PASSWORD', 'E_INVALID_AUTH_UID'].includes(error.code)) {
|
||||
// session.flash('errors', { login: error.message });
|
||||
// return response.redirect('/login');
|
||||
// }
|
||||
// if ([401].includes(error.status)) {
|
||||
// session.flash('errors', { login: error.message });
|
||||
// return response.redirect('/dashboard');
|
||||
// }
|
||||
|
||||
// Handle Axios errors
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
|
||||
|
||||
if (dbErrors) {
|
||||
// return ctx.response.status(503).json({
|
||||
// status: 'error',
|
||||
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||
// details: {
|
||||
// code: error.code,
|
||||
// type: error.status,
|
||||
// ports: error.errors.map((err: any) => ({
|
||||
// port: err.port,
|
||||
// address: err.address,
|
||||
// })),
|
||||
// },
|
||||
// });
|
||||
// return inertia.render('Errors/postgres_error', {
|
||||
// status: 'error',
|
||||
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||
// details: {
|
||||
// code: error.code,
|
||||
// type: error.status,
|
||||
// ports: error.errors.map((err: any) => ({
|
||||
// port: err.port,
|
||||
// address: err.address,
|
||||
// })),
|
||||
// },
|
||||
// });
|
||||
}
|
||||
}
|
||||
|
||||
// Handle simple ECONNREFUSED errors
|
||||
// if (error.code === 'ECONNREFUSED') {
|
||||
// return ctx.response.status(503).json({
|
||||
// status: 'error',
|
||||
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
|
||||
// code: error.code,
|
||||
// });
|
||||
// }
|
||||
|
||||
// https://github.com/inertiajs/inertia-laravel/issues/56
|
||||
// let test = response.getStatus(); //200
|
||||
// let header = request.header('X-Inertia'); // true
|
||||
// if (request.header('X-Inertia') && [500, 503, 404, 403, 401, 200].includes(response.getStatus())) {
|
||||
if (request.header('X-Inertia') && [422].includes(error.status)) {
|
||||
// session.flash('errors', error.messages.errors);
|
||||
session.flash('errors', error.messages);
|
||||
return response.redirect().back();
|
||||
// return inertia.render('errors/server_error', {
|
||||
// return inertia.render('errors/server_error', {
|
||||
// // status: response.getStatus(),
|
||||
// error: error,
|
||||
// });
|
||||
// ->toResponse($request)
|
||||
// ->setStatusCode($response->status());
|
||||
}
|
||||
|
||||
// Handle simple ECONNREFUSED errors
|
||||
// if (error.code === 'ECONNREFUSED') {
|
||||
// return ctx.response.status(503).json({
|
||||
// status: 'error',
|
||||
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
|
||||
// code: error.code,
|
||||
// });
|
||||
// }
|
||||
// Dynamically change the error templates based on the absence of X-Inertia header
|
||||
// if (!ctx.request.header('X-Inertia')) {
|
||||
// this.statusPages = {
|
||||
// '401..403': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||
// '404': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||
// '500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||
// };
|
||||
// }
|
||||
|
||||
/**
|
||||
* Forward rest of the exceptions to the parent class
|
||||
*/
|
||||
return super.handle(error, ctx);
|
||||
if (ctx.inertia) {
|
||||
return ctx.inertia.render('Errors/ServerError', { error: error.message, code: 500 });
|
||||
}
|
||||
return ctx.response.status(500).send(error.message);
|
||||
}
|
||||
};
|
||||
|
||||
public async handle(error: any, ctx: HttpContext) {
|
||||
/**
|
||||
* The method is used to report error to the logging service or
|
||||
* the a third party error monitoring service.
|
||||
*
|
||||
* @note You should not attempt to send a response from this method.
|
||||
* WICHTIG: Validierungsfehler (422) NICHT manuell abfangen!
|
||||
* AdonisJS 6 + VineJS + Inertia machen das automatisch.
|
||||
* Wenn du es hier manuell machst, überschreibst du den Standard-Flow.
|
||||
*/
|
||||
async report(error: unknown, ctx: HttpContext) {
|
||||
return super.report(error, ctx);
|
||||
}
|
||||
}
|
||||
|
||||
return super.handle(error, ctx)
|
||||
}
|
||||
}
|
||||
54
app/services/validation_service.ts
Normal file
54
app/services/validation_service.ts
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import app from '@adonisjs/core/services/app'
|
||||
import { errors } from '@vinejs/vine'
|
||||
|
||||
/**
|
||||
* The ValidationService handles manual construction of validation errors
|
||||
* that are compatible with the VanillaErrorReporter and AdonisJS Session.
|
||||
*/
|
||||
export class ValidationService {
|
||||
/**
|
||||
* Builds a validation error in the array-of-objects format without throwing it.
|
||||
* Use this when you need the error object itself, e.g. multipart.abort(error).
|
||||
*/
|
||||
make(field: string, message: string, rule: string = 'manual') {
|
||||
return new errors.E_VALIDATION_ERROR([
|
||||
{
|
||||
field,
|
||||
message,
|
||||
rule,
|
||||
},
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws a manual validation error in the array-of-objects format
|
||||
* which prevents the ".reduce is not a function" error in the session.
|
||||
*/
|
||||
throw(field: string, message: string, rule: string = 'manual') {
|
||||
throw this.make(field, message, rule)
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws multiple manual validation errors at once.
|
||||
*/
|
||||
throwMany(errorObjects: Array<{ field: string; message: string; rule?: string }>) {
|
||||
throw new errors.E_VALIDATION_ERROR(
|
||||
errorObjects.map((err) => ({
|
||||
field: err.field,
|
||||
message: err.message,
|
||||
rule: err.rule || 'manual',
|
||||
}))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize and export the singleton instance
|
||||
*/
|
||||
let validation: ValidationService
|
||||
|
||||
await app.booted(async () => {
|
||||
validation = await app.container.make(ValidationService)
|
||||
})
|
||||
|
||||
export { validation as default }
|
||||
|
|
@ -122,3 +122,8 @@ function extractPivotAttributes(person: any) {
|
|||
}
|
||||
return pivotAttributes;
|
||||
}
|
||||
|
||||
// in #app/utils/utility-functions
|
||||
export function errorMessage(error: unknown): string {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,203 +1,50 @@
|
|||
// import { ValidationError } from '../errors/validation_error.js';
|
||||
import { errors } from '@vinejs/vine';
|
||||
import type { ErrorReporterContract, FieldContext } from '@vinejs/vine/types';
|
||||
import string from '@poppinss/utils/string';
|
||||
import { errors } from '@vinejs/vine'
|
||||
import type { ErrorReporterContract, FieldContext } from '@vinejs/vine/types'
|
||||
|
||||
/**
|
||||
* Shape of the Vanilla error node
|
||||
*/
|
||||
export type VanillaErrorNode = {
|
||||
[field: string]: string[];
|
||||
};
|
||||
export interface MessagesBagContract {
|
||||
get(pointer: string, rule: string, message: string, arrayExpressionPointer?: string, args?: any): string;
|
||||
}
|
||||
/**
|
||||
* Message bag exposes the API to pull the most appropriate message for a
|
||||
* given validation failure.
|
||||
*/
|
||||
export class MessagesBag implements MessagesBagContract {
|
||||
messages: Message;
|
||||
wildCardCallback;
|
||||
constructor(messages: string[]) {
|
||||
this.messages = messages;
|
||||
this.wildCardCallback = typeof this.messages['*'] === 'function' ? this.messages['*'] : undefined;
|
||||
}
|
||||
/**
|
||||
* Transform message by replace placeholders with runtime values
|
||||
*/
|
||||
transform(message: any, rule: string, pointer: string, args: any) {
|
||||
/**
|
||||
* No interpolation required
|
||||
*/
|
||||
if (!message.includes('{{')) {
|
||||
return message;
|
||||
}
|
||||
return string.interpolate(message, { rule, field: pointer, options: args || {} });
|
||||
}
|
||||
/**
|
||||
* Returns the most appropriate message for the validation failure.
|
||||
*/
|
||||
get(pointer: string, rule: string, message: string, arrayExpressionPointer: string, args: any) {
|
||||
let validationMessage = this.messages[`${pointer}.${rule}`];
|
||||
/**
|
||||
* Fetch message for the array expression pointer if it exists
|
||||
*/
|
||||
if (!validationMessage && arrayExpressionPointer) {
|
||||
validationMessage = this.messages[`${arrayExpressionPointer}.${rule}`];
|
||||
}
|
||||
/**
|
||||
* Fallback to the message for the rule
|
||||
*/
|
||||
if (!validationMessage) {
|
||||
validationMessage = this.messages[rule];
|
||||
}
|
||||
/**
|
||||
* Transform and return message. The wildcard callback is invoked when custom message
|
||||
* is not defined
|
||||
*/
|
||||
return validationMessage
|
||||
? this.transform(validationMessage, rule, pointer, args)
|
||||
: this.wildCardCallback
|
||||
? this.wildCardCallback(pointer, rule, arrayExpressionPointer, args)
|
||||
: message;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Shape of the error message collected by the SimpleErrorReporter
|
||||
*/
|
||||
type SimpleError = {
|
||||
message: string;
|
||||
field: string;
|
||||
rule: string;
|
||||
index?: number;
|
||||
meta?: Record<string, any>;
|
||||
};
|
||||
export interface Message {
|
||||
[key: string]: any;
|
||||
}
|
||||
/**
|
||||
* Simple error reporter collects error messages as an array of object.
|
||||
* Each object has following properties.
|
||||
*
|
||||
* - message: string
|
||||
* - field: string
|
||||
* - rule: string
|
||||
* - index?: number (in case of an array member)
|
||||
* - args?: Record<string, any>
|
||||
* Der VanillaErrorReporter sammelt Validierungsfehler im Standardformat,
|
||||
* damit die AdonisJS Session-Middleware sie korrekt verarbeiten (reducen) kann.
|
||||
*/
|
||||
export class VanillaErrorReporter implements ErrorReporterContract {
|
||||
// private messages;
|
||||
// private bail;
|
||||
/**
|
||||
* Boolean, um zu prüfen, ob Fehler vorliegen
|
||||
*/
|
||||
hasErrors: boolean = false
|
||||
|
||||
/**
|
||||
* Sammlung der Fehler als Array (erforderlich für AdonisJS 6 Session)
|
||||
*/
|
||||
errors: any[] = []
|
||||
|
||||
/**
|
||||
* Diese Methode wird von VineJS für jeden Validierungsfehler aufgerufen
|
||||
*/
|
||||
report(
|
||||
message: string,
|
||||
rule: string,
|
||||
field: FieldContext,
|
||||
meta?: Record<string, any>
|
||||
): void {
|
||||
this.hasErrors = true
|
||||
|
||||
/**
|
||||
* Boolean to know one or more errors have been reported
|
||||
*/
|
||||
hasErrors: boolean = false;
|
||||
/**
|
||||
* Collection of errors
|
||||
*/
|
||||
// errors: SimpleError[] = [];
|
||||
errors: Message = {};
|
||||
/**
|
||||
* Report an error.
|
||||
* Wir pushen das Objekt in das Array.
|
||||
* Das Feld 'field' erhält den vollständigen Pfad (z.B. "user.email").
|
||||
*/
|
||||
this.errors.push({
|
||||
message,
|
||||
rule,
|
||||
field: field.getFieldPath(),
|
||||
...meta,
|
||||
});
|
||||
}
|
||||
|
||||
// constructor(messages: MessagesBagContract) {
|
||||
// this.messages = messages;
|
||||
// }
|
||||
|
||||
report(message: string, rule: string, field: FieldContext, meta?: Record<string, any> | undefined): void {
|
||||
// const error: SimpleError = {
|
||||
// message,
|
||||
// rule,
|
||||
// field: field.getFieldPath()
|
||||
// };
|
||||
// if (meta) {
|
||||
// error.meta = meta;
|
||||
// }
|
||||
// if (field.isArrayMember) {
|
||||
// error.index = field.name as number;
|
||||
// }
|
||||
// this.errors.push(error);
|
||||
this.hasErrors = true;
|
||||
// if (this.errors[field.getFieldPath()]) {
|
||||
// this.errors[field.getFieldPath()]?.push(message);
|
||||
// } else {
|
||||
// this.errors[field.getFieldPath()] = [message];
|
||||
// }
|
||||
const error: SimpleError = {
|
||||
message,
|
||||
rule,
|
||||
field: field.getFieldPath(), // ?field.wildCardPath.split('.')[0] : field.getFieldPath(),
|
||||
};
|
||||
// field: 'titles.0.value'
|
||||
// message: 'Main Title is required'
|
||||
// rule: 'required' "required"
|
||||
|
||||
if (meta) {
|
||||
error.meta = meta;
|
||||
}
|
||||
// if (field.isArrayMember) {
|
||||
// error.index = field.name;
|
||||
// }
|
||||
this.hasErrors = true;
|
||||
|
||||
// var test = field.getFieldPath();
|
||||
|
||||
// this.errors.push(error);
|
||||
// if (this.errors[error.field]) {
|
||||
// this.errors[error.field]?.push(message);
|
||||
// }
|
||||
if (field.isArrayMember) {
|
||||
// Check if the field has wildCardPath and if the error field already exists
|
||||
if (this.errors[error.field]) {
|
||||
// Do nothing, as we don't want to push further messages
|
||||
} else {
|
||||
// If the error field already exists, push the message
|
||||
if (this.errors[error.field]) {
|
||||
this.errors[error.field].push(message);
|
||||
} else {
|
||||
this.errors[error.field] = [message];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.errors[error.field]) {
|
||||
this.errors[error.field]?.push(message);
|
||||
} else {
|
||||
this.errors[error.field] = [message];
|
||||
}
|
||||
}
|
||||
|
||||
// } else {
|
||||
// // normal field
|
||||
// this.errors[field.field] = [message];
|
||||
// }
|
||||
|
||||
/**
|
||||
* Collecting errors as per the JSONAPI spec
|
||||
*/
|
||||
// this.errors.push({
|
||||
// code: rule,
|
||||
// detail: message,
|
||||
// source: {
|
||||
// pointer: field.wildCardPath,
|
||||
// },
|
||||
// ...(meta ? { meta } : {}),
|
||||
// });
|
||||
|
||||
// let pointer: string = field.wildCardPath as string; //'display_name'
|
||||
// // if (field.isArrayMember) {
|
||||
// // this.errors[pointer] = field.name;
|
||||
// // }
|
||||
// this.errors[pointer] = this.errors[pointer] || [];
|
||||
// // this.errors[pointer].push(message);
|
||||
// this.errors[pointer].push(this.messages.get(pointer, rule, message, arrayExpressionPointer, args));
|
||||
}
|
||||
/**
|
||||
* Returns an instance of the validation error
|
||||
*/
|
||||
createError() {
|
||||
return new errors.E_VALIDATION_ERROR(this.errors);
|
||||
}
|
||||
}
|
||||
export {};
|
||||
/**
|
||||
* Erstellt die eigentliche Exception.
|
||||
* Da 'this.errors' nun ein Array ist, funktioniert .reduce()
|
||||
* in der Session-Middleware reibungslos.
|
||||
*/
|
||||
createError() {
|
||||
return new errors.E_VALIDATION_ERROR(this.errors);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue