From 974af099af108f8c3f3603cdea705ecf67c19aef Mon Sep 17 00:00:00 2001 From: Mateusz Ziarko Date: Mon, 21 Oct 2024 19:59:25 +0200 Subject: [PATCH] 2 / Controllers, Services, Routes, Lifecycle hooks --- .tmp/data.db | Bin 1048576 -> 1048576 bytes package.json | 1 + .../booking/content-types/booking/schema.json | 8 +- src/api/booking/controllers/booking.ts | 98 +++++++++++++++++- src/api/booking/routes/booking.ts | 4 +- src/api/booking/routes/utils.ts | 13 +++ src/api/booking/services/booking.ts | 56 +++++++++- types/generated/contentTypes.d.ts | 5 +- 8 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/api/booking/routes/utils.ts diff --git a/.tmp/data.db b/.tmp/data.db index 4afa4f2bd674c45cdf816937015dc47bb122d9e9..59d5d59f34234815acd88a72b87f18bd584c208b 100644 GIT binary patch delta 4812 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 aI`CWbAR-%*wc+%R&tacQ`5yQ_^?w0;^NkAt delta 3311 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 ({ + // Validate the availability of a car within the following dates + async validateAvailability(ctx) { + const sanitizedQuery = await this.sanitizeQuery(ctx); + + const { from, to, car } = sanitizedQuery; + try { + return strapi.service('api::booking.booking').validateAvailability(from, to, car); + } catch (err) { + throw new errors.HttpError('Not available'); + } + }, + + // Extend the create booking method + async create(ctx, next) { + + // Validate availablity + await this.validateAvailability(ctx, next); + + // Before creation generate reference number + const refNumber = strapi.service('api::booking.booking').generateReferenceNumber(); + + const { car, ...restBody } = ctx.request.body.data; + + const nextCtx = { + ...ctx, + query: {}, // Clean the query to avoid any conflict + request: { + ...ctx.request, + body: { + ...ctx.request.body, + data: { + ...restBody, + ref_number: refNumber, + } + }, + }, + }; + + console.log(nextCtx.request.body) + + // Call the native create booking method + const { data } = await super.create(nextCtx); + const { documentId } = data; + + // Link booking to car + const bookedCar = await strapi.service('api::booking.booking').linkBookingToCar(documentId, car); + + // Link booking to user + const customer = await strapi.service('api::booking.booking').linkBookingToUser(documentId, ctx.state.user.documentId); + + console.log('bookedCar', bookedCar); + console.log('customer', customer); + const { ref_number } = data; + + console.log('data', data); + + // try { + // // Send a confirmation email + // await strapi.plugins['email'].services.email.send({ + // to: ctx.state.user.email, + // subject: `[#${ref_number}]Booking confirmation`, + // text: `Your booking number #${ref_number} has been confirmed` + // }); + // } catch (err) { + // console.error('Error sending email', err); + // } + + return { + data: { + ...data, + car: bookedCar, + customer, + }, + }; + }, + + // Method 3: Replacing a core action with proper sanitization + // async find(ctx) { + // // validateQuery (optional) + // // to throw an error on query params that are invalid or the user does not have access to + // await this.validateQuery(ctx); + + // // sanitizeQuery to remove any query params that are invalid or the user does not have access to + // // It is strongly recommended to use sanitizeQuery even if validateQuery is used + // const sanitizedQueryParams = await this.sanitizeQuery(ctx); + // const { results, pagination } = await strapi.service('api::restaurant.restaurant').find(sanitizedQueryParams); + + // // sanitizeOutput to ensure the user does not receive any data they do not have access to + // const sanitizedResults = await this.sanitizeOutput(results, ctx); + + // return this.transformResponse(sanitizedResults, { pagination }); + // } +})); \ No newline at end of file diff --git a/src/api/booking/routes/booking.ts b/src/api/booking/routes/booking.ts index 68165e5..4eeb982 100644 --- a/src/api/booking/routes/booking.ts +++ b/src/api/booking/routes/booking.ts @@ -4,4 +4,6 @@ import { factories } from '@strapi/strapi'; -export default factories.createCoreRouter('api::booking.booking'); + + +export default factories.createCoreRouter('api::booking.booking'); \ No newline at end of file diff --git a/src/api/booking/routes/utils.ts b/src/api/booking/routes/utils.ts new file mode 100644 index 0000000..3fc5234 --- /dev/null +++ b/src/api/booking/routes/utils.ts @@ -0,0 +1,13 @@ + +export default { + routes: [ + { + method: 'POST', + path: '/bookings/validate-availability', + handler: 'api::booking.booking.validateAvailability', + config: { + auth: false, + }, + }, + ], + }; \ No newline at end of file diff --git a/src/api/booking/services/booking.ts b/src/api/booking/services/booking.ts index e956352..644e700 100644 --- a/src/api/booking/services/booking.ts +++ b/src/api/booking/services/booking.ts @@ -2,6 +2,58 @@ * booking service */ -import { factories } from '@strapi/strapi'; +import { factories, type Data } from '@strapi/strapi'; -export default factories.createCoreService('api::booking.booking'); +export default factories.createCoreService('api::booking.booking', ({ strapi }) => ({ + + // Generate unique reference number + generateReferenceNumber(): string { + return Date.now().toString(36).toUpperCase(); + }, + + + // Validate the availability of a car within the following dates + async validateAvailability(from: string, to: string, id: Data.DocumentID): Promise { + const bookings = await strapi.documents('api::booking.booking').findMany({ + filters: { + car: { + documentId: id + }, + from: { $lt: new Date(to) }, + to: { $gt: new Date(from) } + }, + }); + + console.log('bookings', bookings); + + return !bookings.length; + }, + + // Link booking to car + async linkBookingToCar(booking: Data.DocumentID, car: Data.DocumentID): Promise { + // Link booking to car + return strapi.documents('api::car.car').update({ + documentId: car, + data: { + bookings: { + connect: [booking], + }, + }, + status: 'published', + }); + }, + + // Link booking to user + async linkBookingToUser(booking: Data.DocumentID, user: Data.DocumentID): Promise { + return strapi.documents('plugin::users-permissions.user').update({ + documentId: user, + data: { + bookings: { + connect: [booking], + }, + }, + status: 'published', + }); + }, + +})); diff --git a/types/generated/contentTypes.d.ts b/types/generated/contentTypes.d.ts index ee61437..8f65c53 100644 --- a/types/generated/contentTypes.d.ts +++ b/types/generated/contentTypes.d.ts @@ -8,9 +8,12 @@ export interface ApiBookingBooking extends Struct.CollectionTypeSchema { displayName: 'Booking'; }; options: { - draftAndPublish: true; + draftAndPublish: false; }; attributes: { + ref_number: Schema.Attribute.String & + Schema.Attribute.Required & + Schema.Attribute.Unique; from: Schema.Attribute.DateTime & Schema.Attribute.Required; to: Schema.Attribute.DateTime; car: Schema.Attribute.Relation<'manyToOne', 'api::car.car'>;