Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate to TS #747

Draft
wants to merge 13 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,5 @@ dist/
# Experimental
xxx_*
xxx/*

@cds-models
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
},
{
"name": "cds watch",
"command": "npx cds watch",
"command": "npx cds-ts watch",
"env": {
"DEBUG": "sqlite"
},
Expand Down
4 changes: 4 additions & 0 deletions db/schema.cds
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@ entity BookingSupplement : managed {
// Code Lists
//

@singular: 'BookingStatus'
@plural: 'BookingStatuses'
entity BookingStatus : CodeList {
key code : String enum {
New = 'N';
Expand All @@ -78,6 +80,8 @@ entity BookingStatus : CodeList {
};
};

@singular: 'TravelStatus'
@plural: 'TravelStatuses'
entity TravelStatus : CodeList {
key code : String enum {
Open = 'O';
Expand Down
10 changes: 10 additions & 0 deletions jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"compilerOptions": {
"moduleResolution": "nodenext",
"paths": {
"#cds-models/*": [
"./@cds-models/*"
]
}
}
}
5,319 changes: 544 additions & 4,775 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
"cross-spawn": "^7.0.3",
"http-proxy": "^1.18.1",
"jest": "^29.0.2",
"qunit": "^2.9.3"
"qunit": "^2.9.3",
"@cap-js/cds-typer": "^0"
},
"cds": {
"requires": {
Expand Down Expand Up @@ -118,5 +119,8 @@
"sapux": [
"app/travel_processor",
"app/travel_analytics"
]
],
"imports": {
"#cds-models/*": "./@cds-models/*/index.js"
}
}
4 changes: 4 additions & 0 deletions srv/analytics-service.cds
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ service AnalyticsService @(path:'/analytics') {
// @(restrict: [
// { grant: 'READ', to: 'authenticated-user'},
// ])
@singular: 'Booking'
@plural: 'Bookings'
@readonly
entity Bookings as projection on my.Booking {
@UI.Hidden: false
Expand Down Expand Up @@ -60,6 +62,8 @@ service AnalyticsService @(path:'/analytics') {
};

// for value help
@singular: 'BookingsStatus'
@plural: 'BookingsStatuses'
entity BookingStatus as projection on my.BookingStatus;

// for detail page:
Expand Down
98 changes: 55 additions & 43 deletions srv/travel-service.js → srv/travel-service.ts
Original file line number Diff line number Diff line change
@@ -1,80 +1,85 @@
const cds = require ('@sap/cds'); require('./workarounds')

class TravelService extends cds.ApplicationService {
init() {
import { Bookings } from '#cds-models/AnalyticsService';
import * as cds from '@sap/cds';
require('./workarounds')

export class TravelServiceImpl extends cds.ApplicationService {
private _update_totals4: Function = () => undefined; // forward declaration

async init() {

/**
* Reflect definitions from the service's CDS model
*/
const { Travel, Booking, BookingSupplement } = this.entities


const { Travel, Booking, BookingSupplement, TravelStatus, BookingStatus } = await import('#cds-models/TravelService')
const TravelService = (await import('#cds-models/TravelService')).default
/**
* Fill in primary keys for new Travels.
* Note: In contrast to Bookings and BookingSupplements that has to happen
* upon SAVE, as multiple users could create new Travels concurrently.
*/
this.before ('CREATE', 'Travel', async req => {
const { maxID } = await SELECT.one `max(TravelID) as maxID` .from (Travel)
this.before ('CREATE', Travel, async req => {
const { maxID } = await SELECT(`max(TravelID) as maxID`).from (Travel)
req.data.TravelID = maxID + 1
})


/**
* Fill in defaults for new Bookings when editing Travels.
*/
this.before ('NEW', 'Booking.drafts', async (req) => {
this.before ('NEW', Booking.drafts, async (req) => {
const { to_Travel_TravelUUID } = req.data
const { status } = await SELECT `TravelStatus_code as status` .from (Travel.drafts, to_Travel_TravelUUID)
if (status === 'X') throw req.reject (400, 'Cannot add new bookings to rejected travels.')
const { maxID } = await SELECT.one `max(BookingID) as maxID` .from (Booking.drafts) .where ({to_Travel_TravelUUID})
const { status } = await SELECT `TravelStatus_code as status` .from ('Travel.drafts', to_Travel_TravelUUID)
if (status === TravelStatus.code.Canceled) throw req.reject (400, 'Cannot add new bookings to rejected travels.')
const { BookingID: maxID } = await SELECT.one(`max(BookingID) as maxID`).from (Bookings) .where ({to_Travel_TravelUUID})
req.data.BookingID = maxID + 1
req.data.BookingStatus_code = 'N'
req.data.BookingStatus_code = BookingStatus.code.New
req.data.BookingDate = (new Date).toISOString().slice(0,10) // today
})


/**
* Fill in defaults for new BookingSupplements when editing Travels.
*/
this.before ('NEW', 'BookingSupplement.drafts', async (req) => {
this.before ('NEW', BookingSupplement.drafts, async (req) => {
const { to_Booking_BookingUUID } = req.data
const { maxID } = await SELECT.one `max(BookingSupplementID) as maxID` .from (BookingSupplement.drafts) .where ({to_Booking_BookingUUID})
req.data.BookingSupplementID = maxID + 1
})


/**
* Changing Booking Fees is only allowed for not yet accapted Travels.
*/
this.before ('UPDATE', 'Travel.drafts', async (req) => { if ('BookingFee' in req.data) {
const { status } = await SELECT.one `TravelStatus_code as status` .from (req.subject)
if (status === 'A') req.reject(400, 'Booking fee can not be updated for accepted travels.', 'BookingFee')
this.before ('UPDATE', Travel.drafts, async (req) => { if ('BookingFee' in req.data) {
// FIXME: TS v
const { status } = await SELECT.one `TravelStatus_code as status` .from (req.subject as unknown as typeof Travel)
if (status === TravelStatus.code.Accepted) req.reject(400, 'Booking fee can not be updated for accepted travels.', 'BookingFee')
}})


/**
* Update the Travel's TotalPrice when its BookingFee is modified.
*/
this.after ('UPDATE', 'Travel.drafts', (_,req) => { if ('BookingFee' in req.data) {
this.after ('UPDATE', Travel.drafts, (_,req) => { if ('BookingFee' in req.data) {
return this._update_totals4 (req.data.TravelUUID)
}})


/**
* Update the Travel's TotalPrice when a Booking's FlightPrice is modified.
*/
this.after ('UPDATE', 'Booking.drafts', async (_,req) => { if ('FlightPrice' in req.data) {
this.after ('UPDATE', Bookings.drafts, async (_,req) => { if ('FlightPrice' in req.data) {
// We need to fetch the Travel's UUID for the given Booking target
const { travel } = await SELECT.one `to_Travel_TravelUUID as travel` .from (req.subject)
// FIXME: TS v
const { travel } = await SELECT.one `to_Travel_TravelUUID as travel` .from (req.subject as unknown as typeof Booking)
return this._update_totals4 (travel)
}})


/**
* Update the Travel's TotalPrice when a Supplement's Price is modified.
*/
this.after ('UPDATE', 'BookingSupplement.drafts', async (_,req) => { if ('Price' in req.data) {
this.after ('UPDATE', BookingSupplement.drafts, async (_,req) => { if ('Price' in req.data) {
// We need to fetch the Travel's UUID for the given Supplement target
const { travel } = await SELECT.one `to_Travel_TravelUUID as travel` .from (Booking.drafts)
.where `BookingUUID = ${ SELECT.one `to_Booking_BookingUUID` .from (BookingSupplement.drafts).where({BookSupplUUID:req.data.BookSupplUUID}) }`
Expand All @@ -90,7 +95,7 @@ init() {
// Find out which travel is affected before the delete
const { BookSupplUUID } = req.data
const { to_Travel_TravelUUID } = await SELECT.one
.from(BookingSupplement.drafts, ['to_Travel_TravelUUID'])
.from('BookingSupplement.drafts', ['to_Travel_TravelUUID'])
.where({ BookSupplUUID })
// Delete handled by generic handlers
const res = await next()
Expand All @@ -102,11 +107,11 @@ init() {
/**
* Update the Travel's TotalPrice when a Booking is deleted.
*/
this.on('CANCEL', Booking.drafts, async (req, next) => {
this.on('CANCEL', Bookings.drafts, async (req, next) => {
// Find out which travel is affected before the delete
const { BookingUUID } = req.data
const { to_Travel_TravelUUID } = await SELECT.one
.from(Booking.drafts, ['to_Travel_TravelUUID'])
.from('Booking.drafts', ['to_Travel_TravelUUID'])
.where({ BookingUUID })
// Delete handled by generic handlers
const res = await next()
Expand All @@ -119,8 +124,10 @@ init() {
/**
* Helper to re-calculate a Travel's TotalPrice from BookingFees, FlightPrices and Supplement Prices.
*/
this._update_totals4 = function (travel) {
this._update_totals4 = function (travel: typeof Travel) {
// Using plain native SQL for such complex queries
// FIXME: TS v
// @ts-ignore
return cds.run(`UPDATE ${Travel.drafts} SET
TotalPrice = coalesce(BookingFee,0)
+ ( SELECT coalesce (sum(FlightPrice),0) from ${Booking.drafts} where to_Travel_TravelUUID = TravelUUID )
Expand All @@ -132,18 +139,18 @@ init() {
/**
* Validate a Travel's edited data before save.
*/
this.before ('SAVE', 'Travel', req => {
this.before ('SAVE', Travel, req => {
const { BeginDate, EndDate, BookingFee, to_Agency_AgencyID, to_Customer_CustomerID, to_Booking, TravelStatus_code } = req.data, today = (new Date).toISOString().slice(0,10)

// validate only not rejected travels
if (TravelStatus_code !== 'X') {
if (TravelStatus_code !== TravelStatus.code.Canceled) {
if (BookingFee == null) req.error(400, "Enter a booking fee", "in/BookingFee") // 0 is a valid BookingFee
if (!BeginDate) req.error(400, "Enter a begin date", "in/BeginDate")
if (!EndDate) req.error(400, "Enter an end date", "in/EndDate")
if (!to_Agency_AgencyID) req.error(400, "Enter a travel agency", "in/to_Agency_AgencyID")
if (!to_Customer_CustomerID) req.error(400, "Enter a customer", "in/to_Customer_CustomerID")

for (const booking of to_Booking) {
for (const booking of to_Booking ?? []) {
const { BookingUUID, ConnectionID, FlightDate, FlightPrice, BookingStatus_code, to_Carrier_AirlineID, to_Customer_CustomerID } = booking
if (!ConnectionID) req.error(400, "Enter a flight", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/ConnectionID`)
if (!FlightDate) req.error(400, "Enter a flight date", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/FlightDate`)
Expand All @@ -152,47 +159,52 @@ init() {
if (!to_Carrier_AirlineID) req.error(400, "Enter an airline", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_Carrier_AirlineID`)
if (!to_Customer_CustomerID) req.error(400, "Enter a customer", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_Customer_CustomerID`)

for (const suppl of booking.to_BookSupplement) {
for (const suppl of booking.to_BookSupplement ?? []) {
const { BookSupplUUID, Price, to_Supplement_SupplementID } = suppl
if (!Price) req.error(400, "Enter a price", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_BookSupplement(BookSupplUUID='${BookSupplUUID}',IsActiveEntity=false)/Price`)
if (!to_Supplement_SupplementID) req.error(400, "Enter a supplement", `in/to_Booking(BookingUUID='${BookingUUID}',IsActiveEntity=false)/to_BookSupplement(BookSupplUUID='${BookSupplUUID}',IsActiveEntity=false)/to_Supplement_SupplementID`)
}
}
}

if (BeginDate < today) req.error (400, `Begin Date ${BeginDate} must not be before today ${today}.`, 'in/BeginDate')
if (BeginDate > EndDate) req.error (400, `Begin Date ${BeginDate} must be before End Date ${EndDate}.`, 'in/BeginDate')
if (!BeginDate || !EndDate) req.error (400, `Either Begin Date or End Date has not been set`, 'in/BeginDate')
if (BeginDate! < today) req.error (400, `Begin Date ${BeginDate} must not be before today ${today}.`, 'in/BeginDate')
if (BeginDate! > EndDate!) req.error (400, `Begin Date ${BeginDate} must be before End Date ${EndDate}.`, 'in/BeginDate')
})


//
// Action Implementations...
//

this.on ('acceptTravel', req => UPDATE (req.subject) .with ({TravelStatus_code:'A'}))
this.on ('rejectTravel', req => UPDATE (req.subject) .with ({TravelStatus_code:'X'}))
this.on ('deductDiscount', async req => {
const { acceptTravel, deductDiscount, rejectTravel } = Travel.actions

this.on (acceptTravel, req => UPDATE (req.subject) .with ({TravelStatus_code: TravelStatus.code.Accepted}))
this.on (rejectTravel, req => UPDATE (req.subject) .with ({TravelStatus_code: TravelStatus.code.Canceled}))
this.on (deductDiscount, TravelService.name, async req => {
// FIXME: TS v
const subject = req.subject as unknown as string
//@ts-ignore FIXME action param invalid
let discount = req.data.percent / 100
let succeeded = await UPDATE (req.subject)
.where `TravelStatus_code != 'A'`
.and `BookingFee is not null`
.with (`
TotalPrice = round (TotalPrice - BookingFee * ${discount}, 3),
BookingFee = round (BookingFee - BookingFee * ${discount}, 3)
`)
` as {})
// FIXME: TS ^
if (!succeeded) { //> let's find out why...
let travel = await SELECT.one `TravelID as ID, TravelStatus_code as status, BookingFee` .from (req.subject)
let travel = await SELECT.one `TravelID as ID, TravelStatus_code as status, BookingFee` .from (subject)
if (!travel) throw req.reject (404, `Travel "${travel.ID}" does not exist; may have been deleted meanwhile.`)
if (travel.status === 'A') req.reject (400, `Travel "${travel.ID}" has been approved already.`)
if (travel.status === TravelStatus.code.Accepted) req.reject (400, `Travel "${travel.ID}" has been approved already.`)
if (travel.BookingFee == null) throw req.reject (404, `No discount possible, as travel "${travel.ID}" does not yet have a booking fee added.`)
} else {
return this.read(req.subject)
// FIXME: TS v
return this.read<typeof Travel>(subject)
}
})


// Add base class's handlers. Handlers registered above go first.
return super.init()

}}
module.exports = {TravelService}
}}
Loading