init commit

This commit is contained in:
Valdior 2025-05-14 21:45:16 +02:00
commit d8446080cc
328 changed files with 55669 additions and 0 deletions

18
.eslintrc.js Normal file
View File

@ -0,0 +1,18 @@
module.exports = {
parser: '@typescript-eslint/parser', // Specifies the ESLint parser
extends: [
'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
},
rules: {
// Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any":"off",
"@typescript-eslint/interface-name-prefix":"off"
},
};

5
.estlintignore Normal file
View File

@ -0,0 +1,5 @@
.webpack/*
schema.d.ts
validation.ts
redoc-static.html

21
.gitignore vendored Normal file
View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
package-lock.json
.build
.webpack
*/**/.env
*/**/postman.json
*/**/schema.d.ts
*/**/*.d.ts
src/config/.env.json
.serverless/
config/.env.json
handler.ts
.slslogs.txt
.vscode

8
.prettierrc.js Normal file
View File

@ -0,0 +1,8 @@
module.exports = {
semi: true,
trailingComma: 'all',
singleQuote: true,
printWidth: 150,
tabWidth: 4,
arrowParens: "always"
};

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# workinflex-backend

View File

@ -0,0 +1,23 @@
mutation getCode {
signInEmailCodeRequest(input: { email: "tom@makeit-studio.com" }) {
message
}
}
mutation signInWithCode {
signInEmailCodeConfirm(input: { email: "tom@makeit-studio.com", code: 731560 }) {
idToken
}
}
query me {
me {
_id
}
}
mutation completeRegistration {
completeRegistration(input: { firstName: "Tom", lastName: "Roelants", organisation: "Makeit", position: "Dev" }) {
_id
}
}

36
__tests/admin/config.ts Normal file
View File

@ -0,0 +1,36 @@
import { Thunder } from './helpers/graphql-zeus';
import fetch from 'node-fetch';
const url = 'http://localhost:4001';
const token =
'eyJhbGciOiJSUzI1NiIsImtpZCI6IjVmOTcxMmEwODczMTcyMGQ2NmZkNGEyYTU5MmU0ZGZjMmI1ZGU1OTUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vc29mdHdhcmUtdGVhbS1zdGFnaW5nIiwiYXVkIjoic29mdHdhcmUtdGVhbS1zdGFnaW5nIiwiYXV0aF90aW1lIjoxNjEwNzE1ODA4LCJ1c2VyX2lkIjoiTDNpQ3RkZDRzb2djYVJ1Y1RXbVVJNjM0aWo1MyIsInN1YiI6IkwzaUN0ZGQ0c29nY2FSdWNUV21VSTYzNGlqNTMiLCJpYXQiOjE2MTA3MTU4MDgsImV4cCI6MTYxMDcxOTQwOCwiZW1haWwiOiJzc3llZC5tYWtlaXRAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsic3N5ZWQubWFrZWl0QGdtYWlsLmNvbSJdfSwic2lnbl9pbl9wcm92aWRlciI6ImN1c3RvbSJ9fQ.o3HXVp5lrIk_bzaTqb-aA7bpRJLb-Jp7lPz7_O7PQdsOaw5-L066HFtp5I-meS2CdpvUXy1REvAntGEeTNFukBzOkcRjBYagaxwUd_KpZOOoru5wpvTW8jJXlzmQXmDbHm2zfmzSCxsmrdZz62p_mKKJgGw5QTZB_LxUqTo6v0rhboXOowTzvAaHf1iwSU84JfrZwKU4xd0031RZPFbi6NDgHprYIDx7L5Fsl6bQhXDEXELDe4RNYQ5b0CKXa8rdJE9rBuroGJAJhB42_iwDwCa07FwBLIJzXefKqI97s15s23RzVT5o81EtIDOlOI6nBhucsad4boxacT6VBuS_vw';
export const thunder = Thunder(async (query) => {
const headers: any = {
'Content-Type': 'application/json',
};
if (token) headers.authorization = token;
const response = await fetch(url, {
body: JSON.stringify({ query }),
method: 'POST',
headers: headers,
});
if (!response.ok) {
return new Promise((resolve, reject) => {
response
.text()
.then((text) => {
try {
reject(JSON.parse(text));
} catch (err) {
reject(text);
}
})
.catch(reject);
});
}
const json = await response.json();
return json.data;
});

File diff suppressed because it is too large Load Diff

0
__tests/admin/index.ts Normal file
View File

7
__tests/app/config.ts Normal file
View File

@ -0,0 +1,7 @@
import { Thunder } from './helpers/graphql-zeus';
import { testRunQuery } from '../../lib/seed/__tests/helper';
const url = 'http://localhost:4000';
const token = 'eyJhbGciOiJSUzI1NiIsImtpZCI6IjRlMDBlOGZlNWYyYzg4Y2YwYzcwNDRmMzA3ZjdlNzM5Nzg4ZTRmMWUiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vd29ya2luZ2ZsZXgtcHJvZHVjdGlvbiIsImF1ZCI6IndvcmtpbmdmbGV4LXByb2R1Y3Rpb24iLCJhdXRoX3RpbWUiOjE2MTYxNzE2MTcsInVzZXJfaWQiOiI4TW1EdmZHbWV0Ylc5aVE5S0Iybm55TFRsV3gyIiwic3ViIjoiOE1tRHZmR21ldGJXOWlROUtCMm5ueUxUbFd4MiIsImlhdCI6MTYxNjE3MTYxNywiZXhwIjoxNjE2MTc1MjE3LCJlbWFpbCI6Im5pY29sYXMuYS5iZXJuaWVyK3dvcmtAZ21haWwuY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImZpcmViYXNlIjp7ImlkZW50aXRpZXMiOnsiZW1haWwiOlsibmljb2xhcy5hLmJlcm5pZXIrd29ya0BnbWFpbC5jb20iXX0sInNpZ25faW5fcHJvdmlkZXIiOiJjdXN0b20ifX0.JQvWIyuMmh2tWtf6I1ACWadhyC7LbwvBdaqE0wtXy5YX2dj_cdrKyV1H35nmJ4xRXAoTfMIdarnfPqZaCcCDrAMVey4Jt2BU9dQ0COoQ-qz5KGzeFbeUU-MDSx3sP7sEm_qsdcgPvBHPAj2WnBjrUh58Je1JRVORcXbn9oopJ0v-U2sSn1H43Ycv5qDGF9eqPHaz3ex0NmVum77JRPIKGOKPopCDKrJgMG1tlUiSsr2Qo0YgmxuzY5SrIvVImXmyclNMNrEiRsemTACBScX9seDl_vKosbgIPFaTFqtJksI8NeKxBQFCKz78DwZpZPHxwlN7IEP-ilzIbbppFoHFIQ'
export const thunder = Thunder(testRunQuery(url, token));

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

16
__tests/app/index.ts Normal file
View File

@ -0,0 +1,16 @@
import { finaliseCheckoutWithStripe, reserveOneBooking } from './src/bookings';
import { getOneWorkspace, searchWorkspace } from './src/workspace';
/*
Workspace
*/
// searchWorkspace();
// getOneWorkspace();
/*
Bookings
*/
reserveOneBooking();
// finaliseCheckoutWithStripe();

View File

@ -0,0 +1,72 @@
import { thunder } from '../config';
import { BookingsRessourceEnum, DurationTypeEnum } from '../helpers/graphql-zeus';
export const reserveOneBooking = async () => {
const reserveOneBooking = await thunder.mutation({
reserveOneBooking: [
{
input: {
durationType: DurationTypeEnum.slot,
slot: {
startDate: '2021-03-18T23:00:00.000Z',
endDate: '2021-03-19T23:00:00.000Z',
startTime: '12:00',
endTime: '14:30',
},
ressourceId: '6187782d-0445-4c0d-8c31-6c0c48a95312',
ressourceModel: BookingsRessourceEnum.workspaces,
},
},
{
_id: true,
'finalPrice':true,
subTotalPrice:true,
"vatPrice":true,
paymentIntent: {
stripePaymentIntentData: {
client_secret: true,
id: true,
},
},
},
],
});
};
export const finaliseCheckoutWithStripe = async () => {
return await thunder.mutation({
finaliseCheckoutWithStripe: [
{
input: {
billingInfo: {
firstName: 'Jay',
lastName: 'Son',
email: '',
address: {
city: 'BX',
country: 'Belgium',
},
},
contactInfo: {
firstName: 'Jay',
lastName: 'Son',
email: '',
address: {
city: 'BX',
country: 'Belgium',
},
},
},
},
{
_id: true,
paymentIntent: {
stripePaymentIntentData: {
client_secret: true,
id: true,
},
},
},
],
});
};

View File

@ -0,0 +1,49 @@
import { thunder } from '../config';
import { BookingsRessourceEnum, DurationTypeEnum } from '../helpers/graphql-zeus';
export const searchWorkspace = async () => {
const searchWorkspace = await thunder.query({
workspacesSearchMany: [
{
dateFrom: '2021-03-18T00:00:00',
dateTo: '2021-03-20T00:00:00',
hoursFrom: '10:00',
hoursTo: '12:00',
},
{
_id: true,
title: true,
},
],
});
console.log(searchWorkspace);
return searchWorkspace;
};
export const getOneWorkspace = async () => {
return await thunder.query({
workspacesGetOne: [
{
id: '6187782d-0445-4c0d-8c31-6c0c48a95312',
},
{
_id: true,
title: true,
getOwner: {
firstName: true,
lastName: true,
},
getBookings: [
{},
{
dates: {
startDate: true,
endDate: true,
},
},
],
},
],
});
};

11
__tests__/booking.gql Normal file
View File

@ -0,0 +1,11 @@
mutation createBooking {
createOneBooking(input:{ressourceModel:workspaces,ressourceId:"718aa94e-f13d-4ee3-9610-c12c98750c08",dates:{startDate:"2020-11-01T23:15:17.044+0000",endDate:"2020-11-02T23:15:17.044+0000"},capacity:10}){_id,ownerId,owner{email}}
}
query bookingsGetMany {
bookingsGetMany{_id, workspace{_id,title}}
}
query bookingsGetOne {
bookingsGetOne(id:"2a3c6b3d-e8e5-4a4d-9c10-6e438ac22e10"){_id, workspace{_id,title}}
}

7
__tests__/signin.gql Normal file
View File

@ -0,0 +1,7 @@
mutation resetPassword {
resetPassword(input: { email: "sanawar@makeit-studio.com" }) {
message
}
}

2
__tests__/signup.gql Normal file
View File

@ -0,0 +1,2 @@
query me {me{paymentInfo{stripeInfo{customerId},paymentMethods{sPayMethodId,default,nameOnCard},billingInfos{firstName,lastName,vatNumber}}}}
query login {login(creds:{email:"nicolas.a.bernier@gmail.com",password:"aaaaaa"}){localId,idToken}}

66
__tests__/workspace.gql Normal file
View File

@ -0,0 +1,66 @@
query workspacesSearchMany {
workspacesSearchMany(
workspaceType: open
priceMin: 5
priceMax: 15
dateFrom: "2020-09-25T15:58:40.803+0000"
dateTo: "2020-12-20T15:58:40.803+0000"
hoursFrom: "08:00"
hoursTo: "12:00"
) {
_id
title {
en
}
}
}
query workspaceGetOne {
workspacesGetOne(id: "5061682a-11df-48a8-9894-9ebc577c63d5") {
_id
title {
en
}
}
}
query workspacesGetMine {
workspacesGetMine {
_id
title {
en
}
}
}
mutation workspacesAddOne {
workspacesAddOne(
input: {
place: {
placeId: "ChIJrZYyJf_Rw0cR8I227YjcDEQ"
loc: { type: "Point", coordinates: [4.3879295, 50.6819832] }
formattedAddress: "Avenue de la Paix 36, 1420 Braine-l'Alleud, Belgium"
}
workspaceType: open
pricingPerHour: 25
currency: eur
maxCapacity: 4
minStay: 1
maxStay: 2
title: { fr: "Le rouge petillant", en: "The Red Velvet" }
availability: {
dateFrom: "2020-09-24T15:58:40.803+0000"
dateTo: "2020-12-24T15:58:40.803+0000"
weekend: true
hours: [{ from: "08:00", to: "20:00" }]
exceptions: [{ dateFrom: "2020-10-24T15:58:40.803+0000", dateTo: "2020-10-31T15:58:40.803+0000" }]
}
equipmentIds: []
featureIds: []
}
) {
_id
title {
en
}
}
}

24
config/config.ts Normal file
View File

@ -0,0 +1,24 @@
import { seedErrorConfig } from '@seed/interfaces/components.errors';
import { engineBookingErrorConfig } from '@services/module-booking/components.errors';
import { enginePaymentErrorConfig } from '@services/module-payments/components/components.errors';
export enum NotificationEnum {
// emailCodeSignin = "emailCodeSignin",
// magicLink = "magicLink",
adminAccountAddOne = "adminAccountAddOne",
resetPassword = "resetPassword",
bookingUserConfirm = "bookingUserConfirm",
bookingProConfirm = "bookingProConfirm",
bookingUserCancel = "bookingUserCancel",
bookingProCancel = "bookingProCancel",
welcomeUser = "welcomeUser",
}
export const ErrorsConfig = {
...seedErrorConfig,
...enginePaymentErrorConfig,
...engineBookingErrorConfig,
// Auth & Registration
4000: 'You already completed your profile',
};

19
config/domains.yaml Normal file
View File

@ -0,0 +1,19 @@
## Instal ##
## run : ./node_modules/.bin/sls plugin install --name serverless-domain-manager --config ./lib/seed/serverless-domains.yaml
## run : SLS_DEBUG=* ./node_modules/.bin/sls create_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile
## SLS_DEBUG=* ./node_modules/.bin/sls delete_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile
domains:
production: api.work-in-flex.com
staging: staging-api.work-in-flex.com
dev: dev-api.work-in-flex.com
dev-v2: dev-v2-api.work-in-flex.com
customDomain:
basePath: '${self:service}'
domainName: ${self:custom.domains.${self:custom.stage}}
stage: '${self:custom.stage}'
createRoute53Record: true
certificateName: work-in-flex.com
certificateArn: arn:aws:acm:us-east-1:555646219416:certificate/07e83105-1554-4e3d-b968-0a34d77911ad
endpointType: 'edge'
securityPolicy: tls_1_2

View File

@ -0,0 +1,32 @@
app: ${file(./package.json):name}
service: cronjob
package:
individually: false
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
environment:
AWS_compute: ${self:service}-${self:provider.stage}-compute
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: '*'
plugins:
- serverless-webpack
custom:
stage: ${opt:stage, self:provider.stage}
webpack:
webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file
includeModules:
forceExclude:
- aws-sdk
- puppeteer
functions:
removeDraftBookings:
handler: lib/__cronjobs/handler.removeDraftBookings
timeout: 300 # optional, in seconds, default is 6
events:
- schedule: rate(1 minute)

29
lib/__hooks/index.ts Normal file
View File

@ -0,0 +1,29 @@
export class AsyncHooksService {
/* [MODULES - SHOP] */
public async afterCheckout(): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
public async afterOrderCancel(data,ctx): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
public async afterOrdersMarkOneAsPaid(data,ctx): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
public async afterOrdersMarkOneAsUnPaid(data,ctx): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
public async afterOrdersReimbursed(data,ctx): Promise<any> {
console.log('[NO ACTION] - afterOrdersReimbursed');
}
/* [MODULES - EVENTS] */
public async afterCheckoutEvent(): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
}

19
lib/seed/.gitignore vendored Normal file
View File

@ -0,0 +1,19 @@
node_modules
.DS_Store
package-lock.json
.build
.webpack
*/**/.env
*/**/postman.json
*/**/schema.d.ts
*/**/*.d.ts
src/config/.env.json
.serverless/
config/.env.json
serverless.yaml

73
lib/seed/README.md Normal file
View File

@ -0,0 +1,73 @@
# Make-it Seed V5
[![](https://ci6.googleusercontent.com/proxy/2HHmNs0zn0uGJb9RqML73pX6Bmd-BntMaIYR6IpXeIHKqn2_tG60C1pLNtMHoLHOk1I5CT8k5x9gvb-C1zuudHnD5HDOpvHljVYcKmDL--rb0Ar8IT4UB7YaG1c=s0-d-e1-ft#https://makeit-assets.s3.eu-central-1.amazonaws.com/signature_sanawar.png)](https://makeit-studio.com)
![Build Status](https://travis-ci.org/joemccann/dillinger.svg?branch=master)
### GrapQL carefull
- Fields resolvers must await the response (cannot return a promise)
- Classical errors
-- Cannot determine GraphQL output type for 'xxx' of 'YOUR_CLASS' class. Does the value used as its TS type or explicit type is decorated with a proper decorator or is it a proper output value?
--- You forgot to put @ObjectType or @InputType in your class definition. Or your forgot to precise on the decorator what type of field it was @Field(() => YOUR_CLASS)
### Serverless carefull
- Test your serverless file npx sls print --config ./devOps/serverless-upload.yaml
### Installation
In order to make the seed run and setup your project you need to
- copy the account service in your application folder (see example folder)
- copy the \_\_loaders file in your application folder (see example folder)
- copy the \_\_resolvers.ts file in your application folder (see example folder)
- copy the .env file in your config folder (see example folder)
- put the correct admin email
### Domains
- run : ./node_modules/.bin/sls plugin install --name serverless-domain-manager --config ./lib/seed/serverless-domains.yaml
- run : SLS_DEBUG=\* ./node_modules/.bin/sls create_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile
- to delete : SLS_DEBUG=\* ./node_modules/.bin/sls delete_domain --config ./lib/seed/serverless-domains.yaml --aws-profile awsProfile
### VSCODE CONFIG
npm i -g eslint eslint-config-prettier eslint-plugin-prettier prettier
### If you need REST APIS
npm install --save express serverless-http
Check examples/app-rest.ts
# SOME ENV CONFIG
TWILIO_FROM_NUMBER
TWILIO_ACCOUNT_SID
TWILIO_AUTH_TOKEN
# How to write tests
npm i -g graphql-zeus
Have your dev running
zeus http://localhost:4000 ./**tests/app/helpers --ts
zeus http://localhost:4001 ./**tests/admin/helpers --ts
# Search Engine
--- Free form text
(1) Create text index on searchT field
# Cronjobs
copy examples/**cronjobs to lib/**cronjobs
Add this to your package.json
"cronjobs": [
"./lib/__cronjobs",
"serverless-cron.yaml",
"handler.ts"
]

46
lib/seed/__loaders.ts Normal file
View File

@ -0,0 +1,46 @@
import CategoryModel from '@services/module-cms/functions/categories/category.model';
import ListModel from '@services/module-cms/functions/lists/list.model';
import PageModel from '@services/module-cms/functions/pages/pages.model';
import AccountModel from '@src/accounts/account.model';
import DataLoader from 'dataloader';
import StreamEngineModel from './engine/utils/streams/stream.model';
import { buildEngineLoader, buildLoader } from './services/database/LoaderService';
import NotificationModel from './services/notifications/notifications.model';
/*
!!! Do not use, copy on parent only !
*/
const seedModelLoaders = {
'stream.changes': new StreamEngineModel(),
streams: new StreamEngineModel(),
notifications: new NotificationModel(),
accounts: new AccountModel(),
pages: new PageModel(),
};
class SeedLoaders {
/* CMS */
listLoader: DataLoader<string, ListModel, string>;
categoryLoader: DataLoader<string, CategoryModel, string>;
// tagsLoader: DataLoader<string, TagSchema, string>;
// answerLoader: DataLoader<string, AnswerEngineSchema, string>;
/* ACCOUNT */
accountLoader: DataLoader<string, AccountModel, string>;
public constructor() {
this.listLoader = buildLoader<ListModel>(new ListModel());
this.categoryLoader = buildLoader<CategoryModel>(new CategoryModel());
// this.tagsLoader = buildEngineLoader<TagModel, TagSchema>(new TagModel());
// this.answerLoader = buildEngineLoader<AnswerEngineModel, AnswerEngineSchema>(new AnswerEngineModel());
this.accountLoader = buildLoader<AccountModel>(new AccountModel());
}
}

View File

@ -0,0 +1,33 @@
import { clog } from '../helpers/Utils';
import fetch from 'node-fetch';
export function testRunQuery(url: string, token: string): (query: string, variables?: Record<string, any>) => Promise<any> {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
return async (query, variables) => {
const headers: any = {
'Content-Type': 'application/json',
};
if (token && (token as any) != '') headers.authorization = token;
const r = JSON.stringify({ query, variables });
clog({ query, variables });
const response = await fetch(url, {
body: r,
method: 'POST',
headers: headers,
});
try {
const text = await response.text();
const res = JSON.parse(text);
if (res.errors) clog(res.errors[0]);
clog(res.data);
} catch (err) {
console.log(err);
}
return;
};
}

66
lib/seed/app-admin.ts Normal file
View File

@ -0,0 +1,66 @@
import 'reflect-metadata';
import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware';
import Firebase from '@seed/services/auth/FirebaseService';
import { AdminResolvers } from '@src/__indexes/__resolvers.index.admin';
import { SettingsCache } from './graphql/Settings';
import { buildSchema } from 'type-graphql';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ApolloServer } = require('apollo-server');
const PORT = process.env.PORT || 4001;
const bootstrap = async (): Promise<any> => {
process.env.NODE_ENV = 'local';
const settingsI = SettingsCache.getInstance();
await settingsI.refreshCache();
try {
const schema = await buildSchema({
resolvers: AdminResolvers as any,
authChecker: authMiddleware,
});
const server = new ApolloServer({
// typeDefs,
// resolvers,
schema,
formatError: errorMiddleware,
formatResponse: (response): any => {
return response;
},
context: ctxMiddleware,
tracing: false,
plugins: [
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
requestDidStart: () => ({
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
didResolveOperation({ request, document }) {
return complexityMiddleware(schema, request.variables, document);
},
}),
},
],
});
try {
const token = await Firebase.getInstance().createTokenId(process.env.TEST_ADMIN_EMAIL ? process.env.TEST_ADMIN_EMAIL : '');
console.log(token);
} catch (error) {
console.log('no token available');
}
server.listen(PORT).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
} catch (error) {
console.log('[GRAPH ERROR]', error);
throw error;
}
};
bootstrap();

72
lib/seed/app.ts Normal file
View File

@ -0,0 +1,72 @@
import 'reflect-metadata';
import { buildSchema, NonEmptyArray } from 'type-graphql';
import { authMiddleware, errorMiddleware, ctxMiddleware, complexityMiddleware } from '@seed/graphql/Middleware';
import Firebase from '@seed/services/auth/FirebaseService';
import { AppResolvers } from '@src/__indexes/__resolvers.index';
import { SettingsCache } from './graphql/Settings';
import { runBeforeAll } from '@src/__indexes/__bootstrap';
import { nullToUndefined } from './helpers/Utils';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { ApolloServer } = require('apollo-server');
const PORT = process.env.PORT || 4000;
const bootstrap = async (): Promise<any> => {
process.env.NODE_ENV = 'local';
const settingsI = SettingsCache.getInstance();
await settingsI.refreshCache();
try {
const schema = await buildSchema({
resolvers: (AppResolvers as unknown) as NonEmptyArray<Function>,
authChecker: authMiddleware,
validate: false, // disable automatic validation or pass the default config object
});
const server = new ApolloServer({
// typeDefs,
// resolvers,
schema,
formatError: errorMiddleware,
formatResponse: (response): any => {
return nullToUndefined(response);
},
context: ctxMiddleware,
tracing: false,
plugins: [
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
requestDidStart: () => ({
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
didResolveOperation({ request, document }) {
return complexityMiddleware(schema, request.variables, document);
},
}),
},
],
});
try {
const token = await Firebase.getInstance().createTokenId(process.env.TEST_USER_EMAIL ? process.env.TEST_USER_EMAIL : '');
console.log(`Token for ${process.env.TEST_USER_EMAIL}`, token.idToken);
} catch (error) {
console.log('no token available');
}
await runBeforeAll();
server.listen(PORT).then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
} catch (error) {
console.log('[GRAPH ERROR]', error);
throw error;
}
};
bootstrap();

View File

@ -0,0 +1,117 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-var-requires */
const execa = require('execa');
const Listr = require('listr');
const package = require('../../../package.json');
const fs = require('fs');
if (!package.devops) throw `No devops configuration found in package.json`;
if (!package.devops.services) throw `No devops services found in package.json`;
if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`;
if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json';
// Config input
const awsProfile = package.devops.awsProfile;
const awsRegion = package.devops.awsRegion;
// Arguments checks
const deployBase = function() {
return new Listr(
[
{
title: 'Deploy domains for DEV',
task: async () => {
const subprocess = execa(
'npx',
[
'./node_modules/.bin/sls',
'create_domain',
'--config',
'./lib/seed/devops/serverless-domains.yaml',
'--stage',
'dev',
'--aws-profile',
awsProfile,
'--region',
awsRegion,
],
{ env: { 'SLS_DEBUG=*': '*' } },
);
subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' }));
const { stdout } = await subprocess;
},
},
{
title: 'Deploy domains for STAGING',
task: async () => {
const subprocess = execa(
'npx',
[
'./node_modules/.bin/sls',
'create_domain',
'--config',
'./lib/seed/devops/serverless-domains.yaml',
'--stage',
'staging',
'--aws-profile',
awsProfile,
'--region',
awsRegion,
],
{ env: { 'SLS_DEBUG=*': '*' } },
);
subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' }));
const { stdout } = await subprocess;
},
},
{
title: 'Deploy domains for PRODUCTION',
task: async () => {
const subprocess = execa(
'npx',
[
'./node_modules/.bin/sls',
'create_domain',
'--config',
'./lib/seed/devops/serverless-domains.yaml',
'--stage',
'production',
'--aws-profile',
awsProfile,
'--region',
awsRegion,
],
{ env: { 'SLS_DEBUG=*': '*' } },
);
subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' }));
const { stdout } = await subprocess;
},
},
],
{ concurrent: false },
);
};
const tasksList = [];
tasksList.push({
title: 'Deploying Domains',
task: () => {
return deployBase();
},
});
const tasks = new Listr(tasksList, { concurrent: false });
tasks.run().catch((err) => {
console.error(err);
});

View File

@ -0,0 +1,98 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-var-requires */
const execa = require('execa');
const Listr = require('listr');
const package = require('../../../package.json');
const fs = require('fs');
if (!package.devops) throw `No devops configuration found in package.json`;
if (!package.devops.services) throw `No devops services found in package.json`;
if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`;
if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json';
// Config input
const paths = package.devops.services;
const awsProfile = package.devops.awsProfile;
const awsRegion = package.devops.awsRegion;
// Arguments checks
const stage = process.argv[2] || 'dev';
const sName = process.argv[3] || 'all';
const deployBase = function(basePath) {
const baseDirectory = basePath[0];
const serverlessFile = baseDirectory + '/' + basePath[1];
let handlerFile;
if (basePath[2]) handlerFile = baseDirectory + '/' + basePath[2];
else handlerFile = baseDirectory + '/' + 'handler.ts';
return new Listr(
[
{
title: 'Remove handler file',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}),
},
{
title: 'Copy handler file',
task: () => {
console.log('handler file here', handlerFile);
execa('cp', [handlerFile, 'handler.ts']).then((result) => {});
},
},
{
title: 'Deploy serverless file',
task: async () => {
console.log('serverless file here', serverlessFile);
const subprocess = execa(
'npx',
['sls', 'deploy', '--config', serverlessFile, '--stage', stage, '--aws-profile', awsProfile, '--region', awsRegion],
{ env: { 'SLS_DEBUG=*': '*' } },
);
console.log('deploying logs here', '.slslogs.txt');
subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' }));
const { stdout } = await subprocess;
},
},
{
title: 'Remove handler file',
task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}),
},
],
{ concurrent: false },
);
};
let functions = Object.keys(paths);
if (sName != 'all') {
functions = sName.split(',');
for (let index = 0; index < functions.length; index++) {
if (!paths.hasOwnProperty(functions[index])) throw `Bad functions name for ${functions[index]}`;
}
}
const tasksList = [];
for (let index = 0; index < functions.length; index++) {
const element = functions[index];
tasksList.push({
title: 'Deploying' + ' ' + element,
task: () => {
return deployBase(paths[element]);
},
});
}
const tasks = new Listr(tasksList, { concurrent: false });
tasks.run().catch((err) => {
console.error(err);
});

103
lib/seed/devops/deploy.js Normal file
View File

@ -0,0 +1,103 @@
/* eslint-disable @typescript-eslint/no-empty-function */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable @typescript-eslint/no-var-requires */
const execa = require('execa');
const Listr = require('listr');
const package = require('../../../package.json');
const fs = require('fs');
if (!package.devops) throw `No devops configuration found in package.json`;
if (!package.devops.services) throw `No devops services found in package.json`;
if (!package.devops.awsProfile) throw `No devops awsProfile found in package.json`;
if (!package.devops.awsRegion) throw 'No devops awsRegion found in package.json';
// Config input
const paths = package.devops.services;
const awsProfile = package.devops.awsProfile;
const awsRegion = package.devops.awsRegion;
// Arguments checks
const stage = process.argv[2] || 'dev';
const sName = process.argv[3] || 'all';
const deployBase = function(basePath) {
const baseDirectory = basePath[0];
const serverlessFile = baseDirectory + '/' + basePath[1];
let handlerFile;
if (basePath[2]) handlerFile = baseDirectory + '/' + basePath[2];
else handlerFile = baseDirectory + '/' + 'handler.ts';
return new Listr(
[
{
title: 'Remove handler file',
// eslint-disable-next-line @typescript-eslint/no-unused-vars
task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}),
},
{
title: 'Copy handler file',
task: () => {
console.log('handler file here', handlerFile);
execa('cp', [handlerFile, 'handler.ts']).then((result) => {});
},
},
{
title: 'Copy serverless file',
task: () => {
console.log('serverless file here', serverlessFile);
execa('cp', [serverlessFile, 'serverless.yaml']).then((result) => {});
},
},
{
title: 'Deploy serverless file',
task: async () => {
const subprocess = execa(
'npx',
['sls', 'deploy', /*'--config', serverlessFile,*/ '--stage', stage, '--aws-profile', awsProfile, '--region', awsRegion],
{ env: { 'SLS_DEBUG=*': '*' } },
);
console.log('deploying logs here', '.slslogs.txt');
subprocess.stdout.pipe(fs.createWriteStream('.slslogs.txt', { flags: 'a' }));
const { stdout } = await subprocess;
},
},
{
title: 'Remove handler file',
task: () => execa('rm', ['-f', 'handler.ts']).then((result) => {}),
},
],
{ concurrent: false },
);
};
let functions = Object.keys(paths);
if (sName != 'all') {
functions = sName.split(',');
for (let index = 0; index < functions.length; index++) {
if (!paths.hasOwnProperty(functions[index])) throw `Bad functions name for ${functions[index]}`;
}
}
const tasksList = [];
for (let index = 0; index < functions.length; index++) {
const element = functions[index];
tasksList.push({
title: 'Deploying' + ' ' + element,
task: () => {
return deployBase(paths[element]);
},
});
}
const tasks = new Listr(tasksList, { concurrent: false });
tasks.run().catch((err) => {
console.error(err);
});

View File

@ -0,0 +1,10 @@
import 'reflect-metadata';
import { ServerInstance } from '@seed/graphql/Server';
import { AdminResolvers } from '@src/__indexes/__resolvers.index.admin';
export async function adminHandler(event: any, context: any): Promise<any> {
const apolloInstance = await ServerInstance.getInstance(AdminResolvers);
const response = await apolloInstance.run(event, context);
return response;
}

View File

@ -0,0 +1,9 @@
import 'reflect-metadata';
import { ServerInstance } from '@seed/graphql/Server';
import { AppResolvers } from '@src/__indexes/__resolvers.index';
export async function appHandler(event: any, context: any): Promise<any> {
const apolloInstance = await ServerInstance.getInstance(AppResolvers);
const response = await apolloInstance.run(event, context);
return response;
}

View File

@ -0,0 +1,153 @@
/* eslint-disable @typescript-eslint/no-use-before-define */
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import 'reflect-metadata';
import { modelsLoaders } from '@src/__indexes/__loaders';
import ChangeStreamModel from '@lib/seed/services/change-stream/change-stream.model';
import { StreamOperationType, PostHookStatus } from '@lib/seed/services/change-stream/change-stream.components';
import { AsyncHooksService } from '@lib/__hooks';
import { createApolloContext } from '@seed/graphql/Middleware';
import { SettingsCache } from '@seed/graphql/Settings';
export const hookHandler = async (event: any): Promise<void> => {
const settingsI = SettingsCache.getInstance();
await settingsI.refreshCache();
process.env.isHook = 'true';
const { streamId, notificationRessourceId } = event;
if (streamId) {
await streamHandler(streamId);
} else if (notificationRessourceId) {
await notificationHandler(notificationRessourceId);
} else {
console.log('[ASYNC - ERROR]', 'was called with no info');
}
return;
};
async function notificationHandler(streamId: string) {
const stream = await (await new ChangeStreamModel().db()).findOneAndUpdate(
{ _id: streamId, hookStatus: PostHookStatus.new },
{ $set: { hookStatus: PostHookStatus.inProcess } },
);
if (stream && stream.value) {
const streamData = stream.value;
console.log('[ASYNC - STREAM] Stream', stream.value.collection, stream.value.operation);
try {
if (streamData.operation == StreamOperationType.hook) {
const HookService = new AsyncHooksService();
if (!HookService[streamData.collection])
throw `[ASYNC - STREAM] ERROR - No AsyncHooksService for this ${streamData.collection}. Are you sure you put it in the AsyncHooksService ?`;
if (streamData.insertedValues && streamData.insertedValues.length > 0) {
const paramsArray = streamData.insertedValues as any[];
for (let index = 0; index < paramsArray.length; index++) {
// Deal with CTX
if (paramsArray[index].ctx) {
paramsArray[index] = await createApolloContext(paramsArray[index].ctx.user._id, paramsArray[index].ctx.organisationId);
}
}
await HookService[streamData.collection](...paramsArray);
} else await HookService[streamData.collection]();
} else {
if (!modelsLoaders[streamData.collection])
throw `[ASYNC - STREAM] ERROR - No ModelLoaders for this ${streamData.collection}. Are you sure you put it in the ModelLoaders ?`;
console.log('streamData', streamData);
const model = modelsLoaders[streamData.collection];
const modelData = await model.getOne({ _id: streamData.documentKey }, null);
console.log('model', model);
switch (streamData.operation) {
case StreamOperationType.insert:
await model.afterCreate();
break;
case StreamOperationType.update:
await model.afterUpdate();
break;
case StreamOperationType.delete:
await model.afterDelete();
break;
default:
throw `[ASYNC - STREAM] ERROR - No operation ${streamData.operation} for this ${streamData.collection}`;
}
}
await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.completed } });
console.log('[ASYNC - STREAM] COMPLETE', { _id: streamId });
} catch (error) {
console.error('[ASYNC - STREAM] ERROR', error);
await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.error } });
}
} else {
console.error('[ASYNC - STREAM] ERROR', 'No new stream found', streamId);
}
}
async function streamHandler(streamId: string) {
const stream = await (await new ChangeStreamModel().db()).findOneAndUpdate(
{ _id: streamId, hookStatus: PostHookStatus.new },
{ $set: { hookStatus: PostHookStatus.inProcess } },
);
if (stream && stream.value) {
const streamData = stream.value;
console.log('[ASYNC - STREAM] Stream', stream.value.collection, stream.value.operation);
try {
if (streamData.operation == StreamOperationType.hook) {
const HookService = new AsyncHooksService();
if (!HookService[streamData.collection])
throw `[ASYNC - STREAM] ERROR - No AsyncHooksService for this ${streamData.collection}. Are you sure you put it in the AsyncHooksService ?`;
if (streamData.insertedValues && streamData.insertedValues.length > 0) {
const paramsArray = streamData.insertedValues as any[];
for (let index = 0; index < paramsArray.length; index++) {
// Deal with CTX
if (paramsArray[index].ctx) {
paramsArray[index] = await createApolloContext(paramsArray[index].ctx.user._id, paramsArray[index].ctx.organisationId);
}
}
await HookService[streamData.collection](...paramsArray);
} else await HookService[streamData.collection]();
} else {
if (!modelsLoaders[streamData.collection])
throw `[ASYNC - STREAM] ERROR - No ModelLoaders for this ${streamData.collection}. Are you sure you put it in the ModelLoaders ?`;
console.log('streamData', streamData);
const model = modelsLoaders[streamData.collection];
const modelData = await model.getOne({ _id: streamData.documentKey }, null);
console.log('model', model);
switch (streamData.operation) {
case StreamOperationType.insert:
await model.afterCreate();
break;
case StreamOperationType.update:
await model.afterUpdate();
break;
case StreamOperationType.delete:
await model.afterDelete();
break;
default:
throw `[ASYNC - STREAM] ERROR - No operation ${streamData.operation} for this ${streamData.collection}`;
}
}
await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.completed } });
console.error('[ASYNC - STREAM] COMPLETE', { _id: streamId });
} catch (error) {
console.error('[ASYNC - STREAM] ERROR', error);
await (await new ChangeStreamModel().db()).updateOne({ _id: streamId }, { $set: { hookStatus: PostHookStatus.error } });
}
} else {
console.error('[ASYNC - STREAM] ERROR', 'No new stream found', streamId);
}
}

View File

@ -0,0 +1,55 @@
app: ${file(./package.json):name}
service: admin
package:
individually: true
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
environment:
AWS_Hooks: hooks-${self:provider.stage}-hookHandler
S3_BUCKET: ${self:app}-appdata
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: '*'
- Effect: Allow
Action:
- s3:*
Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/*
plugins:
- serverless-webpack
- serverless-domain-manager
custom:
stage: ${opt:stage, self:provider.stage}
domains: ${file(./config/domains.yaml):domains}
customDomain: ${file(./config/domains.yaml):customDomain}
webpack:
webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file
includeModules:
forceExclude:
- aws-sdk
- puppeteer
functions:
admin:
handler: handler.adminHandler
timeout: 60
events:
- http:
path: /
method: post
cors:
origin: '*'
headers:
- Content-Type
- Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
- organisationid
- http:
path: /
method: get
cors: true

View File

@ -0,0 +1,60 @@
app: ${file(./package.json):name}
service: app
package:
individually: true
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
environment:
AWS_Hooks: hooks-${self:provider.stage}-hookHandler
S3_BUCKET: ${self:app}-appdata
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: '*'
- Effect: Allow
Action:
- s3:*
Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/*
plugins:
- serverless-webpack
- serverless-domain-manager
- serverless-offline
custom:
stage: ${opt:stage, self:provider.stage}
domains: ${file(./config/domains.yaml):domains}
customDomain: ${file(./config/domains.yaml):customDomain}
serverless-offline:
httpPort: 4000
lambdaPort: 4010
webpack:
webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file
includeModules:
forceExclude:
- aws-sdk
- puppeteer
functions:
app:
handler: handler.appHandler
timeout: 60
events:
- http:
path: /
method: post
cors:
origin: '*'
headers:
- Content-Type
- Authorization
- X-Authorization
- X-Api-Key
- X-Amz-Security-Token
- X-Amz-User-Agent
- organisationid
- http:
path: /
method: get
cors: true

View File

@ -0,0 +1,12 @@
app: ${file(./package.json):name}
service: domains
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
plugins:
- serverless-domain-manager
custom:
stage: ${opt:stage, self:provider.stage}
domains: ${file(./config/domains.yaml):domains}
customDomain: ${file(./config/domains.yaml):customDomain}

View File

@ -0,0 +1,37 @@
app: ${file(./package.json):name}
service: hooks
package:
individually: true
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
environment:
AWS_Hooks: hooks-${self:provider.stage}-hookHandler
S3_BUCKET: ${self:app}-appdata
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: '*'
- Effect: Allow
Action:
- s3:*
Resource: arn:aws:s3:::${self:provider.environment.S3_BUCKET}/*
plugins:
- serverless-webpack
custom:
stage: ${opt:stage, self:provider.stage}
webpack:
webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file
includeModules:
forceExclude:
- aws-sdk
- puppeteer
functions:
hookHandler:
handler: handler.hookHandler
memorySize: 1024 # optional, in MB, default is 1024
timeout: 600 # optional, in seconds, default is 6
async: true

View File

@ -0,0 +1,161 @@
import { AccountTypeEnum } from '@src/accounts/account.components';
import AccountModel from '@src/accounts/account.model';
import { newError } from '@seed/helpers/Error';
import _ from 'lodash';
export class EngineAccessService {
static addPermissions<U, T>(ressource: any, type: ('r' | 'w' | 'd')[], ids: (string | string)[]): void {
for (let index = 0; index < type.length; index++) {
const t = type[index];
ressource.permissions[t] = ressource.permissions[t].concat(ids);
}
}
static addPermissionToQuery(account: AccountModel | null, query: 'get' | 'update' | 'delete', params: any): any {
let types: any[] = ['public'];
if (account && account.types) {
/*
* Verify if admin, no need to add the query filters
*/
if (account.types.includes(AccountTypeEnum.admin)) return params;
/*
* Verify if organisation type of access
*/
// if (account.organisationIds) {
// params.organisationId = { $in: account.organisationIds };
// }
/*
* Add the account id and type
*/
types.push(account._id);
types = types.concat(account.types);
}
if (!params.$and) params.$and = [];
switch (query) {
default:
case 'get':
params.$and.push({ 'permissions.r': { $in: types } });
break;
case 'update':
params.$and.push({ 'permissions.w': { $in: types } });
break;
case 'delete':
params.$and.push({ 'permissions.d': { $in: types } });
break;
}
return params;
}
static checkPermissions<U, T>(ressource: any, account: AccountModel | null, type: 'c' | 'r' | 'w' | 'd'): boolean {
// Adding the public by default
let perm: any[] = [AccountTypeEnum.public];
if (account && account.types) {
/*
* Verify if admin
*/
if (account.types.includes(AccountTypeEnum.admin)) return true;
/*
* Verify if organisation type of access
*/
if (
ressource.organisationId &&
account.organisationIds &&
!account.organisationIds.includes(ressource.organisationId) &&
!ressource.permissions.r.includes(AccountTypeEnum.public)
)
throw newError(2100, {
allowed: ressource.organisationId,
you: account.organisationIds,
});
/*
* Add the account id and type
*/
perm.push(account._id);
perm = perm.concat(account.types);
}
/*
* Verify on the ressource level
*/
const permissions = ressource.permissions[type];
// if there is no permission on the ressource, return true
if (!permissions) return true;
let hasPerm = false;
// Verifying if it matches
for (let index = 0; index < perm.length; index++) {
const element = perm[index];
if (permissions.includes(element)) {
hasPerm = true;
break;
}
}
if (!hasPerm)
throw newError(2000, {
allowed: permissions,
you: { _id: account?._id, types: account?.types },
});
return true;
}
static checkOrganisationPermissions<U, T>(ressource: any, organisationId: string): boolean {
if (organisationId == ressource.organisationId || organisationId == ressource._id) return true;
throw newError(2100, { allowedOrgId: ressource.organisationId, youOrgId: organisationId });
}
static addOrganisationToQuery(account: AccountModel | null, query: 'get' | 'update' | 'delete', params: any): any {
let types: any[] = ['public'];
if (account && account.types) {
/*
* Verify if admin, no need to add the query filters
*/
if (account.types.includes(AccountTypeEnum.admin)) return params;
/*
* Verify if organisation type of access
*/
if (account.organisationIds) {
params.organisationId = { $in: account.organisationIds };
}
/*
* Add the account id and type
*/
types.push(account._id);
types = types.concat(account.types);
}
switch (query) {
default:
case 'get':
params = { ...params, $or: [{ 'permissions.r': { $in: _.uniq(types) } }] };
break;
case 'update':
params = { ...params, $or: [{ 'permissions.w': { $in: _.uniq(types) } }] };
break;
case 'delete':
params = { ...params, $or: [{ 'permissions.d': { $in: _.uniq(types) } }] };
break;
}
return params;
}
}

View File

@ -0,0 +1,277 @@
import _ from 'lodash';
import { ObjectType } from 'type-graphql';
import { classToPlain, Exclude, plainToClassFromExist } from 'class-transformer';
import { Collection, ObjectId } from 'mongodb';
import DB from '@seed/services/database/DBService';
import { Permission } from '@seed/interfaces/permission';
import { ApolloContext } from '@seed/interfaces/context';
import { IEngineSchema, MetaBy } from './EngineSchema';
import { GetArgs } from '@seed/graphql/Request';
import {
bulk,
deleteOne,
getAll,
getCount,
getMany,
getOne,
getQuery,
saveMany,
saveOne,
updateOne,
updateOneCustom,
updateOneSubField,
} from './utils/crud.utils';
import { getCountGeneric, getManyGeneric, getManyGenericWithArgs, getOneGeneric } from './utils/service.utils';
import {
BulkInput,
DeleteOneInput,
GetAllInput,
GetCountInput,
GetManyInput,
GetOneInput,
GetQueryInput,
SaveManyInput,
SaveOneInput,
UpdateOneCustomInput,
UpdateOneInput,
UpdateOneSubField,
} from './utils/__interface';
import { EnginePathComponent } from '@seed/interfaces/components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { nullToUndefined } from '@seed/helpers/Utils';
@ObjectType()
export abstract class EngineModel<SchemaDBInterface, SchemaDB, Schema> implements IEngineSchema {
@Exclude()
public collectionName: ModelCollectionEnum | string;
@Exclude()
public permissions: Permission;
@Exclude()
defaultSort: string;
dbData: SchemaDB;
searchT?: string;
_id: string;
organisationId?: string | undefined;
paths?: EnginePathComponent[];
by?: MetaBy | undefined;
createdAt: Date;
updatedAt: Date;
tagsIds?: string[];
public constructor(init: {
collectionName: ModelCollectionEnum | string;
permissions: Permission;
dataInit: SchemaDB & Partial<IEngineSchema>;
defaultSort?: string;
}) {
this.collectionName = init.collectionName;
this.permissions = init.permissions;
this.dbData = init.dataInit;
this.permissions = {
c: init.permissions.c,
r: init.permissions.r,
w: init.permissions.w,
d: init.permissions.d,
};
this.createdAt = new Date();
this.updatedAt = new Date();
this.defaultSort = init.defaultSort || 'createdAt desc';
}
/*
*
* Model functions
*
*/
public get(): Schema {
const data: any = this.dbData;
if (this._id) data._id = this._id;
if (this.permissions) data.permissions = this.permissions;
if (this.organisationId) data.organisationId = this.organisationId;
if (this.by) data.by = this.by;
if (this.createdAt) data.createdAt = this.createdAt;
if (this.updatedAt) data.updatedAt = this.updatedAt;
return this.plainToClass(data) as Schema;
}
public set(doc: any): void {
const newData = {
...classToPlain(this.dbData), // Get old data
...doc, // Replace with new data
};
this.dbData = _.assign(this.dbData, newData);
if (doc._id) this._id = doc._id;
if (doc.permissions) {
this.permissions = {
c: _.uniq(_.concat(doc.permissions.c)),
r: _.uniq(_.concat(doc.permissions.r)),
w: _.uniq(_.concat(doc.permissions.w)),
d: _.uniq(_.concat(doc.permissions.d)),
};
}
if (doc.organisationId) this.organisationId = doc.organisationId;
if (doc.paths) this.paths = doc.paths;
if (doc.createdAt) this.createdAt = doc.createdAt;
if (doc.updatedAt) this.updatedAt = doc.updatedAt;
}
async beforeCreate?(ctx?: ApolloContext | null): Promise<void>;
async beforeUpdate?(ctx?: ApolloContext | null): Promise<void>;
async beforeDelete?(ctx?: ApolloContext | null): Promise<void>;
async afterCreate?(changeStream: any): Promise<void>;
async afterUpdate?(changeStream: any): Promise<void>;
async afterDelete?(changeStream: any): Promise<void>;
/*
*
* Search Functions & Helpers
*
*/
searchEngine?(): any;
searchText?(): any;
/*
*
* Transform Functions
*
*/
abstract plainToClass(plain: any): Schema | Schema[];
/*
*
* DB Functions
*
*/
public async db(): Promise<Collection<SchemaDBInterface & IEngineSchema>> {
return await (await DB.getInstance()).db.collection<SchemaDBInterface & IEngineSchema>(this.collectionName);
}
generatePaths?(): EnginePathComponent[];
public getPaths(): EnginePathComponent[] {
return this.generatePaths ? this.generatePaths() : this.paths ? this.paths : [];
}
public getFullPath(): EnginePathComponent[] {
const myPath = this.getPaths();
myPath.push({
ressourceModel: this.collectionName,
ressourceId: this._id,
});
return myPath;
}
// ██████╗ ██╗ ██╗███████╗██████╗ ██╗ ██╗
// ██╔═══██╗██║ ██║██╔════╝██╔══██╗╚██╗ ██╔╝
// ██║ ██║██║ ██║█████╗ ██████╔╝ ╚████╔╝
// ██║▄▄ ██║██║ ██║██╔══╝ ██╔══██╗ ╚██╔╝
// ╚██████╔╝╚██████╔╝███████╗██║ ██║ ██║
// ╚══▀▀═╝ ╚═════╝ ╚══════╝╚═╝ ╚═╝ ╚═╝
public async getQuery(input: GetQueryInput<SchemaDBInterface>): Promise<any> {
return getQuery(this, input);
}
public async getMany(input: GetManyInput<SchemaDBInterface>): Promise<Schema[]> {
return getMany(this, input);
}
public async getCount(input: GetCountInput<SchemaDBInterface>): Promise<number> {
return getCount(this, input);
}
public async getAll(input: GetAllInput<SchemaDBInterface>): Promise<Schema[]> {
return getAll(this, input);
}
public async getOne(input: GetOneInput<SchemaDBInterface>): Promise<Schema> {
let newInput;
if (!input.query) {
newInput = { query: input };
} else newInput = input;
return getOne(this, newInput);
}
// ███╗ ███╗██╗ ██╗████████╗ █████╗ ████████╗██╗ ██████╗ ███╗ ██╗
// ████╗ ████║██║ ██║╚══██╔══╝██╔══██╗╚══██╔══╝██║██╔═══██╗████╗ ██║
// ██╔████╔██║██║ ██║ ██║ ███████║ ██║ ██║██║ ██║██╔██╗ ██║
// ██║╚██╔╝██║██║ ██║ ██║ ██╔══██║ ██║ ██║██║ ██║██║╚██╗██║
// ██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║ ██║ ██║╚██████╔╝██║ ╚████║
// ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═══╝
public async saveOne(input: SaveOneInput<SchemaDBInterface>): Promise<Schema> {
return saveOne(this, input);
}
public async updateOne(input: UpdateOneInput<SchemaDBInterface>): Promise<Schema> {
return updateOne(this, input);
}
public async updateOneCustom(input: UpdateOneCustomInput<SchemaDBInterface>): Promise<Schema> {
return updateOneCustom(this, input);
}
public async updateOneSubField(input: UpdateOneSubField<SchemaDBInterface>): Promise<Schema> {
return updateOneSubField(this, input);
}
public async deleteOne(input: DeleteOneInput<SchemaDBInterface>): Promise<Schema> {
return deleteOne(this, input);
}
public async saveMany(input: SaveManyInput<SchemaDBInterface>): Promise<Schema[]> {
return saveMany(this, input);
}
public async bulk(input: BulkInput<SchemaDBInterface, SchemaDB, Schema>): Promise<void> {
return bulk(this, input);
}
// ██████╗ ███████╗███╗ ██╗███████╗██████╗ ██╗ ██████╗███████╗
// ██╔════╝ ██╔════╝████╗ ██║██╔════╝██╔══██╗██║██╔════╝██╔════╝
// ██║ ███╗█████╗ ██╔██╗ ██║█████╗ ██████╔╝██║██║ ███████╗
// ██║ ██║██╔══╝ ██║╚██╗██║██╔══╝ ██╔══██╗██║██║ ╚════██║
// ╚██████╔╝███████╗██║ ╚████║███████╗██║ ██║██║╚██████╗███████║
// ╚═════╝ ╚══════╝╚═╝ ╚═══╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════
public async getOneGeneric(id: string, ctx: ApolloContext): Promise<Schema> {
return getOneGeneric(this, id, ctx);
}
public async getManyGeneric(query: any, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise<Schema[]> {
return getManyGeneric(this, query, pagination, ctx);
}
public async getCountGeneric(baseArguments: any, ctx: ApolloContext): Promise<number> {
return getCountGeneric(this, baseArguments, ctx);
}
public async getManyGenericWithArgs(baseArguments: any, ctx: ApolloContext | null): Promise<Schema[]> {
return getManyGenericWithArgs(this, baseArguments, ctx);
}
}

View File

@ -0,0 +1,58 @@
import { EnginePathComponent } from '@seed/interfaces/components';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { Field, ID, ObjectType, Authorized, InterfaceType } from 'type-graphql';
@ObjectType()
export class MetaBy {
@Field(() => ID, { nullable: true })
createdBy?: string;
@Field(() => ID, { nullable: true })
updatedBy?: string;
@Field(() => ID, { nullable: true })
deletedBy?: string;
}
@ObjectType()
export class MetaPermissions {
@Field(() => [String])
r: string[];
@Authorized(AccountTypeEnum.admin)
@Field(() => [String])
w: string[];
@Authorized(AccountTypeEnum.admin)
@Field(() => [String])
d: string[];
}
@InterfaceType()
export abstract class IEngineSchema {
@Field(() => ID)
_id: string;
@Field(() => ID, { nullable: true })
organisationId?: string;
@Field(() => [EnginePathComponent], { nullable: true })
paths?: EnginePathComponent[];
searchT?: string;
@Field(() => MetaBy, { nullable: true })
by?: MetaBy;
@Field(() => MetaPermissions, { nullable: true })
permissions: MetaPermissions;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => [String], { nullable: true })
tagsIds?: string[];
}

View File

@ -0,0 +1,40 @@
import { registerDecorator, ValidationOptions, ValidatorConstraint, ValidatorConstraintInterface, ValidationArguments } from 'class-validator';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import DB from '@seed/services/database/DBService';
import { newError } from '@seed/helpers/Error';
@ValidatorConstraint({ async: true })
export class IsRefExistConstraint implements ValidatorConstraintInterface {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
async validate(ressourceId: any, args: ValidationArguments & any) {
// Validate only when in the DB object
if (args.targetName.includes('DB')) {
const obj = args.object as any;
// Check what kind of id check
if (obj.ressourceId && obj.ressourceModel) {
const res = await (await DB.getInstance()).db.collection(ModelCollectionEnum[obj.ressourceModel]).findOne({ _id: obj.ressourceId });
if (!res) throw newError(4040, obj);
} else {
}
}
args.hasValidated = true;
return true;
}
}
export function IsRefExist(validationOptions?: ValidationOptions) {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
return function(object: Record<string, any>, propertyName: string) {
registerDecorator({
target: object.constructor,
propertyName: propertyName,
options: validationOptions,
constraints: [],
validator: IsRefExistConstraint,
});
};
}

View File

View File

@ -0,0 +1,114 @@
import { Query, Arg, Resolver, Ctx, Args, Mutation, ClassType } from 'type-graphql';
import { ApolloContext } from '@seed/interfaces/context';
import { EngineMiddleware, EngineMiddlewareInput } from '@seed/graphql/MiddlewareV2';
import { EngineModel } from '../EngineModel';
export const createEngineQueryResolver = <U extends ClassType<any>, T extends ClassType<any>, ARGS extends ClassType<any>>(init: {
domain: string;
schemaName: U;
modelName: T;
argsType: ARGS;
engineMiddleware: EngineMiddlewareInput;
}): any => {
@Resolver({ isAbstract: true })
abstract class BaseQueryResolver {
/*
*/
/*
*/
@Query(() => init.schemaName, { name: `${init.domain}GetOne` })
@EngineMiddleware(init.engineMiddleware)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new init.modelName() as EngineModel<any, any, any>;
const result = await model.getOneGeneric(id, ctx);
return result;
}
@Query(() => [init.schemaName], { name: `${init.domain}GetMany` })
@EngineMiddleware(init.engineMiddleware)
async getMany(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new init.modelName() as EngineModel<any, any, any>;
return model.getManyGenericWithArgs(args, ctx);
}
@Query(() => Number, { name: `${init.domain}GetCount` })
@EngineMiddleware(init.engineMiddleware)
async getCount(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<number> {
const model = new init.modelName() as EngineModel<any, any, any>;
return model.getCountGeneric(args, ctx);
}
}
return BaseQueryResolver;
};
export const createEngineMutationResolver = <
U extends ClassType<any>,
T extends ClassType<any>,
NEW extends ClassType<any>,
EDIT extends ClassType<any>
>(init: {
domain: string;
schemaName: U;
modelName: T;
newInput: NEW;
editInput: EDIT;
engineMiddleware: EngineMiddlewareInput;
validate?: boolean;
}): any => {
@Resolver({ isAbstract: true })
abstract class BaseMutationResolver {
/*
*/
@Mutation(() => init.schemaName, { name: `${init.domain}AddOne` })
@EngineMiddleware({ ...init.engineMiddleware, validations: [{ schema: init.newInput }] })
async addOne(@Arg('input', () => init.newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise<U> {
const model = new init.modelName() as EngineModel<any, any, any>;
return await await model.saveOne({ newData: input, ctx });
}
@Mutation(() => init.schemaName, { name: `${init.domain}EditOne` })
@EngineMiddleware({ ...init.engineMiddleware, validations: [{ schema: init.editInput }] })
async editOne(@Arg('id') id: string, @Arg('input', () => init.editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise<U> {
const model = new init.modelName() as EngineModel<any, any, any>;
return await await model.updateOne({
query: { _id: id },
newData: input,
ctx: ctx,
});
}
@Mutation(() => init.schemaName, { name: `${init.domain}DeleteOne` })
@EngineMiddleware({ ...init.engineMiddleware })
async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<U> {
const model = new init.modelName() as EngineModel<any, any, any>;
return await await model.deleteOne({
query: { _id: id },
ctx: ctx,
});
}
}
return BaseMutationResolver;
};

View File

@ -0,0 +1,76 @@
import { GetArgs } from '@seed/graphql/Request';
import { ApolloContext } from '@seed/interfaces/context';
import { FilterQuery, UpdateQuery } from 'mongodb';
import { EngineModel } from '../EngineModel';
import { IEngineSchema } from '../EngineSchema';
export interface GetQueryInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface GetManyInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
pagination?: GetArgs;
ctx?: ApolloContext | null;
}
export interface GetAllInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface GetCountInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface GetOneInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface SaveOneInput<SchemaDBInterface> {
newData?: SchemaDBInterface & Partial<IEngineSchema>;
additionnalData?: Partial<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
upsert?: boolean;
}
export interface SaveManyInput<SchemaDBInterface> {
models: EngineModel<SchemaDBInterface, any, any>[];
ctx?: ApolloContext | null;
}
export interface BulkInput<SchemaDBInterface, SchemaDB, Schema> {
inserts?: EngineModel<SchemaDBInterface, SchemaDB, Schema>[];
updates?: {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
newData?: Partial<SchemaDBInterface & IEngineSchema>;
updateRequest?: UpdateQuery<SchemaDBInterface & IEngineSchema>;
}[];
ctx?: ApolloContext | null;
}
export interface UpdateOneInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
newData: Partial<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface UpdateOneCustomInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
updateRequest: UpdateQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}
export interface UpdateOneSubField<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
fieldName: keyof SchemaDBInterface;
subPath?: string;
fieldValue: any;
ctx?: ApolloContext | null;
}
export interface DeleteOneInput<SchemaDBInterface> {
query: FilterQuery<SchemaDBInterface & IEngineSchema>;
ctx?: ApolloContext | null;
}

View File

@ -0,0 +1,721 @@
import { parsePaginationOptions } from '@lib/seed/helpers/Request';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { validateOrReject } from 'class-validator';
import { EngineAccessService } from '../EngineAccessService';
import { EngineModel } from '../EngineModel';
import { IEngineSchema } from '../EngineSchema';
import { v4 as uuid } from 'uuid';
import {
BulkInput,
DeleteOneInput,
GetAllInput,
GetCountInput,
GetManyInput,
GetOneInput,
GetQueryInput,
SaveManyInput,
SaveOneInput,
UpdateOneCustomInput,
UpdateOneInput,
UpdateOneSubField,
} from './__interface';
import DB from '@lib/seed/services/database/DBService';
import { createChangeStream } from './streams/stream.service';
import { StreamOperationType } from './streams/schemas/stream.components';
import { BulkWriteOperation, ObjectId } from 'mongodb';
import { newError } from '@seed/helpers/Error';
import { clog } from '@seed/helpers/Utils';
export function getQuery<SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetQueryInput<SchemaDBInterface>,
): any {
let finalQuery: any;
const { query, ctx } = input;
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
// We add the permission (on r field) to make sure that kind of user has access to the ressource
if (!ctx.ctx.noPermissionCheck) finalQuery = EngineAccessService.addPermissionToQuery(account, 'get', { ...query });
// We check if it comes from an organisation
// Check of the user's correct authorisation on that organisation comes from the middleware
if (!ctx.ctx.noOrganisationCheck) {
if (organisationId) {
if (model.collectionName == 'accounts') finalQuery['organisationIds'] = organisationId;
else finalQuery['organisationId'] = { $eq: organisationId };
} else {
// If admin & public query -> do not add model condition and let search every records)
if (account && account.types && !account.types.includes(AccountTypeEnum.public) && !account.types.includes(AccountTypeEnum.admin)) {
finalQuery['organisationId'] = { $exists: false };
}
}
}
} else {
finalQuery = { ...query };
}
return finalQuery;
}
export const getMany = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetManyInput<SchemaDBInterface>,
): Promise<Schema[]> => {
let { pagination } = input;
const finalQuery = getQuery(model, input);
if (!pagination) pagination = { limit: 100, skip: 0 };
if (!pagination.sort) pagination.sort = model.defaultSort;
const paginationOption = parsePaginationOptions(pagination);
try {
console.log('search request', JSON.stringify(finalQuery, null, 2));
const dbResults = await (await model.db()).find(finalQuery, paginationOption).toArray();
return model.plainToClass(dbResults) as any;
} catch (error) {
return [];
}
};
export const getCount = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetCountInput<SchemaDBInterface>,
): Promise<number> => {
const finalQuery = getQuery(model, input);
try {
const result = (await (await model.db()).countDocuments(finalQuery)) as any;
return result;
} catch (error) {
return 0;
}
};
export const getAll = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetAllInput<SchemaDBInterface>,
): Promise<any> => {
const finalQuery = getQuery(model, input);
try {
clog(finalQuery);
const res = await (await model.db()).find(finalQuery).toArray();
return model.plainToClass(res);
} catch (error) {
return [];
}
};
export const getOne = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: GetOneInput<SchemaDBInterface>,
): Promise<Schema> => {
const { query, ctx } = input;
let dataToReturn;
if (query._id) {
const _id = query._id as any;
if (_id.match(/^[0-9a-fA-F]{24}$/)) {
query._id = new ObjectId(_id) as any;
}
}
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
const doc = await (await model.db()).findOne(query);
if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName });
dataToReturn = model.plainToClass(doc) as any;
model.set(dataToReturn);
if (!ctx.ctx.noOrganisationCheck) {
if (organisationId) {
if (model.collectionName != 'accounts') EngineAccessService.checkOrganisationPermissions(model, organisationId);
}
}
if (!ctx.ctx.noPermissionCheck) EngineAccessService.checkPermissions(dataToReturn, account, 'r');
} else {
const doc = await (await model.db()).findOne(query);
if (!doc) throw await newError(4041, { id: query._id, model: model.collectionName });
dataToReturn = model.plainToClass(doc) as any;
model.set(dataToReturn);
}
return model.get();
};
export const saveOne = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: SaveOneInput<SchemaDBInterface>,
): Promise<Schema> => {
const { newData, additionnalData, upsert } = input;
let ctx = input.ctx;
if (newData) model.set(newData);
if (additionnalData) model.set(additionnalData);
// Set the path
model.set({ paths: model.getPaths() });
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
EngineAccessService.addPermissions(model, ['r', 'w', 'd'], [account._id]);
// TODO : Check create permissions
if (organisationId) {
model.set({ organisationId });
}
if (model.by) model.by.createdBy = account._id;
else {
model.by = {
createdBy: account._id,
};
}
}
// if (!ctx) ctx = await createApolloContext();
try {
if (model.beforeCreate) await model.beforeCreate(ctx);
await validateOrReject(model.dbData as any);
} catch (error) {
throw error;
}
const dataToSave = model.get() as any;
if (model.searchText) dataToSave.searchT = model.searchText();
if (!model._id) model._id = uuid();
delete dataToSave._id;
try {
const result = await (await model.db()).findOneAndUpdate(
{ _id: model._id } as any,
{
$set: dataToSave,
$setOnInsert: {
_id: model._id,
},
} as any,
{ upsert: upsert ? upsert : true, returnOriginal: false },
);
if (result.ok == 0) throw await newError(1000, result);
await createChangeStream(model, StreamOperationType.insert);
return model.get();
} catch (error) {
throw await newError(1000, error);
}
};
export const updateOne = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneInput<SchemaDBInterface>,
): Promise<Schema> => {
try {
const { query, newData, ctx } = input;
if (!model._id) await model.getOne(input);
if (newData) model.set(newData);
model.set({ paths: model.getPaths() });
if (ctx) {
const account = ctx.ctx.user;
// Check if doc exists & correct permissions
if (model.by) model.by.updatedBy = account._id;
else {
model.by = {
updatedBy: account._id,
};
}
EngineAccessService.checkPermissions(model, account, 'w');
}
if (model.beforeUpdate) await model.beforeUpdate(ctx);
const dataToSave = model.get() as any;
if (model.searchText) dataToSave.searchT = model.searchText();
const result = await (await model.db()).findOneAndUpdate(query, { $set: dataToSave }, { upsert: false, returnOriginal: false });
if (result.ok != 1) throw await newError(1000, result);
model.set(result.value);
await createChangeStream(model, StreamOperationType.update);
return model.get();
} catch (error) {
throw error;
}
};
export const updateOneCustom = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneCustomInput<SchemaDBInterface>,
): Promise<Schema> => {
try {
const { query, updateRequest, ctx } = input;
if (!model._id) await model.getOne(input);
if (ctx) {
const account = ctx.ctx.user;
// Check if doc exists
// dataBeforeChange = model.get();
EngineAccessService.checkPermissions(model, account, 'w');
if (model.by) model.by.updatedBy = account._id;
else {
model.by = {
updatedBy: account._id,
};
}
// Validate new data that will go into the DB
// const validation = await model.validate();
// if (!validation.success) return validation;
}
// Prepare data
model.updatedAt = new Date();
const result = await (await model.db()).findOneAndUpdate(query, updateRequest, { upsert: false, returnOriginal: false });
if (result.ok != 1) throw await newError(1000, result);
if (result.value) model.set(result.value);
await createChangeStream(model, StreamOperationType.update);
return model.get();
} catch (error) {
throw error;
}
};
export const deleteOne = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: DeleteOneInput<SchemaDBInterface>,
): Promise<Schema> => {
const { query, ctx } = input;
try {
if (!model._id) await model.getOne(input);
if (model.beforeDelete) await model.beforeDelete(ctx);
if (ctx) {
const account = ctx.ctx.user;
// Check if doc exists & correct permissions
EngineAccessService.checkPermissions(model, account, 'd');
if (model.by) model.by.deletedBy = account._id;
else {
model.by = {
deletedBy: account._id,
};
}
}
const result = await (await model.db()).findOneAndDelete(query);
if (result.ok != 1) throw await newError(1001);
const deletedModel = '_deleted_' + model.collectionName;
await (await DB.getInstance()).db.collection(deletedModel).insertOne(model.get());
await createChangeStream(model, StreamOperationType.delete);
return model.get();
} catch (error) {
throw error;
}
};
export const saveMany = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: SaveManyInput<SchemaDBInterface>,
): Promise<Schema[]> => {
const ctx = input.ctx;
const models = input.models;
const dataToSaves: any[] = [];
for (let index = 0; index < models.length; index++) {
const element = models[index];
// Set the path
element.set({ paths: element.getPaths() });
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
EngineAccessService.addPermissions(element, ['r', 'w', 'd'], [account._id]);
// TODO : Check create permissions
if (organisationId) {
element.set({ organisationId });
}
if (element.by) element.by.createdBy = account._id;
else {
element.by = {
createdBy: account._id,
};
}
try {
if (element.beforeCreate) await element.beforeCreate(ctx);
await validateOrReject(element.dbData);
} catch (error) {
throw error;
}
}
const dataToSave = element.get() as any;
if (element.searchText) dataToSave.searchT = element.searchText();
if (!element._id) dataToSave._id = uuid();
dataToSaves.push(dataToSave);
}
try {
const result = await (await model.db()).insertMany(dataToSaves, { ordered: true });
if (result.result.ok == 0) throw await newError(1000, result);
// await createChangeStream(model, StreamOperationType.insert);
return dataToSaves;
} catch (error) {
throw await newError(1000, error);
}
};
export const bulk = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: BulkInput<SchemaDBInterface, SchemaDB, Schema>,
): Promise<void> => {
const { inserts, updates, ctx } = input;
const bulkOps: BulkWriteOperation<SchemaDBInterface & IEngineSchema>[] = [];
if (inserts) {
inserts.forEach((element) => {
bulkOps.push({
insertOne: {
document: element.get() as any,
},
});
});
}
if (updates) {
updates.forEach((element) => {
if (element.newData)
bulkOps.push({
updateOne: {
filter: element.query,
update: { $set: element.newData },
},
});
else if (element.updateRequest)
bulkOps.push({
updateOne: {
filter: element.query,
update: element.updateRequest,
},
});
});
}
const bOpsArray: BulkWriteOperation<SchemaDBInterface & IEngineSchema>[][] = [];
if (bulkOps.length > 0) {
const chunk = 1000;
let i, j;
for (i = 0, j = bulkOps.length; i < j; i += chunk) {
bOpsArray.push(bulkOps.slice(i, i + chunk));
}
} else bOpsArray.push(bulkOps);
try {
for (let index = 0; index < bOpsArray.length; index++) {
const element = bOpsArray[index];
const res = await (await model.db()).bulkWrite(element);
console.log(res);
}
} catch (error) {
throw await newError(1000, error);
}
return;
};
export const updateOneSubField = async <SchemaDBInterface, SchemaDB, Schema>(
model: EngineModel<SchemaDBInterface, SchemaDB, Schema>,
input: UpdateOneSubField<SchemaDBInterface>,
): Promise<any> => {
const { fieldName, fieldValue, subPath, query, ctx } = input;
let updatePath = fieldName as string;
if (subPath) updatePath += subPath;
try {
const filterQ = {
...query,
};
return await model.updateOneCustom({
query: filterQ,
updateRequest: {
$set: {
[`${updatePath}`]: fieldValue,
} as any,
},
ctx,
});
} catch (error) {
throw error;
}
};
// public async saveMany(newDataArr: Partial<this>[], ctx: ApolloContext | null): Promise<any[]> {
// // Initialize the change stream
// const changeStreams: any[] = [];
// const afterCreatePromises: any[] = [];
// const prehooksPromises = newDataArr.map(async (newData) => {
// const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
// if (ctx) {
// const account = ctx.ctx.user;
// const organisationId = ctx.ctx.organisationId;
// addPermissions(clone, ['r', 'w', 'd'], [account._id]);
// if (organisationId) {
// newData.organisationId = organisationId;
// }
// newData.createdBy = account._id;
// }
// clone.set(newData);
// if (!ctx) ctx = await createApolloContext();
// if (clone.prehook) await clone.prehook();
// if (clone.beforeCreate) await clone.beforeCreate(ctx);
// const { _id, ...dataToSave } = clone.get();
// const savedId = _id ? _id : uuid();
// clone.set({ _id: savedId });
// const changeStreamId = uuid();
// const changeStreamData = {
// _id: changeStreamId,
// operation: StreamOperationType.insert,
// collection: clone.collectionName,
// documentKey: clone._id,
// insertedValues: clone.get(),
// hookStatus: PostHookStatus.new,
// createdAt: new Date(),
// updateAt: new Date(),
// };
// changeStreams.push(changeStreamData);
// // eslint-disable-next-line @typescript-eslint/no-this-alias
// afterCreatePromises.push(async () => {
// if (clone.afterCreate) {
// const res = await asyncHook(changeStreamData);
// if (res) {
// await clone.afterCreate(changeStreamData);
// await (await DB.getInstance()).db
// .collection('stream.changes')
// .updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
// }
// }
// });
// return clone.get();
// });
// try {
// const dataArr = await Promise.all(prehooksPromises);
// await (await this.db()).insertMany(dataArr, { ordered: false });
// await (await DB.getInstance()).db.collection('stream.changes').insertMany(changeStreams, { ordered: false });
// await promiseAll(afterCreatePromises);
// return dataArr;
// } catch (error) {
// console.error(error);
// throw await newError('db.error', error);
// }
// }
// ██████╗ ███████╗██████╗ ███╗ ███╗██╗███████╗███████╗██╗ ██████╗ ███╗ ██╗███████╗
// ██╔══██╗██╔════╝██╔══██╗████╗ ████║██║██╔════╝██╔════╝██║██╔═══██╗████╗ ██║██╔════╝
// ██████╔╝█████╗ ██████╔╝██╔████╔██║██║███████╗███████╗██║██║ ██║██╔██╗ ██║███████╗
// ██╔═══╝ ██╔══╝ ██╔══██╗██║╚██╔╝██║██║╚════██║╚════██║██║██║ ██║██║╚██╗██║╚════██║
// ██║ ███████╗██║ ██║██║ ╚═╝ ██║██║███████║███████║██║╚██████╔╝██║ ╚████║███████║
// ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚═╝╚══════╝╚══════╝╚═╝ ╚═════╝ ╚═╝ ╚═══╝╚══════╝
// public async updateQueryWithPermission(query: FilterQuery<this>, ctx: ApolloContext): Promise<FilterQuery<this>> {
// const account = ctx.ctx.user;
// const organisationId = ctx.ctx.organisationId;
// // We add the permission (on r field) to make sure that kind of user has access to the ressource
// const finalQuery = addPermissionToQuery(account, 'get', { ...query });
// // We check if it comes from an organisation
// // Check of the user's correct authorisation on that organisation comes from the middleware
// if (organisationId) {
// finalQuery['organisationId'] = { $eq: organisationId };
// } else {
// finalQuery['organisationId'] = { $exists: false };
// }
// return finalQuery;
// }
// ███████╗██╗ ██╗██████╗ ██████╗ ███████╗███████╗███████╗
// ██╔════╝██║ ██║██╔══██╗ ██╔══██╗██╔════╝██╔════╝██╔════╝
// ███████╗██║ ██║██████╔╝█████╗██████╔╝█████╗ ███████╗███████╗
// ╚════██║██║ ██║██╔══██╗╚════╝██╔══██╗██╔══╝ ╚════██║╚════██║
// ███████║╚██████╔╝██████╔╝ ██║ ██║███████╗███████║███████║
// ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝╚══════╝╚══════╝
// public async addOneSubModel<SUB>(
// parentQ: { _id: string },
// subModelRefField: string,
// subModelFieldName: keyof this,
// subModel: SUB,
// subModelInput: any,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// try {
// subModelInput[subModelRefField] = parentQ._id;
// const subM = await subModel.saveOne(subModelInput, ctx);
// await this.updateOneCustom(parentQ as any, { $push: { [`${subModelFieldName}`]: subM._id } } as any, null);
// return subM;
// } catch (error) {
// throw error;
// }
// }
// public async editOneSubModel<SUB extends BaseGraphModel>(
// subModel: SUB,
// subModelId: string,
// subModelInput: any,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// try {
// // No need to check if belongs to the correct organisation, the middleware does that
// const subM = await subModel.updateOne({ _id: subModelId } as any, subModelInput, ctx);
// return subM;
// } catch (error) {
// throw error;
// }
// }
// public async deleteOneSubModel<SUB extends BaseGraphModel>(
// parentQ: FilterQuery<this>,
// subModelFieldName: keyof this,
// subModel: SUB,
// subModelId: string,
// ctx: ApolloContext | null,
// ): Promise<SUB> {
// try {
// const subM = await subModel.deleteOne({ _id: subModelId } as any, ctx);
// await this.updateOneCustom(parentQ, { $pull: { [`${subModelFieldName}`]: subM._id } } as any, null);
// return subM;
// } catch (error) {
// throw error;
// }
// }
// public getOneSubRessource(subRessoureFieldName: keyof this | string, subRessoureId: string): any | undefined {
// try {
// const subress = _.get(this, subRessoureFieldName);
// if (subress) {
// const ind = _.findIndex(subress, { _id: subRessoureId });
// if (ind !== -1) return subress[ind];
// }
// return;
// } catch (error) {
// throw error;
// }
// }
// public getDefaultSubRessource(subRessoureFieldName: keyof this | string): any | undefined {
// try {
// const subress = _.get(this, subRessoureFieldName);
// if (subress) {
// const ind = _.findIndex(subress, { default: true });
// if (ind !== -1) return subress[ind];
// return subress[0];
// }
// return;
// } catch (error) {
// throw error;
// }
// }
// public async addOneSubRessource(
// parentQ: { _id: string },
// subRessoureFieldName: keyof this | string,
// subRessoure: any,
// ctx: ApolloContext | null,
// ): Promise<any> {
// try {
// if (subRessoure.default && _.get(this, subRessoureFieldName)) {
// // Remove all default from others
// await this.updateOneCustom(
// { _id: this._id } as any,
// {
// $set: { [`${subRessoureFieldName}.$[].default`]: false } as any,
// },
// ctx,
// );
// } else subRessoure.default = false;
// // Generate ID if not in the model
// if (!subRessoure._id) subRessoure._id = uuid();
// await this.updateOneCustom(parentQ as any, { $push: { [`${subRessoureFieldName}`]: subRessoure } } as any, ctx);
// return subRessoure;
// } catch (error) {
// throw error;
// }
// }
// public async deleteOneSubRessource(
// parentQ: { _id: string },
// subRessoureFieldName: keyof this | string,
// subRessoureId: string,
// ctx: ApolloContext | null,
// ): Promise<string> {
// try {
// if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw await newError('notFound', '400');
// const filterQ = {
// ...parentQ,
// [`${subRessoureFieldName}._id`]: subRessoureId,
// };
// await this.updateOneCustom(filterQ as any, { $pull: { [`${subRessoureFieldName}`]: { _id: subRessoureId } } } as any, ctx);
// return subRessoureId;
// } catch (error) {
// throw error;
// }
// }

View File

@ -0,0 +1,118 @@
import { GetArgs } from '@lib/seed/graphql/Request';
import { ApolloContext } from '@lib/seed/interfaces/context';
import { baseSearchFunction } from '@lib/seed/services/database/DBRequestService';
import { EngineModel } from '../EngineModel';
/*
*/
export const getOneGeneric = async (model: any, id: string, ctx: ApolloContext): Promise<any> => {
try {
return await (model as EngineModel<any, any, any>).getOne({ query: { _id: id }, ctx });
} catch (error) {
throw error;
}
};
export const getManyGeneric = async (model: any, query: any, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise<any> => {
return baseSearchFunction({
model: model,
query: query,
pagination: pagination,
engine: true,
ctx: ctx,
});
};
export const getCountGeneric = async (model: any, baseArguments: any, ctx: ApolloContext): Promise<any> => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { pagination, ...args } = baseArguments;
const result = await baseSearchFunction({
model: model,
query: args,
count: true,
engine: true,
ctx: ctx,
});
return result;
};
export const getManyGenericWithArgs = async (model: any, baseArguments: any, ctx: ApolloContext | null): Promise<any> => {
try {
const { pagination, ...args } = baseArguments;
return baseSearchFunction({
model: model,
query: args,
pagination: pagination,
engine: true,
ctx: ctx,
});
} catch (error) {
throw error;
}
};
// export const editOneGeneric = async <T>(model: T, id: string, body: Partial<T>, ctx: ApolloContext): Promise<T> => {
// try {
// await model as EngineModel<any,any,any>).updateOne({ _id: id } as any, body, ctx);
// return model as EngineModel<any,any,any>).get();
// } catch (error) {
// throw error;
// }
// };
// export const deleteOneGeneric = async <T>(model: T, id: string, ctx: ApolloContext): Promise<T> => {
// try {
// await model as EngineModel<any,any,any>).deleteOne({ _id: id } as any, ctx);
// return model as EngineModel<any,any,any>).get();
// } catch (error) {
// throw error;
// }
// };
// export const permissionsAddOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise<any> => {
// try {
// await model as EngineModel<any,any,any>).updateOneCustom({ _id: id }, { $addToSet: { [`${input.permissionType}`]: input.permission } }, ctx);
// return model as EngineModel<any,any,any>).get();
// } catch (error) {
// throw error;
// }
// };
// export const permissionsRemoveOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise<any> => {
// try {
// await model as EngineModel<any,any,any>).updateOneCustom({ _id: id }, { $pull: { [`${input.permissionType}`]: input.permission } }, ctx);
// return model as EngineModel<any,any,any>).get();
// } catch (error) {
// throw error;
// }
// };
// export const publishOneGeneric = async <T>(model: T, id: string, ctx: ApolloContext): Promise<any> => {
// try {
// const missingInputs: string[] = [];
// // TODO THE CHECK OF THE INPUTS
// if (missingInputs.length > 0) throw newError('missingInput', '400', missingInputs);
// return editOneGeneric(model, id, { published: true, publicationDate: new Date() } as any, ctx);
// } catch (error) {
// throw error;
// }
// };
// export const unpublishOneGeneric = async <T>(model: T, id: string, ctx: ApolloContext): Promise<any> => {
// try {
// return editOneGeneric(model, id, { published: false } as any, ctx);
// } catch (error) {
// throw error;
// }
// };

View File

@ -0,0 +1,41 @@
import { registerEnumType } from 'type-graphql';
/*
*/
export enum StreamOperationType {
insert = 'insert',
delete = 'delete',
replace = 'replace',
update = 'update',
hook = 'hook',
}
registerEnumType(StreamOperationType, {
name: 'StreamOperationType',
});
export enum PostHookStatus {
new = 'new',
inProcess = 'inProcess',
completed = 'completed',
error = 'error',
noAction = 'noAction',
}
registerEnumType(PostHookStatus, {
name: 'PostHookStatus',
});
/*
*/

View File

@ -0,0 +1,11 @@
import { GetManyArgs } from '@seed/graphql/Request';
import { Field, ArgsType } from 'type-graphql';
@ArgsType()
export class StreamArgs extends GetManyArgs {
@Field({ nullable: true })
documentKey?: string;
@Field({ nullable: true })
hookStatus?: string;
}

View File

@ -0,0 +1,50 @@
import { AsyncHooksService } from '@lib/__hooks';
import { IEngineSchema, MetaBy, MetaPermissions } from '@seed/engine/EngineSchema';
import { StreamOperationType, PostHookStatus } from '@seed/services/change-stream/change-stream.components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import GraphQLJSON from 'graphql-type-json';
import { InputType, ObjectType, Field } from 'type-graphql';
@ObjectType()
@InputType()
export class StreamBaseSchema {
@Field(() => StreamOperationType)
operation: StreamOperationType;
@Field(() => String)
collection: ModelCollectionEnum | keyof AsyncHooksService | string;
@Field({ nullable: true })
documentKey?: string;
@Field(() => PostHookStatus)
hookStatus: PostHookStatus;
@Field({ nullable: true })
logMessage?: string;
@Field(() => GraphQLJSON, { nullable: true })
insertedValues?: any;
@Field(() => GraphQLJSON, { nullable: true })
updatedValues?: any;
@Field(() => GraphQLJSON, { nullable: true })
beforeUpdateValues?: any;
logInfo?: any;
}
@ObjectType()
export class StreamDBInterfaceSchema extends StreamBaseSchema {}
@ObjectType()
export class StreamDBSchema extends StreamDBInterfaceSchema {}
@ObjectType({ implements: IEngineSchema })
export class StreamSchema extends StreamDBSchema implements IEngineSchema {
_id: string;
organisationId?: string | undefined;
by?: MetaBy | undefined;
permissions: MetaPermissions;
createdAt: Date;
updatedAt: Date;
}

View File

@ -0,0 +1,49 @@
import { ObjectType } from 'type-graphql';
import { Permission } from '@seed/interfaces/permission';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { StreamDBInterfaceSchema, StreamDBSchema, StreamSchema } from './schemas/stream.schema';
import { EngineModel } from '@seed/engine/EngineModel';
import { IEngineSchema } from '@seed/engine/EngineSchema';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { plainToClass } from 'class-transformer';
import { CustomSearchEngine } from '@seed/services/database/DBRequestService';
import { StreamArgs } from './schemas/stream.schema.input';
const permissions: Permission = {
c: [AccountTypeEnum.admin],
r: [AccountTypeEnum.admin],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
};
@ObjectType()
export default class StreamEngineModel extends EngineModel<StreamDBInterfaceSchema, StreamDBSchema, StreamSchema> {
public constructor(input?: StreamDBInterfaceSchema & Partial<IEngineSchema>) {
const dataInit = plainToClass(StreamDBSchema, input || {});
super({
// ...init,
collectionName: ModelCollectionEnum.streams,
permissions: permissions,
dataInit,
});
}
plainToClass(plain: any): StreamSchema | StreamSchema[] {
return plainToClass(StreamSchema, plain);
}
searchEngine(): CustomSearchEngine<StreamArgs> {
const sEngine: CustomSearchEngine<StreamArgs> = {
documentKey: {
operation: '$eq',
},
hookStatus: {
operation: '$eq',
},
};
return sEngine;
}
}

View File

@ -0,0 +1,84 @@
import { AsyncHooksService } from '@lib/__hooks';
import { EngineModel } from '@seed/engine/EngineModel';
import { IEngineSchema } from '@seed/engine/EngineSchema';
import DB from '@seed/services/database/DBService';
import { asyncHook } from '@seed/services/hooks/hooks.decorator';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { v4 as uuid } from 'uuid';
import { StreamOperationType, PostHookStatus } from './schemas/stream.components';
import { StreamDBInterfaceSchema } from './schemas/stream.schema';
export const createChangeStream = async <T extends EngineModel<any, any, any>>(model: T, operation: StreamOperationType): Promise<void> => {
let operationFunction;
const changeStreamId = uuid();
const base = {
_id: changeStreamId,
operation,
collection: model.collectionName,
documentKey: model._id,
hookStatus: PostHookStatus.new,
createdAt: new Date(),
updatedAt: new Date(),
};
switch (operation) {
case StreamOperationType.insert:
(base as any).insertedValues = model.get();
operationFunction = 'afterCreate';
break;
case StreamOperationType.update:
case StreamOperationType.replace:
(base as any).updatedValues = model.get();
operationFunction = 'afterUpdate';
break;
case StreamOperationType.delete:
(base as any).updatedValues = model.get();
operationFunction = 'afterDelete';
break;
default:
break;
}
const dataToSave: StreamDBInterfaceSchema & IEngineSchema = {
...base,
permissions: {
r: [AccountTypeEnum.admin],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
},
};
await (await DB.getInstance()).db.collection(ModelCollectionEnum['streams']).insertOne(dataToSave);
if (operationFunction && model[operationFunction] && (await asyncHook(dataToSave))) {
await model[operationFunction]();
await (await DB.getInstance()).db
.collection(ModelCollectionEnum['streams'])
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
};
export const createAsyncStream = async <T extends EngineModel<any, any, any>>(collection: keyof AsyncHooksService, data: any): Promise<void> => {
const changeStreamId = uuid();
const base = {
_id: changeStreamId,
operation: StreamOperationType.hook,
collection,
insertedValues: data,
hookStatus: PostHookStatus.new,
createdAt: new Date(),
updatedAt: new Date(),
};
const dataToSave: StreamDBInterfaceSchema & IEngineSchema = {
...base,
permissions: {
r: [AccountTypeEnum.admin],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
},
};
await (await DB.getInstance()).db.collection(ModelCollectionEnum['streams']).insertOne(dataToSave);
};

View File

@ -0,0 +1,9 @@
import { registerEnumType } from 'type-graphql';
export enum ModelCollectionEnum {
'streams' = 'streams',
accounts = 'accounts',
}
registerEnumType(ModelCollectionEnum, {
name: 'ModelLoadersEnum',
});

View File

@ -0,0 +1,37 @@
app: ${file(./package.json):name}
service: cronjob
package:
individually: false
provider:
name: aws
stage: ${opt:stage,'dev'}
runtime: nodejs12.x
environment:
AWS_compute: ${self:service}-${self:provider.stage}-compute
iamRoleStatements:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource: '*'
plugins:
- serverless-webpack
custom:
stage: ${opt:stage, self:provider.stage}
webpack:
webpackConfig: './lib/seed/webpack.config.js' # Name of webpack configuration file
includeModules:
forceExclude:
- aws-sdk
- puppeteer
functions:
treatRemainingDaysCron:
handler: lib/__cronjobs/handler.cron1
timeout: 300 # optional, in seconds, default is 6
events:
- schedule: rate(1 day)
jobNotificationsCron:
handler: lib/__cronjobs/handler.cron2
timeout: 300
events:
- schedule: rate(1 day)

View File

@ -0,0 +1,8 @@
import { ApolloContext } from '@seed/interfaces/context';
import { promiseAll, sleep } from '@seed/helpers/Utils';
export const afterCheckoutEvent = async (data: any, ctx: ApolloContext): Promise<void> => {
const promises: Promise<any>[] = [];
await promiseAll(promises);
};

View File

@ -0,0 +1,35 @@
import { asyncHookDecorator } from '@seed/services/hooks/hooks.decorator';
import { args } from 'chrome-aws-lambda';
import { afterCheckoutEvent } from './functions/example.hooks';
import { afterOrderCancel, afterOrdersMarkOneAsPaid, afterOrdersMarkOneAsUnPaid } from './functions/orders.hooks';
@asyncHookDecorator()
export class AsyncHooksService {
/* [MODULES - SHOP] */
public async afterCheckout(...args: any): Promise<any> {
console.log('[NO ACTION] - afterCheckout');
}
public async afterOrderCancel(...args: Parameters<typeof afterOrderCancel>): Promise<any> {
return afterOrderCancel(...args);
}
public async afterOrdersMarkOneAsPaid(...args: Parameters<typeof afterOrderCancel>): Promise<any> {
return afterOrdersMarkOneAsPaid(...args);
}
public async afterOrdersMarkOneAsUnPaid(...args: Parameters<typeof afterOrderCancel>): Promise<any> {
return afterOrdersMarkOneAsUnPaid(...args);
}
public async afterOrdersReimbursed(...args: Parameters<typeof afterOrderCancel>): Promise<any> {
console.log('[NO ACTION] - afterOrdersReimbursed');
}
/* [MODULES - EVENTS] */
public async afterCheckoutEvent(...args: Parameters<typeof afterCheckoutEvent>): Promise<any> {
return afterCheckoutEvent(...args);
}
}

View File

@ -0,0 +1,11 @@
import 'reflect-metadata';
import serverless from 'serverless-http';
import express from 'express';
const app = express();
app.get('/', function(req, res) {
res.send('Hello World!');
});
module.exports.handler = serverless(app);

View File

@ -0,0 +1,34 @@
import ErrorSettingsModel from '@services/module-cms/functions/error-settings/errors-settings.model';
/*
*/
/*
*/
export enum NotificationEnum {
'notification' = 'notification',
}
export const ErrorsConfig: {
[key: string]: {
message: string;
translations?: ErrorSettingsModel;
};
} = {
code: {
message: 'message',
},
};

View File

@ -0,0 +1,17 @@
## Instal ##
## run : node ./lib/seed/devops/deploy-domains.js
## Carefull - Must be us-east-1 for certificate (created beforehand)
domains:
production: XXXX.com
staging: staging-XXXX.com
dev: dev-XXXX.com
customDomain:
basePath: '${self:service}'
domainName: ${self:custom.domains.${self:custom.stage}}
stage: '${self:custom.stage}'
createRoute53Record: true
certificateName: XXXX
certificateArn: arn:aws:acm:us-east-1:555646219416:certificate/07e83105-1554-4e3d-b968-0a34d77911ad
endpointType: 'edge'
securityPolicy: tls_1_2

View File

@ -0,0 +1,20 @@
{
"_id" : "23wzbPpt4OWXtqs92dAaNvrue9v1",
"d" : [
"admin"
],
"email" : "admin@xxxx",
"r" : [
"admin"
],
"types" : [
"admin"
],
"w" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"firstName" : "Admin",
"lastName" : "Project",
}

View File

@ -0,0 +1,170 @@
{
"_id" : "b92eebfa-2bd9-4976-9a6d-2fba6bad14fb",
"key" : "FIREBASE_project_id",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "f6f8f6d6-25a4-4ce7-a00b-8b0a7188a5cf",
"key" : "FIREBASE_private_key_id",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "00698ef9-7dc3-4942-973d-ac5823ca6399",
"key" : "FIREBASE_private_key",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "829f390c-ca20-4dec-a679-4f2117fee710",
"key" : "FIREBASE_client_email",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "7f86b770-f6ce-4c99-a6ad-7eda6c3eac04",
"key" : "FIREBASE_client_id",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "262c6d6f-ff34-46b4-9639-f8baa75186ce",
"key" : "FIREBASE_auth_uri",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "b34ec29f-02c1-4699-ab08-83dd920bce7a",
"key" : "FIREBASE_token_uri",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "35409f42-f87f-47ae-8505-2328f98a3979",
"key" : "FIREBASE_auth_provider_x509_cert_url",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "fbb3f410-6717-49af-8004-75c9f110e7db",
"key" : "FIREBASE_client_x509_cert_url",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "34e7ad98-3c5b-4288-802e-57f6f67a2c9f",
"key" : "FIREBASE_apiKey",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},

View File

@ -0,0 +1,169 @@
{
"_id" : "28ade57d-697b-4cac-9e50-62cd4a8c8e61",
"key" : "FILE_allowedMime",
"value" :"image/jpeg,image/png,image/gif,application/pdf,image/svg+xml,application/xml,application/zip",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "6f106c8f-df69-403d-b92b-6bba314ce6ef",
"key" : "TEST_USER_EMAIL",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "cfc32155-cd1c-4d5e-bfa9-425ef27065ce",
"key" : "TEST_ADMIN_EMAIL",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "8c4c28fb-d89d-434c-a962-f4c43dbda68d",
"key" : "STRIPE_SKEY",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "dec94533-a457-4c66-9ead-4e754243ec89",
"key" : "STRIPE_PUBLIC_KEY",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "870d1674-367a-4937-9d66-d36e889d4953",
"key" : "STRIPE_CURRENCY",
"value" :"usd",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "5ed19459-1b4b-462b-88fb-5fbb2d698229",
"key" : "STRIPE_ORDERDESCRIPTION",
"value" :"You order from our webplatform",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "354b4491-497f-4e3f-9569-06863e19fadd",
"key" : "MJ_APIKEY_PUBLIC",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},
{
"_id" : "2e69bc94-28c9-400e-bd77-1714fb4f8300",
"key" : "MJ_APIKEY_PRIVATE",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
},{
"_id" : "5320e684-77af-4432-b229-60d63825a823",
"key" : "GOOGLE_MAP_apiKey",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"r" : [
"admin",
],
"w" : [
"admin",
],
"d" : [
"admin",
],
}

View File

@ -0,0 +1,219 @@
{
"_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb",
"key" : "resetPassword",
"templateId" : {
"en" : "",
"fr" : "",
"nl" : ""
},
"fromEmail" : "no-reply@makeit-studio.com",
"replyToEmail" : "software@makeit-studio.com",
"fromName" : "Make it | Studio",
"subject" : {
"en" : "You requested a new password",
"fr" : "Vous avez demandé un nouveau mot de passe",
"nl" : "You requested a new password"
},
"body" : {
"en" : "<p>Here is the password reset link : ${resetPasswordLink} </p>",
"fr" : "<p>Voici le lien pour réinitialiser votre mot de passe : ${resetPasswordLink}</p>",
"nl" : "<p>Here is the password reset link : ${resetPasswordLink} </p>"
},
"availableFields" : [
"firstName",
"lastName",
"email",
"resetPasswordLink"
],
"custom" : false,
"type" : "emails",
"cci" : [
],
"r" : [
"admin"
],
"w" : [
"admin"
],
"d" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
},
{
"_id" : "0f245c6c-e39a-465f-9374-e2268a0cbf83",
"key" : "adminAccountAddOne",
"templateId" : {
"en" : "",
"fr" : "",
"nl" : ""
},
"fromEmail" : "no-reply@makeit-studio.com",
"replyToEmail" : "software@makeit-studio.com",
"fromName" : "Make it | Studio",
"subject" : {
"en" : "The administrator of [PROJECT] created an account for you",
"fr" : "L'administrateur de [PROJECT] a crée un compte pour vous!",
"nl" : "The administrator of [PROJECT] created an account for you"
},
"body" : {
"en" : "<p>Please setup your password by clicking this link : ${resetPasswordLink} </p>",
"fr" : "<p>Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink} </p>",
"nl" : "<p>Please setup your password by clicking this link : ${resetPasswordLink} </p>"
},
"availableFields" : [
"firstName",
"lastName",
"email",
"resetPasswordLink"
],
"custom" : false,
"type" : "emails",
"cci" : [
],
"r" : [
"admin"
],
"w" : [
"admin"
],
"d" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
},
{
"_id" : "16496dfc-5935-495b-a352-52efe158d55c",
"key" : "organisationsAddOneAccount",
"templateId" : {
"en" : "",
"fr" : "",
"nl" : ""
},
"fromEmail" : "no-reply@makeit-studio.com",
"replyToEmail" : "software@makeit-studio.com",
"fromName" : "Make it | Studio",
"subject" : {
"en" : "The administrator of [ORG] created an account for you",
"fr" : "L'administrateur de l'organisation [ORG] a crée un compte pour vous!",
"nl" : "The administrator of [ORG] created an account for you"
},
"body" : {
"en" : "<p>Please setup your password by clicking this link : ${resetPasswordLink} </p>",
"fr" : "<p>Pouvez-vous configurer un mot de passe en cliquant sur ce lien ? : ${resetPasswordLink} </p>",
"nl" : "<p>Please setup your password by clicking this link : ${resetPasswordLink} </p>"
},
"availableFields" : [
"firstName",
"lastName",
"email",
"resetPasswordLink"
],
"custom" : false,
"type" : "emails",
"cci" : [
],
"r" : [
"admin"
],
"w" : [
"admin"
],
"d" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
},
{
"_id" : "16496dfc-5875-495b-axx2-52efe158d55c",
"key" : "emailCodeSignIn",
"templateId" : {
"en" : "",
"fr" : "",
"nl" : ""
},
"fromEmail" : "no-reply@makeit-studio.com",
"replyToEmail" : "software@makeit-studio.com",
"fromName" : "Make it | Studio",
"subject" : {
"en" : "Here is your code to login : ${code}",
"fr" : "Voici votre code d'accès: ${code}",
"nl" : "Here is your code to login : ${code}"
},
"body" : {
"en" : "Here is your code to login : ${code}",
"fr" : "Voici votre code d'accès: ${code}",
"nl" : "Here is your code to login : ${code}"
},
"availableFields" : [
"firstName",
"code"
],
"custom" : false,
"type" : "emails",
"cci" : [
],
"r" : [
"admin"
],
"w" : [
"admin"
],
"d" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
},
{
"_id" : "04e48fa2-714f-4d7f-ae17-c791ccf6e5bb",
"key" : "magicLink",
"templateId" : {
"en" : "",
"fr" : "",
"nl" : ""
},
"subject" : {
"en" : "Your magic link is ready",
"fr" : "Votre demande de lien d'accès",
"nl" : "Your magic link is ready"
},
"body" : {
"en" : "Here is your link to login : ${magicLink}",
"fr" : "Voici votre lien d'accès: ${magicLink}",
"nl" : "Here is your link to login : ${magicLink}"
},
"fromEmail" : "no-reply@makeit-studio.com",
"replyToEmail" : "software@makeit-studio.com",
"fromName" : "Make it | Studio",
"availableFields" : [
"firstName",
"lastName",
"email",
"magicLink"
],
"custom" : false,
"type" : "emails",
"cci" : [
],
"r" : [
"admin"
],
"w" : [
"admin"
],
"d" : [
"admin"
],
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
}

View File

@ -0,0 +1,51 @@
{
"_id" : "28ade57d-df69-4cac-9e50-62cd4a8c8e61",
"key" : "TWILIO_FROM_NUMBER",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "28ade57d-df69-403d-b92b-6bba314ce6ef",
"key" : "TWILIO_ACCOUNT_SID",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},
{
"_id" : "cfc32155-b92b-4d5e-bfa9-425ef27065ce",
"key" : "TWILIO_AUTH_TOKEN",
"value" :"",
"type" : "env",
"createdAt" : ISODate(),
"updatedAt" : ISODate(),
"d" : [
"admin",
],
"r" : [
"admin",
],
"w" : [
"admin",
]
},

View File

@ -0,0 +1,47 @@
mutation listsAddOne {
listsAddOne1: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_2.svg",fr:"combined-shape_2.svg",nl:"combined-shape_2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_2.svg"}}){_id}
listsAddOne2: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_3.svg",fr:"combined-shape_3.svg",nl:"combined-shape_3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_3.svg"}}){_id}
listsAddOne3: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_4.svg",fr:"combined-shape_4.svg",nl:"combined-shape_4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_4.svg"}}){_id}
listsAddOne4: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_5.svg",fr:"combined-shape_5.svg",nl:"combined-shape_5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_5.svg"}}){_id}
listsAddOne5: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_6.svg",fr:"combined-shape_6.svg",nl:"combined-shape_6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_6.svg"}}){_id}
listsAddOne6: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_7.svg",fr:"combined-shape_7.svg",nl:"combined-shape_7.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_7.svg"}}){_id}
listsAddOne7: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_8.svg",fr:"combined-shape_8.svg",nl:"combined-shape_8.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_8.svg"}}){_id}
listsAddOne8: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_9.svg",fr:"combined-shape_9.svg",nl:"combined-shape_9.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_9.svg"}}){_id}
listsAddOne9: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_10.svg",fr:"combined-shape_10.svg",nl:"combined-shape_10.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_10.svg"}}){_id}
listsAddOne11: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_11.svg",fr:"combined-shape_11.svg",nl:"combined-shape_11.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_11.svg"}}){_id}
listsAddOne12: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_12.svg",fr:"combined-shape_12.svg",nl:"combined-shape_12.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_12.svg"}}){_id}
listsAddOne13: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_13.svg",fr:"combined-shape_13.svg",nl:"combined-shape_13.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_13.svg"}}){_id}
listsAddOne14: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_14.svg",fr:"combined-shape_14.svg",nl:"combined-shape_14.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_14.svg"}}){_id}
listsAddOne15: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_15.svg",fr:"combined-shape_15.svg",nl:"combined-shape_15.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_15.svg"}}){_id}
listsAddOne16: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_16.svg",fr:"combined-shape_16.svg",nl:"combined-shape_16.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_16.svg"}}){_id}
listsAddOne17: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_17.svg",fr:"combined-shape_17.svg",nl:"combined-shape_17.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_17.svg"}}){_id}
listsAddOne18: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_18.svg",fr:"combined-shape_18.svg",nl:"combined-shape_18.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_18.svg"}}){_id}
listsAddOne19: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_19.svg",fr:"combined-shape_19.svg",nl:"combined-shape_19.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_19.svg"}}){_id}
listsAddOne21: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_20.svg",fr:"combined-shape_20.svg",nl:"combined-shape_20.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_20.svg"}}){_id}
listsAddOne22: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_21.svg",fr:"combined-shape_21.svg",nl:"combined-shape_21.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_21.svg"}}){_id}
listsAddOne23: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_22.svg",fr:"combined-shape_22.svg",nl:"combined-shape_22.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_22.svg"}}){_id}
listsAddOne24: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_23.svg",fr:"combined-shape_23.svg",nl:"combined-shape_23.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_23.svg"}}){_id}
listsAddOne25: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_24.svg",fr:"combined-shape_24.svg",nl:"combined-shape_24.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_24.svg"}}){_id}
listsAddOne26: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_25.svg",fr:"combined-shape_25.svg",nl:"combined-shape_25.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_25.svg"}}){_id}
listsAddOne27: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_26.svg",fr:"combined-shape_26.svg",nl:"combined-shape_26.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_26.svg"}}){_id}
listsAddOne28: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_27.svg",fr:"combined-shape_27.svg",nl:"combined-shape_27.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_27.svg"}}){_id}
listsAddOne29: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_28.svg",fr:"combined-shape_28.svg",nl:"combined-shape_28.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_28.svg"}}){_id}
listsAddOne31: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_29.svg",fr:"combined-shape_29.svg",nl:"combined-shape_29.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_29.svg"}}){_id}
listsAddOne32: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_30.svg",fr:"combined-shape_30.svg",nl:"combined-shape_30.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_30.svg"}}){_id}
listsAddOne33: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape_31.svg",fr:"combined-shape_31.svg",nl:"combined-shape_31.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape_31.svg"}}){_id}
listsAddOne34: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-2.svg",fr:"combined-shape-copy-2.svg",nl:"combined-shape-copy-2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-2.svg"}}){_id}
listsAddOne35: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-3.svg",fr:"combined-shape-copy-3.svg",nl:"combined-shape-copy-3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-3.svg"}}){_id}
listsAddOne36: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-4.svg",fr:"combined-shape-copy-4.svg",nl:"combined-shape-copy-4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-4.svg"}}){_id}
listsAddOne37: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-5.svg",fr:"combined-shape-copy-5.svg",nl:"combined-shape-copy-5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-5.svg"}}){_id}
listsAddOne38: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-6.svg",fr:"combined-shape-copy-6.svg",nl:"combined-shape-copy-6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-6.svg"}}){_id}
listsAddOne39: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-7.svg",fr:"combined-shape-copy-7.svg",nl:"combined-shape-copy-7.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-7.svg"}}){_id}
listsAddOne41: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape-copy-8.svg",fr:"combined-shape-copy-8.svg",nl:"combined-shape-copy-8.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape-copy-8.svg"}}){_id}
listsAddOne42: listsAddOne(input:{ressourceType:equipment,title:{en:"combined-shape.svg",fr:"combined-shape.svg",nl:"combined-shape.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/combined-shape.svg"}}){_id}
listsAddOne43: listsAddOne(input:{ressourceType:feature,title:{en:"group.svg",fr:"group.svg",nl:"group.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/group.svg"}}){_id}
listsAddOne44: listsAddOne(input:{ressourceType:feature,title:{en:"shape_2.svg",fr:"shape_2.svg",nl:"shape_2.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_2.svg"}}){_id}
listsAddOne45: listsAddOne(input:{ressourceType:feature,title:{en:"shape_3.svg",fr:"shape_3.svg",nl:"shape_3.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_3.svg"}}){_id}
listsAddOne46: listsAddOne(input:{ressourceType:feature,title:{en:"shape_4.svg",fr:"shape_4.svg",nl:"shape_4.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_4.svg"}}){_id}
listsAddOne47: listsAddOne(input:{ressourceType:feature,title:{en:"shape_5.svg",fr:"shape_5.svg",nl:"shape_5.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_5.svg"}}){_id}
listsAddOne48: listsAddOne(input:{ressourceType:feature,title:{en:"shape_6.svg",fr:"shape_6.svg",nl:"shape_6.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape_6.svg"}}){_id}
listsAddOne49: listsAddOne(input:{ressourceType:feature,title:{en:"shape.svg",fr:"shape.svg",nl:"shape.svg"},thumbnail:{large:"https://workinflex-uploads-production.s3.eu-central-1.amazonaws.com/shape.svg"}}){_id}
}

View File

@ -0,0 +1,150 @@
import { AccountTypeEnum } from '@src/accounts/account.components';
import { newError } from '@seed/helpers/Error';
export const checkPermissions = (ressource: any, account: any | null, type: 'c' | 'r' | 'w' | 'd'): boolean => {
// Adding the public by default
let perm: any[] = [AccountTypeEnum.public];
if (account && account.types) {
/*
* Verify if admin
*/
if (account.types.includes(AccountTypeEnum.admin)) return true;
/*
* Verify if organisation type of access
*/
if (
ressource.organisationId &&
account.organisationIds &&
!account.organisationIds.includes(ressource.organisationId) &&
!ressource.r.includes(AccountTypeEnum.public)
)
throw newError(2100, { allowed: ressource.organisationId, you: account.organisationIds });
/*
* Add the account id and type
*/
perm.push(account._id);
perm = perm.concat(account.types);
}
/*
* Verify on the ressource level
*/
const permissions = ressource[type];
// if there is no permission on the ressource, return true
if (!permissions) return true;
let hasPerm = false;
// Verifying if it matches
for (let index = 0; index < perm.length; index++) {
const element = perm[index];
if (permissions.includes(element)) {
hasPerm = true;
break;
}
}
if (!hasPerm) throw newError(2000, { allowed: permissions, you: account });
return true;
};
export const checkOrganisationPermissions = (ressource: any, organisationId: string): boolean => {
if (organisationId == ressource.organisationId || organisationId == ressource._id) return true;
throw newError(2000, { allowedOrgId: ressource.organisationId, youOrgId: organisationId });
};
export const addPermissions = (ressource: any, type: ('r' | 'w' | 'd')[], ids: (string | string)[]): void => {
for (let index = 0; index < type.length; index++) {
const t = type[index];
ressource[t] = ressource[t].concat(ids);
}
};
export const addPermissionToQuery = (account: any | null, query: 'get' | 'update' | 'delete', params: any): any => {
let types: any[] = ['public'];
if (account && account.types) {
/*
* Verify if admin, no need to add the query filters
*/
if (account.types.includes(AccountTypeEnum.admin)) return params;
/*
* Verify if organisation type of access
*/
// if (account.organisationIds) {
// params.organisationId = { $in: account.organisationIds };
// }
/*
* Add the account id and type
*/
types.push(account._id);
types = types.concat(account.types);
}
if (!params.$and) params.$and = [];
switch (query) {
default:
case 'get':
params.$and.push({ r: { $in: types } });
break;
case 'update':
params.$and.push({ w: { $in: types } });
break;
case 'delete':
params.$and.push({ d: { $in: types } });
break;
}
return params;
};
export const addOrganisationToQuery = (account: any | null, query: 'get' | 'update' | 'delete', params: any): any => {
let types: any[] = ['public'];
if (account && account.types) {
/*
* Verify if admin, no need to add the query filters
*/
if (account.types.includes(AccountTypeEnum.admin)) return params;
/*
* Verify if organisation type of access
*/
if (account.organisationIds) {
params.organisationId = { $in: account.organisationIds };
}
/*
* Add the account id and type
*/
types.push(account._id);
types = types.concat(account.types);
}
switch (query) {
default:
case 'get':
params = { ...params, $or: [{ r: { $in: types } }] };
break;
case 'update':
params = { ...params, $or: [{ w: { $in: types } }] };
break;
case 'delete':
params = { ...params, $or: [{ d: { $in: types } }] };
break;
}
return params;
};

View File

@ -0,0 +1,828 @@
import { v4 as uuid } from 'uuid';
import { Collection, FilterQuery, UpdateQuery } from 'mongodb';
import { classToPlain, Exclude, plainToClassFromExist } from 'class-transformer';
import { Field, ID, ObjectType, Authorized } from 'type-graphql';
import DB, { createSetRequest } from '@seed/services/database/DBService';
import { Permission } from '@seed/interfaces/permission';
import { newError } from '@seed/helpers/Error';
import { parsePaginationOptions } from '@seed/helpers/Request';
import { GetArgs, GetManyArgs } from './Request';
import { addPermissions, addPermissionToQuery, checkPermissions, checkOrganisationPermissions } from './AccessService';
import { ApolloContext } from '@seed/interfaces/context';
import _ from 'lodash';
import { StreamOperationType, PostHookStatus } from '@seed/services/change-stream/change-stream.components';
import { asyncHook } from '@seed/services/hooks/hooks.decorator';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { promiseAll } from '@seed/helpers/Utils';
import { EnginePathComponent } from '@seed/interfaces/components';
export interface BaseInterface {
_id: string;
updatedAt: Date;
createdAt: Date;
collectionName: string;
}
@ObjectType()
export abstract class BaseGraphModel implements BaseInterface {
@Exclude()
public collectionName: string;
@Exclude()
protected permissions: Permission;
@Exclude()
defaultSort: string;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
@Field(() => ID)
_id: string;
@Field(() => ID, { nullable: true })
organisationId?: string;
@Field(() => ID, { nullable: true })
createdBy?: string;
@Field(() => ID, { nullable: true })
updatedBy?: string;
@Field(() => ID, { nullable: true })
deletedBy?: string;
@Field()
createdAt: Date;
@Field()
updatedAt: Date;
@Field(() => [String])
public r: string[];
@Authorized('admin')
@Field(() => [String])
public w: string[];
@Authorized('admin')
@Field(() => [String])
public d: string[];
paths: EnginePathComponent[];
public constructor(init: { collectionName: string; permissions: Permission; defaultSort?: string }) {
this.collectionName = init.collectionName;
this.permissions = init.permissions;
this.r = init.permissions.r;
this.w = init.permissions.w;
this.d = init.permissions.d;
this.createdAt = new Date();
this.updatedAt = new Date();
this.defaultSort = init.defaultSort || 'createdAt desc';
}
/*
*
* Model functions
*
*/
public get(): any {
return classToPlain(this);
}
public set(doc: any): void {
plainToClassFromExist(this, doc);
if (doc._id) this._id = doc._id;
if (doc.r) this.r = _.uniq(_.concat(this.r, doc.r));
if (doc.w) this.w = _.uniq(_.concat(this.w, doc.w));
if (doc.d) this.d = _.uniq(_.concat(this.d, doc.d));
if (doc.organisationId) this.organisationId = doc.organisationId;
if (doc.createdAt) this.createdAt = doc.createdAt;
if (doc.updatedAt) this.updatedAt = doc.updatedAt;
}
abstract searchOptions(): string[];
abstract filterOptions(): string[];
searchEngine?(): any;
async prehook?(): Promise<void>;
async beforeCreate?(ctx?: ApolloContext | null): Promise<void>;
async beforeUpdate?(ctx?: ApolloContext | null): Promise<void>;
async beforeDelete?(ctx?: ApolloContext | null): Promise<void>;
async afterCreate?(changeStream: any): Promise<void>;
async afterUpdate?(changeStream: any): Promise<void>;
async afterDelete?(changeStream: any): Promise<void>;
/*
*
* DB Functions
*
*/
public async db(): Promise<Collection<this>> {
return await (await DB.getInstance()).db.collection<this>(this.collectionName);
}
public getPath(): EnginePathComponent[] {
if (this.paths) {
this.paths.push({
ressourceModel: this.collectionName,
ressourceId: this._id,
});
return this.paths;
} else
return [
{
ressourceModel: this.collectionName,
ressourceId: this._id,
},
];
}
public getQuery(query: FilterQuery<this>, ctx: ApolloContext | null): any {
let finalQuery: any;
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
// We add the permission (on r field) to make sure that kind of user has access to the ressource
finalQuery = addPermissionToQuery(account, 'get', { ...query });
// We check if it comes from an organisation
// Check of the user's correct authorisation on that organisation comes from the middleware
if (!ctx.ctx.noOrganisationCheck) {
if (organisationId) {
if (this.collectionName == 'accounts') finalQuery['organisationIds'] = organisationId;
else finalQuery['organisationId'] = { $eq: organisationId };
} else {
// If admin & public query -> do not add this condition and let search every records)
if (
account &&
account.types &&
!account.types.includes(AccountTypeEnum.public) &&
!account.types.includes(AccountTypeEnum.admin)
) {
finalQuery['organisationId'] = { $exists: false };
}
}
}
} else {
finalQuery = { ...query };
}
return finalQuery;
}
public async getMany(query: FilterQuery<this>, pagination: GetArgs | undefined, ctx: ApolloContext | null): Promise<any> {
const finalQuery = this.getQuery(query, ctx);
if (!pagination) pagination = { limit: 100, skip: 0 };
if (!pagination.sort) pagination.sort = this.defaultSort;
const paginationOption = parsePaginationOptions(pagination);
try {
console.log('search request', JSON.stringify(finalQuery, null, 2));
return await (await this.db()).find(finalQuery, paginationOption).toArray();
} catch (error) {
return [];
}
}
public async getCount(query: FilterQuery<this>, ctx: ApolloContext | null): Promise<number> {
const finalQuery = this.getQuery(query, ctx);
try {
const result = (await (await this.db()).countDocuments(finalQuery)) as any;
console.log(result);
return result;
} catch (error) {
return 0;
}
}
public async getAll(query: FilterQuery<this>, ctx: ApolloContext | null): Promise<any> {
const finalQuery = this.getQuery(query, ctx);
try {
return await (await this.db()).find(finalQuery).toArray();
} catch (error) {
return [];
}
}
public async getOne(query: FilterQuery<this>, ctx: ApolloContext | null): Promise<this> {
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
const doc = await (await this.db()).findOne(query);
if (!doc) throw newError(404, query);
this.set(doc);
if (!ctx.ctx.noOrganisationCheck) {
if (organisationId) {
if (this.collectionName != 'accounts') checkOrganisationPermissions(this, organisationId);
}
}
checkPermissions(this, account, 'r');
return this;
} else {
const doc = await (await this.db()).findOne(query);
if (!doc) throw newError(404, query);
this.set(doc);
return this;
}
}
public async saveOne(newData: Partial<this>, ctx: ApolloContext | null, upsert = true): Promise<this> {
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
addPermissions(this, ['r', 'w', 'd'], [account._id]);
if (organisationId) {
newData.organisationId = organisationId;
}
newData.createdBy = account._id;
}
this.set(newData);
// if (!ctx) ctx = await createApolloContext();
try {
if (this.prehook) await this.prehook();
if (this.beforeCreate) await this.beforeCreate(ctx);
} catch (error) {
throw error;
}
const { _id, ...dataToSave } = this.get();
const savedId = _id ? _id : uuid();
try {
const result = await (await this.db()).findOneAndUpdate(
{ _id: savedId },
{
$set: dataToSave,
$setOnInsert: {
_id: savedId,
},
} as any,
{ upsert, returnOriginal: false },
);
if (result.ok == 0) throw newError(1000, result);
this.set({ _id: savedId });
const changeStreamId = uuid();
const changeStreamData = {
_id: changeStreamId,
operation: StreamOperationType.insert,
collection: this.collectionName,
documentKey: this._id,
insertedValues: this.get(),
hookStatus: PostHookStatus.new,
createdAt: new Date(),
updateAt: new Date(),
};
await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData);
if (this.afterCreate && (await asyncHook(changeStreamData))) {
await this.afterCreate(changeStreamData);
await (await DB.getInstance()).db
.collection('stream.changes')
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
return this;
} catch (error) {
throw newError(1000, error);
}
}
public async saveMany(newDataArr: Partial<this>[], ctx: ApolloContext | null): Promise<any[]> {
// Initialize the change stream
const changeStreams: any[] = [];
const afterCreatePromises: any[] = [];
const prehooksPromises = newDataArr.map(async (newData) => {
const clone = Object.assign(Object.create(Object.getPrototypeOf(this)), this);
if (ctx) {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
addPermissions(clone, ['r', 'w', 'd'], [account._id]);
if (organisationId) {
newData.organisationId = organisationId;
}
newData.createdBy = account._id;
}
clone.set(newData);
// if (!ctx) ctx = await createApolloContext();
if (clone.prehook) await clone.prehook();
if (clone.beforeCreate) await clone.beforeCreate(ctx);
const { _id, ...dataToSave } = clone.get();
const savedId = _id ? _id : uuid();
clone.set({ _id: savedId });
const changeStreamId = uuid();
const changeStreamData = {
_id: changeStreamId,
operation: StreamOperationType.insert,
collection: clone.collectionName,
documentKey: clone._id,
insertedValues: clone.get(),
hookStatus: PostHookStatus.new,
createdAt: new Date(),
updateAt: new Date(),
};
changeStreams.push(changeStreamData);
// eslint-disable-next-line @typescript-eslint/no-this-alias
afterCreatePromises.push(async () => {
if (clone.afterCreate) {
const res = await asyncHook(changeStreamData);
if (res) {
await clone.afterCreate(changeStreamData);
await (await DB.getInstance()).db
.collection('stream.changes')
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
}
});
return clone.get();
});
try {
const dataArr = await Promise.all(prehooksPromises);
await (await this.db()).insertMany(dataArr, { ordered: false });
await (await DB.getInstance()).db.collection('stream.changes').insertMany(changeStreams, { ordered: false });
await promiseAll(afterCreatePromises);
return dataArr;
} catch (error) {
console.error(error);
throw newError(1000, error);
}
}
public async updateOne(query: FilterQuery<this>, newData: Partial<this>, ctx: ApolloContext | null): Promise<this> {
try {
const dataThatHaveBeenChanged = createSetRequest<this>('', { ...newData, updatedAt: new Date() });
let dataBeforeChange: any;
if (ctx) {
const account = ctx.ctx.user;
// Check if doc exists & correct permissions
if (!this._id) await this.getOne(query, ctx);
dataBeforeChange = this.get();
dataThatHaveBeenChanged.updatedBy = account._id;
this.set(dataThatHaveBeenChanged);
if (this.beforeUpdate) await this.beforeUpdate(ctx);
checkPermissions(this, account, 'w');
const result = await (await this.db()).findOneAndUpdate(
query,
{ $set: dataThatHaveBeenChanged },
{ upsert: false, returnOriginal: false },
);
if (result.ok != 1) throw newError(1000, result);
this.set(result.value);
} else {
if (!this._id) await this.getOne(query, null);
dataBeforeChange = this.get();
this.set(dataThatHaveBeenChanged);
if (this.beforeUpdate) await this.beforeUpdate();
const result = await (await this.db()).findOneAndUpdate(
query,
{ $set: dataThatHaveBeenChanged },
{ upsert: false, returnOriginal: false },
);
if (result.ok != 1) throw newError(1000, result);
this.set(result.value);
}
const changeStreamId = uuid();
const changeStreamData = {
_id: changeStreamId,
operation: StreamOperationType.update,
collection: this.collectionName,
documentKey: this._id,
updatedValues: dataThatHaveBeenChanged,
beforeUpdateValues: dataBeforeChange,
hookStatus: PostHookStatus.new,
createdAt: new Date(),
updateAt: new Date(),
};
await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData);
if (this.afterUpdate && (await asyncHook(changeStreamData))) {
await this.afterUpdate(changeStreamData);
await (await DB.getInstance()).db
.collection('stream.changes')
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
return this;
} catch (error) {
throw error;
}
}
public async updateOneCustom(query: FilterQuery<this>, set: UpdateQuery<this>, ctx: ApolloContext | null): Promise<this> {
try {
let dataBeforeChange: any;
if (ctx) {
const account = ctx.ctx.user;
// Check if doc exists
if (!this._id) await this.getOne(query, ctx);
dataBeforeChange = this.get();
checkPermissions(this, account, 'w');
// Validate new data that will go into the DB
// const validation = await this.validate();
// if (!validation.success) return validation;
// Prepare data
this.updatedAt = new Date();
const result = await (await this.db()).findOneAndUpdate(query, set, { upsert: false, returnOriginal: false });
if (result.ok != 1) throw newError(1000, result);
if (result.value) this.set(result.value);
} else {
if (!this._id) await this.getOne(query, null);
dataBeforeChange = this.get();
// Prepare data
this.updatedAt = new Date();
const result = await (await this.db()).findOneAndUpdate(query, set, { upsert: false, returnOriginal: false });
if (result.ok != 1) throw newError(1000, result);
if (result.value) this.set(result.value);
}
const changeStreamId = uuid();
const changeStreamData = {
_id: changeStreamId,
operation: StreamOperationType.update,
collection: this.collectionName,
documentKey: this._id,
hookStatus: PostHookStatus.new,
updatedValues: JSON.stringify(set),
beforeUpdateValues: dataBeforeChange,
createdAt: new Date(),
updateAt: new Date(),
};
await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData);
if (this.afterUpdate && (await asyncHook(changeStreamData))) {
await this.afterUpdate(changeStreamData);
await (await DB.getInstance()).db
.collection('stream.changes')
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
return this;
} catch (error) {
throw error;
}
}
public async deleteOne(query: FilterQuery<this>, ctx: ApolloContext | null): Promise<this> {
try {
if (ctx) {
const account = ctx.ctx.user;
if (!this._id) await this.getOne(query, ctx);
checkPermissions(this, account, 'd');
if (this.beforeDelete) await this.beforeDelete(ctx);
const result = await (await this.db()).findOneAndDelete(query);
if (result.ok != 1) throw newError(1001);
const deletedModel = '_deleted_' + this.collectionName;
await (await DB.getInstance()).db.collection(deletedModel).insertOne({ ...result.value, deletedBy: account._id });
} else {
if (!this._id) await this.getOne(query, null);
if (this.beforeDelete) await this.beforeDelete();
const result = await (await this.db()).findOneAndDelete(query);
if (result.ok != 1) throw newError(1001);
const deletedModel = '_deleted_' + this.collectionName;
await (await DB.getInstance()).db.collection(deletedModel).insertOne(result.value);
}
const changeStreamId = uuid();
const changeStreamData = {
_id: changeStreamId,
operation: StreamOperationType.delete,
collection: this.collectionName,
documentKey: this._id,
hookStatus: PostHookStatus.new,
beforeUpdateValues: this.get(),
createdAt: new Date(),
updateAt: new Date(),
};
await (await DB.getInstance()).db.collection('stream.changes').insertOne(changeStreamData);
if (this.afterDelete && (await asyncHook(changeStreamData))) {
await this.afterDelete(changeStreamData);
await (await DB.getInstance()).db
.collection('stream.changes')
.updateOne({ _id: changeStreamId }, { $set: { hookStatus: PostHookStatus.completed } });
}
return this;
} catch (error) {
throw error;
}
}
/*
*
* Permissions
*
*/
public async updateQueryWithPermission(query: FilterQuery<this>, ctx: ApolloContext): Promise<FilterQuery<this>> {
const account = ctx.ctx.user;
const organisationId = ctx.ctx.organisationId;
// We add the permission (on r field) to make sure that kind of user has access to the ressource
const finalQuery = addPermissionToQuery(account, 'get', { ...query });
// We check if it comes from an organisation
// Check of the user's correct authorisation on that organisation comes from the middleware
if (organisationId) {
finalQuery['organisationId'] = { $eq: organisationId };
} else {
finalQuery['organisationId'] = { $exists: false };
}
return finalQuery;
}
// public async updateOnePermission(
// query: FilterQuery<this>,
// add: 'add' | 'remove',
// type: 'r' | 'w' | 'd',
// newPerm: string,
// account?: AccountModel,
// ): Promise<this> {
// // Get the object from the DB
// try {
// // Check if doc exists
// // TODO : if (this.get() returns, then don't)
// await this.getOne(query, account);
// if (account) this.checkPermissions(account.get(), 'w');
// // Validate new data that will go into the DB
// // const validation = await this.validate();
// // if (!validation.success) return validation;
// // Prepare data
// this.updatedAt = new Date();
// if (add == 'add') {
// const result = await (await this.db()).findOneAndUpdate(
// query,
// { $addToSet: { [type]: newPerm } },
// { upsert: false, returnOriginal: false },
// );
// if (result.ok != 1) throw newError('general.dbError', '400', result);
// this.set(result.value);
// } else if (add == 'remove') {
// const result = await (await this.db()).findOneAndUpdate(
// query,
// { $pull: { [type]: newPerm } },
// { upsert: false, returnOriginal: false },
// );
// if (result.ok != 1) throw newError('general.dbError', '400', result);
// this.set(result.value);
// }
// return this;
// } catch (error) {
// throw error;
// }
// }
public async addOneSubModel<SUB extends BaseGraphModel>(
parentQ: { _id: string },
subModelRefField: string,
subModelFieldName: keyof this,
subModel: SUB,
subModelInput: any,
ctx: ApolloContext | null,
): Promise<SUB> {
try {
subModelInput[subModelRefField] = parentQ._id;
const subM = await subModel.saveOne(subModelInput, ctx);
await this.updateOneCustom(parentQ as any, { $push: { [`${subModelFieldName}`]: subM._id } } as any, null);
return subM;
} catch (error) {
throw error;
}
}
public async editOneSubModel<SUB extends BaseGraphModel>(
subModel: SUB,
subModelId: string,
subModelInput: any,
ctx: ApolloContext | null,
): Promise<SUB> {
try {
// No need to check if belongs to the correct organisation, the middleware does that
const subM = await subModel.updateOne({ _id: subModelId } as any, subModelInput, ctx);
return subM;
} catch (error) {
throw error;
}
}
public async deleteOneSubModel<SUB extends BaseGraphModel>(
parentQ: FilterQuery<this>,
subModelFieldName: keyof this,
subModel: SUB,
subModelId: string,
ctx: ApolloContext | null,
): Promise<SUB> {
try {
const subM = await subModel.deleteOne({ _id: subModelId } as any, ctx);
await this.updateOneCustom(parentQ, { $pull: { [`${subModelFieldName}`]: subM._id } } as any, null);
return subM;
} catch (error) {
throw error;
}
}
public getOneSubRessource(subRessoureFieldName: keyof this | string, subRessoureId: string): any | undefined {
try {
const subress = _.get(this, subRessoureFieldName);
if (subress) {
const ind = _.findIndex(subress, { _id: subRessoureId });
if (ind !== -1) return subress[ind];
}
return;
} catch (error) {
throw error;
}
}
public getDefaultSubRessource(subRessoureFieldName: keyof this | string): any | undefined {
try {
const subress = _.get(this, subRessoureFieldName);
if (subress) {
const ind = _.findIndex(subress, { default: true });
if (ind !== -1) return subress[ind];
return subress[0];
}
return;
} catch (error) {
throw error;
}
}
public async addOneSubRessource(
parentQ: { _id: string },
subRessoureFieldName: keyof this | string,
subRessoure: any,
ctx: ApolloContext | null,
): Promise<any> {
try {
if (subRessoure.default && _.get(this, subRessoureFieldName)) {
// Remove all default from others
await this.updateOneCustom(
{ _id: this._id } as any,
{
$set: { [`${subRessoureFieldName}.$[].default`]: false } as any,
},
ctx,
);
} else subRessoure.default = false;
// Generate ID if not in the model
if (!subRessoure._id) subRessoure._id = uuid();
await this.updateOneCustom(parentQ as any, { $push: { [`${subRessoureFieldName}`]: subRessoure } } as any, ctx);
return subRessoure;
} catch (error) {
throw error;
}
}
public async editOneSubRessource(
parentQ: { _id: string },
subRessoureFieldName: keyof this | string,
subRessoureId: string,
subRessoure: any,
ctx: ApolloContext | null,
): Promise<any> {
try {
if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw newError(404);
if (subRessoure.default) {
// Remove all default from others
await this.updateOneCustom(
{ _id: this._id } as any,
{
$set: { [`${subRessoureFieldName}.$[].default`]: false } as any,
},
ctx,
);
} else subRessoure.default = false;
const filterQ = {
...parentQ,
[`${subRessoureFieldName}._id`]: subRessoureId,
};
await this.updateOneCustom(
filterQ as any,
{
$set: {
[`${subRessoureFieldName}.$`]: {
...subRessoure,
_id: subRessoureId,
},
} as any,
},
ctx,
);
return {
...subRessoure,
_id: subRessoureId,
};
} catch (error) {
throw error;
}
}
public async deleteOneSubRessource(
parentQ: { _id: string },
subRessoureFieldName: keyof this | string,
subRessoureId: string,
ctx: ApolloContext | null,
): Promise<string> {
try {
if (!this.getOneSubRessource(subRessoureFieldName, subRessoureId)) throw newError(404);
const filterQ = {
...parentQ,
[`${subRessoureFieldName}._id`]: subRessoureId,
};
await this.updateOneCustom(filterQ as any, { $pull: { [`${subRessoureFieldName}`]: { _id: subRessoureId } } } as any, ctx);
return subRessoureId;
} catch (error) {
throw error;
}
}
}

View File

@ -0,0 +1,122 @@
import _ from 'lodash';
import { ApolloError } from 'apollo-server-lambda';
import { GetArgs } from './Request';
import { ApolloContext } from '@seed/interfaces/context';
import { BaseGraphModel } from './BaseGraphModel';
import { NewPermissionInput } from '@seed/interfaces/components';
import { baseSearchFunction } from '@seed/services/database/DBRequestService';
import { newError } from '@seed/helpers/Error';
export const getOneGeneric = async <T extends BaseGraphModel>(model: T, id: string, ctx: ApolloContext): Promise<any> => {
try {
await model.getOne({ _id: id } as any, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const addOneGeneric = async <T extends BaseGraphModel>(model: T, body: Partial<T>, ctx: ApolloContext): Promise<T> => {
try {
await model.saveOne(body, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const editOneGeneric = async <T extends BaseGraphModel>(model: T, id: string, body: Partial<T>, ctx: ApolloContext): Promise<T> => {
try {
await model.updateOne({ _id: id } as any, body, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const deleteOneGeneric = async <T extends BaseGraphModel>(model: T, id: string, ctx: ApolloContext): Promise<T> => {
try {
await model.deleteOne({ _id: id } as any, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const getManyGeneric = async <T extends BaseGraphModel>(model: T, query: any, pagination: GetArgs, ctx: ApolloContext): Promise<any> => {
return baseSearchFunction({
model: model,
query: query,
pagination: pagination,
ctx: ctx,
});
};
export const getCountGeneric = async <T extends BaseGraphModel>(model: T, baseArguments: any, ctx: ApolloContext): Promise<number> => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { pagination, ...args } = baseArguments;
const result = await baseSearchFunction({
model: model,
query: args,
count: true,
ctx: ctx,
});
return result.count || 0;
};
export const getManyGenericWithArgs = async <T extends BaseGraphModel>(model: T, baseArguments: any, ctx: ApolloContext): Promise<any> => {
try {
const { pagination, ...args } = baseArguments;
return baseSearchFunction({
model: model,
query: args,
pagination: pagination,
ctx: ctx,
});
} catch (error) {
throw error;
}
};
export const permissionsAddOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise<any> => {
try {
await model.updateOneCustom({ _id: id }, { $addToSet: { [`${input.permissionType}`]: input.permission } }, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const permissionsRemoveOne = async (model: any, id: string, input: NewPermissionInput, ctx: ApolloContext): Promise<any> => {
try {
await model.updateOneCustom({ _id: id }, { $pull: { [`${input.permissionType}`]: input.permission } }, ctx);
return model.get();
} catch (error) {
throw error;
}
};
export const publishOneGeneric = async <T extends BaseGraphModel>(model: T, id: string, ctx: ApolloContext): Promise<any> => {
try {
const missingInputs: string[] = [];
// TODO THE CHECK OF THE INPUTS
if (missingInputs.length > 0) throw newError(3000, missingInputs);
return editOneGeneric(model, id, { published: true, publicationDate: new Date() } as any, ctx);
} catch (error) {
throw error;
}
};
export const unpublishOneGeneric = async <T extends BaseGraphModel>(model: T, id: string, ctx: ApolloContext): Promise<any> => {
try {
return editOneGeneric(model, id, { published: false } as any, ctx);
} catch (error) {
throw error;
}
};

View File

@ -0,0 +1,204 @@
import _ from 'lodash';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { ApolloError } from 'apollo-server-lambda';
import { AuthChecker, createMethodDecorator } from 'type-graphql';
import Firebase from '@seed/services/auth/FirebaseService';
import { ApolloContext } from '@seed/interfaces/context';
import AccountModel from 'src/accounts/account.model';
import Loaders from '@src/__indexes/__loaders';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { simpleEstimator, getComplexity, fieldExtensionsEstimator } from 'graphql-query-complexity';
import { clog } from '@seed/helpers/Utils';
import { newError } from '@seed/helpers/Error';
import { SecurityLevelEnum } from '@services/module-accounts/account.components';
export const ctxMiddleware = async (ctx: { event?: APIGatewayProxyEvent; context?: Context; req?: any }): Promise<ApolloContext> => {
const user: AccountModel = new AccountModel();
let headers: any = {};
let organisationId = null;
try {
if (ctx.event) headers = ctx.event.headers;
else if (ctx.req) headers = ctx.req.headers;
/*
* Deal with Auth
*/
const authorization: string = headers['x-authorization'] || headers.authorization || headers.Authorization;
if (authorization) {
// Verify the token
const firebaseRes = await Firebase.getInstance().tokenIdentify(authorization);
// Verify that exists in DB
await user.getOne({ _id: firebaseRes.uid }, null);
/*
* Deal with Security
*/
const securityCheck = user.securityCheck();
if (!securityCheck) {
throw newError(2400, {
security: user.security,
securityLevel: process.env.SECURITY_LEVEL || SecurityLevelEnum.nothing,
});
}
/*
* Deal with Organisation
*/
organisationId = headers.organisationId || headers.organisationid;
if (organisationId) {
// Check if admin
if (user.types && user.types.includes(AccountTypeEnum.admin)) {
console.log('[ADMIN BYPASS]');
} else if (!user._id || !user.organisationIds || !user.organisationIds?.includes(organisationId))
throw newError(2101, {
accountOrganisationIds: user.organisationIds,
organisationId: organisationId,
});
console.log('[ORG]', organisationId);
}
console.log('[AUTH - ]', user.types, user?._id);
} else {
console.log('no auth');
}
} catch (error) {
throw error;
}
return {
event: ctx.event || ctx.req || {},
context: ctx.context || ctx.req || {},
ctx: {
organisationId: organisationId,
user: user,
loaders: new Loaders(),
},
};
};
export const authMiddleware: AuthChecker<ApolloContext> = ({ root, args, context, info }, roles) => {
const user = context.ctx.user;
if (!user || !user._id) {
throw newError(2004);
}
if (roles.length > 0) {
roles.push(AccountTypeEnum.admin);
const intersection = _.intersection(roles, user.types);
if (intersection.length > 0) return true;
// no roles matched, restrict access
throw newError(2000, { you: user.types, allowed: roles });
}
return true;
};
export const checkOrganisation = (check = false): any => {
return createMethodDecorator(async ({ root, args, context, info }, next) => {
if (check && !(context as any).ctx.organisationId) throw newError(2102, '400');
return next();
});
};
export const totalPublic = (check = false): any => {
return createMethodDecorator(async ({ root, args, context, info }, next) => {
if (check) (context as any).ctx.noOrganisationCheck = true;
return next();
});
};
export const errorMiddleware = (err): any => {
// Don't give the specific errors to the client.
if (err.extensions) {
const { translations, ...data } = err.extensions;
const errorToReturn = {
code: err.extensions.code,
message: err.extensions.message || err.message,
translations,
data: process.env.NODE_ENV == 'production' ? { message: 'see logs' } : data,
};
clog(errorToReturn);
return errorToReturn;
}
// Otherwise return the original error. The error can also
// be manipulated in other ways, so long as it's returned.
return err;
};
export const complexityMiddleware = (schema: any, variables: any, query: any) => {
const maximumComplexity = process.env.GRAPH_COMPLEXITY ? parseInt(process.env.GRAPH_COMPLEXITY) : 1000;
const complexity = getComplexity({
schema: schema,
// The maximum allowed query complexity, queries above this threshold will be rejected
// The query variables. This is needed because the variables are not available
// in the visitor of the graphql-js library
variables: variables,
// specify operation name only when pass multi-operation documents
query: query,
// Add any number of estimators. The estimators are invoked in order, the first
// numeric value that is being returned by an estimator is used as the field complexity.
// If no estimator returns a value, an exception is raised.
estimators: [
// Add more estimators here...
fieldExtensionsEstimator(),
// This will assign each field a complexity of 1 if no other estimator
// returned a value.
simpleEstimator({
defaultComplexity: 1,
}),
],
});
// Here we can react to the calculated complexity,
// like compare it with max and throw error when the threshold is reached.
if (complexity >= maximumComplexity) {
throw new Error(`Sorry, too complicated query! ${complexity} is over ${maximumComplexity} that is the max allowed complexity.`);
}
// And here we can e.g. subtract the complexity point from hourly API calls limit.
console.log('Used query complexity points:', complexity);
};
export const oneToManyComplexity = ({ args, childComplexity }): number => {
if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit;
else return childComplexity * 10;
};
export const oneToManyComplexityNoLoader = ({ args, childComplexity }): number => {
if (args.pagination && args.pagination.limit) return childComplexity * args.pagination.limit * 100;
else return childComplexity * 10 * 100;
};
export const createApolloContext = async (accountId?: string, organisationId: string | null = null): Promise<ApolloContext> => {
const user: AccountModel = new AccountModel();
if (accountId) {
await user.getOne({ _id: accountId }, null);
}
return {
event: {} as any,
context: {} as any,
ctx: {
organisationId,
user: user,
loaders: new Loaders(),
},
};
};

View File

@ -0,0 +1,132 @@
import _ from 'lodash';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { ApolloError } from 'apollo-server-lambda';
import { ClassType, createMethodDecorator, MiddlewareFn } from 'type-graphql';
import Firebase from '@seed/services/auth/FirebaseService';
import { ApolloContext } from '@seed/interfaces/context';
import AccountModel from 'src/accounts/account.model';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { validateOrReject } from 'class-validator';
import { plainToClass } from 'class-transformer';
import { newError } from '@seed/helpers/Error';
export interface EngineMiddlewareInput {
authorization?: AccountTypeEnum[];
apiKey?: boolean;
checkOrganisation?: boolean;
noOrganisationCheck?: boolean;
noPermissionCheck?: boolean;
validations?: {
inputName?: string;
schema: ClassType<any>;
}[];
}
export const checkApiKey = async ({ context }): Promise<boolean> => {
const user: AccountModel = new AccountModel();
let headers: any = {};
if (context.event) headers = context.event.headers;
else if (context.req) headers = context.req.headers;
/*
* Deal with Auth
*/
const apiKey: string = headers['x-api-key'];
if (apiKey) {
// Verify that exists in DB
try {
const account = await user.getOne({ 'apiKeys.key': apiKey }, null);
context.ctx.user = account;
return true;
} catch (error) {
throw newError(2200, '403');
}
} else throw newError(2202, { required: 'X-API-Key' });
};
export const checkAuth = ({ context }, user, roles: string[]): boolean => {
let headers: any = {};
if (context.event) headers = context.event.headers;
else if (context.req) headers = context.req.headers;
if (roles.includes(AccountTypeEnum.public)) return true;
const authorization: string = headers['x-authorization'] || headers.authorization || headers.Authorization;
if (!authorization) {
throw newError(2002);
}
if (!user || !user._id) {
throw newError(2004, '403');
}
if (roles.length > 0) {
roles.push(AccountTypeEnum.admin);
user.types.push(AccountTypeEnum.public);
const rolesToVerif = _.uniq(roles);
const userTypes = _.uniq(user.types);
const intersection = _.intersection(rolesToVerif, userTypes);
if (intersection.length > 0) return true;
// no roles matched, restrict access
throw newError(2000, { you: user.types, allowed: roles });
}
return true;
};
export const EngineMiddleware = (init: EngineMiddlewareInput): any => {
return createMethodDecorator(async (data, next) => {
// console.log('engine', data);
const { context, args } = data;
let returnError;
if (init.authorization) {
const user = (context as any).ctx.user;
try {
checkAuth({ context }, user, init.authorization);
} catch (error) {
returnError = error;
}
}
if (init.apiKey) {
try {
await checkApiKey({ context });
returnError = null;
} catch (error) {
returnError = error;
}
}
if (returnError) throw returnError;
if (init.checkOrganisation && !(context as any).ctx.organisationId) throw newError(2102, '400');
if (init.noOrganisationCheck) (context as any).ctx.noOrganisationCheck = true;
if (init.noPermissionCheck) (context as any).ctx.noPermissionCheck = true;
if (init.validations)
for (let index = 0; index < init.validations.length; index++) {
const element = init.validations[index];
if (!element.inputName) element.inputName = 'input';
try {
const instance = plainToClass(element.schema, args[element.inputName]);
await validateOrReject(instance);
} catch (error) {
throw newError(3001, error);
}
}
return next();
});
};
export const inputValidation: MiddlewareFn<any> = async ({ root, args, context, info }, next) => {
console.log({ root, args, context, info });
return next();
};

View File

@ -0,0 +1,54 @@
import { Field, Int, InputType, ArgsType } from 'type-graphql';
import { Min, Max } from 'class-validator';
import { DateTabType, RangeType } from '@seed/interfaces/components';
import { DateRangeComponent } from '@seed/interfaces/components.dates';
@InputType()
export class GetArgs {
@Field(() => Int)
@Min(1)
@Max(100)
limit: number;
@Field(() => Int)
@Min(0)
skip: number;
@Field({ nullable: true })
sort?: string;
}
@ArgsType()
export class GetManyArgs {
@Field(() => [String], { nullable: true })
_ids?: string[];
@Field(() => String, { nullable: true })
search?: string;
@Field(() => Date, { nullable: true })
afterCreatedAt?: Date;
@Field(() => Date, { nullable: true })
afterUpdatedAt?: Date;
@Field(() => GetArgs, { nullable: true })
pagination?: GetArgs;
}
@ArgsType()
export class GetManyArgsWithDateRange extends GetManyArgs {
@Field({ nullable: true })
startDate?: Date;
@Field({ nullable: true })
endDate?: Date;
@Field(() => RangeType, { nullable: true })
rangeType?: RangeType;
@Field(() => DateTabType, { nullable: true })
dateTabType?: DateTabType;
@Field(() => DateRangeComponent, { nullable: true })
dateRange?: DateRangeComponent;
}

102
lib/seed/graphql/Server.ts Normal file
View File

@ -0,0 +1,102 @@
import { nullToUndefined } from '@seed/helpers/Utils';
import { ApolloServer } from 'apollo-server-lambda';
import { buildSchema } from 'type-graphql';
import { ctxMiddleware, authMiddleware, errorMiddleware, complexityMiddleware } from './Middleware';
import { SettingsCache } from './Settings';
export async function createServer(resolversEntry: any): Promise<ApolloServer> {
const schema = await buildSchema({
resolvers: resolversEntry,
authChecker: authMiddleware,
validate: false,
});
const server = new ApolloServer({
// typeDefs,
// resolvers,
schema,
formatError: errorMiddleware,
formatResponse: (response): any => {
return nullToUndefined(response);
},
context: ctxMiddleware,
tracing: true,
plugins: [
{
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
requestDidStart: () => ({
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
didResolveOperation({ request, document }) {
return complexityMiddleware(schema, request.variables, document);
},
}),
},
],
});
return server;
}
export class ServerInstance {
private static instance: ServerInstance;
public handler!: any;
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static async getInstance(resolversEntry?: any): Promise<ServerInstance> {
const settingsI = SettingsCache.getInstance();
await settingsI.refreshCache();
if (!this.instance) {
const instance = new ServerInstance();
try {
// let { typeDefs, resolvers } = await buildTypeDefsAndResolvers({
// resolvers: resolversEntry,
// authChecker: authMiddleware,
// });
// typeDefs = typeDefs.replace('scalar Upload', 'scalar UploadFix');
// resolvers = resolvers;
const server = await createServer(resolversEntry);
instance.handler = server.createHandler({
cors: {
origin: '*',
credentials: true,
allowedHeaders: 'Content-Type,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent,organisationid',
},
});
console.log(`[GraphQL] Using new server in ${process.env.NODE_ENV}`);
this.instance = instance;
} catch (error) {
console.log('[GRAPH ERROR]', error);
throw error;
}
} else {
console.log(`[GraphQL] Using same server in ${process.env.NODE_ENV}`);
}
return this.instance;
}
public run(event: AWSLambda.APIGatewayProxyEvent, context: AWSLambda.Context): Promise<any> {
try {
console.log('BEGIN ACTION', event.body ? JSON.parse(event.body) : 'NO BODY');
} catch (error) {
console.log('BEGIN ACTION - Not JSON');
}
return new Promise((resolve, reject) => {
const callback = (error: any, body: unknown): void => {
console.log('END ACTION');
if (error) {
console.error(error);
reject(error);
} else resolve(body);
};
this.handler(event, context, callback);
});
}
}

View File

@ -0,0 +1,202 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */
/* eslint-disable prettier/prettier */
/* eslint-disable @typescript-eslint/no-use-before-define */
import { DateTime } from 'luxon';
import _ from 'lodash';
import env from '@config/.env.json';
import packageFile from 'package.json';
import { SettingsType } from '@seed/interfaces/components';
import { ErrorsConfig, NotificationEnum } from '@config/config';
import { EngineNotificationEnum } from '@seed/interfaces/components.notifications';
import { TranslatableComponent } from '@src/__components/components';
import { promiseAll } from '@seed/helpers/Utils';
import DB from '@seed/services/database/DBService';
import { v4 as uuid } from 'uuid';
export interface GetSettingsOutput {
env: any[];
errors: any[];
emails: any[];
}
export interface EngineServerCacheInfo {
emailsContent: {
[key in NotificationEnum | EngineNotificationEnum]: any;
};
errorsContent: {
[key: string]: {
message: string;
translations?: {
subject?: TranslatableComponent;
body?: TranslatableComponent;
};
};
};
}
export const getSettingsFromDB = async (): Promise<GetSettingsOutput> => {
const settings = (await await (await DB.getInstance()).db.collection('settings').find().toArray()) as (any | any | any)[];
// filtering client side because not too much data and no need to index db
return {
env: _.filter(settings, { type: SettingsType.env }) as any[],
errors: _.filter(settings, { type: SettingsType.errors }) as any[],
emails: _.filter(settings, { type: SettingsType.emails }) as any[],
};
};
export const syncEnvFromDB = (currentEnvInDB: GetSettingsOutput): void => {
// update env
currentEnvInDB.env.map((env) => {
process.env[env.key] = env.value;
});
};
export const initEnvironement = async (env: any): Promise<GetSettingsOutput> => {
const currentEnv = process.env.NODE_ENV && env[process.env.NODE_ENV] ? env[process.env.NODE_ENV] : env.default;
for (const k in currentEnv) {
process.env[k] = currentEnv[k];
}
// Init also Package.json
process.env.awsRegion = packageFile.devops.awsRegion;
const currentSettings = await getSettingsFromDB();
syncEnvFromDB(currentSettings);
// Force the local to be > DB
if (process.env.NODE_ENV == 'local') {
for (const k in currentEnv) {
process.env[k] = currentEnv[k];
}
}
console.log('[Server - Env] Loaded');
return currentSettings;
};
export class SettingsCache {
private static instance: SettingsCache;
private lastCacheDate: DateTime;
public cache: EngineServerCacheInfo;
// eslint-disable-next-line @typescript-eslint/no-empty-function
private constructor() {}
public static getInstance(): SettingsCache {
if (!this.instance) {
this.instance = new SettingsCache();
}
return this.instance;
}
public async refreshCache(): Promise<void> {
// check if needs to refresh the cache
if (!this.lastCacheDate || this.lastCacheDate.diff(DateTime.local(), 'minutes').minutes > 5) {
const settings = await initEnvironement(env);
const promises: Promise<any>[] = [];
// Map for errors
const errorsContent = await mapErrorContent(settings, promises);
// Map for emails
const emailsContent: {
[key in NotificationEnum | EngineNotificationEnum]: any;
} = await mapEmailSettings(settings, promises);
this.cache = {
errorsContent,
emailsContent,
};
this.lastCacheDate = DateTime.local();
if (promises.length > 0) {
console.log('Creating defaut in DB');
await promiseAll(promises);
}
}
}
}
async function mapErrorContent(settings: GetSettingsOutput, newData: Promise<any>[]) {
const errorsContent: {
[key: string]: {
message: string;
translations?: {
subject?: TranslatableComponent;
body?: TranslatableComponent;
};
};
} = {};
for (const key in ErrorsConfig) {
if (Object.prototype.hasOwnProperty.call(ErrorsConfig, key)) {
const errIndex = _.findIndex(settings.errors, { key: key });
errorsContent[key] = {
message: ErrorsConfig[key],
};
if (errIndex === -1) {
console.error('[SETTINGS - ERROR] Creating default', key);
newData.push(
(await DB.getInstance()).db.collection('settings').insertOne({
key,
type: SettingsType.errors,
value: ErrorsConfig[key],
body: new TranslatableComponent(),
_id: uuid(),
r: ['admin'],
w: ['admin'],
d: ['admin'],
createdAt: new Date(),
updatedAt: new Date(),
}),
);
} else {
errorsContent[key].translations = {
subject: settings.errors[errIndex].subject,
body: settings.errors[errIndex].body,
};
}
}
}
return errorsContent;
}
async function mapEmailSettings(settings: GetSettingsOutput, newData: Promise<any>[]) {
const emails = _.groupBy(settings.emails, 'key');
const emailsContent: {
[key in NotificationEnum | EngineNotificationEnum]: any;
} = {} as any;
const enumConcat = [...Object.keys(NotificationEnum), ...Object.keys(EngineNotificationEnum)];
enumConcat.forEach(async (element) => {
if (!emails[element]) {
console.error('[SETTINGS - EMAIL] Creating default', element);
newData.push(
(await DB.getInstance()).db.collection('settings').insertOne({
key: element,
type: SettingsType.emails,
body: new TranslatableComponent(),
custom: false,
fromEmail: '',
fromName: '',
replyToEmail: '',
r: ['admin'],
w: ['admin'],
d: ['admin'],
_id: uuid(),
createdAt: new Date(),
updatedAt: new Date(),
}),
);
} else emailsContent[element] = emails[element];
});
return emailsContent;
}

View File

@ -0,0 +1,106 @@
import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql';
import { BaseGraphModel } from '@seed/graphql/BaseGraphModel';
import ImageComponent, { SEOField } from '@seed/interfaces/components';
import { GetArgs, GetManyArgs } from '@seed/graphql/Request';
import { TranslatableComponent } from '@src/__components/components';
import { Permission } from '@seed/interfaces/permission';
import BaseSEOModel, { NewBaseSEOInput, EditBaseSEOInput } from './BaseSeoModel';
import CategoryModel from '@services/module-cms/functions/categories/category.model';
import { AccountTypeEnum } from '@src/accounts/account.components';
const permissions: Permission = {
c: [AccountTypeEnum.admin],
r: [AccountTypeEnum.public],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
};
@ObjectType()
export default class BaseContentModel extends BaseSEOModel {
public constructor(collectionName?: string, perm?: Permission) {
const cName = collectionName ? collectionName : 'articles';
const permission = perm ? perm : permissions;
super(cName, permission);
}
@Field()
published?: boolean;
@Field({ nullable: true })
publicationDate?: Date;
@Field({ nullable: true })
private?: boolean;
@Field(() => [String], { nullable: true })
categoryIds?: string[];
@Field(() => [CategoryModel], { nullable: 'itemsAndList' })
getCategories?: CategoryModel[];
@Field(() => [ImageComponent], { nullable: true })
downloadableRessources?: ImageComponent[];
@Field(() => TranslatableComponent, { nullable: true })
testimonials?: TranslatableComponent;
searchOptions(): string[] {
return [];
}
}
@ArgsType()
export class BaseContentArgs extends GetManyArgs {}
@InputType()
export class NewBaseContentInput extends NewBaseSEOInput implements Partial<BaseContentModel> {
@Field(() => TranslatableComponent, { nullable: true })
title?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
teaser?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [String], { nullable: true })
categoryIds?: string[];
@Field(() => [ImageComponent], { nullable: true })
downloadableRessources?: ImageComponent[];
published = false;
@Field({ nullable: true })
private?: boolean;
@Field(() => TranslatableComponent, { nullable: true })
testimonials?: TranslatableComponent;
}
@InputType()
export class EditBaseContentInput extends EditBaseSEOInput implements Partial<BaseContentModel> {
@Field(() => TranslatableComponent, { nullable: true })
title?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
teaser?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
content?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [String], { nullable: true })
categoryIds?: string[];
@Field(() => [ImageComponent], { nullable: true })
downloadableRessources?: ImageComponent[];
@Field({ nullable: true })
private?: boolean;
@Field(() => TranslatableComponent, { nullable: true })
testimonials?: TranslatableComponent;
}

View File

@ -0,0 +1,103 @@
import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql';
import { BaseGraphModel } from '@seed/graphql/BaseGraphModel';
import ImageComponent, { SEOField } from '@seed/interfaces/components';
import { GetArgs, GetManyArgs } from '@seed/graphql/Request';
import { TranslatableComponent } from '@src/__components/components';
import { Permission } from '@seed/interfaces/permission';
import { AccountTypeEnum } from '@src/accounts/account.components';
const permissions: Permission = {
c: [AccountTypeEnum.admin],
r: [AccountTypeEnum.public],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
};
@ObjectType()
export default class BaseSEOModel extends BaseGraphModel {
public constructor(collectionName?: string, perm?: Permission) {
const cName = collectionName ? collectionName : 'articles';
const permission = perm ? perm : permissions;
super({
// ...init,
collectionName: cName,
permissions: permission,
});
}
@Field(() => TranslatableComponent, { nullable: true })
title?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
teaser?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [ImageComponent], { nullable: true })
extraImages?: ImageComponent[];
@Field(() => TranslatableComponent, { nullable: true })
content?: TranslatableComponent;
@Field(() => SEOField, { nullable: true })
seo?: SEOField;
@Field({ nullable: true })
urls?: TranslatableComponent;
searchOptions(): string[] {
return [];
}
filterOptions(): string[] {
return [];
}
}
@ArgsType()
export class BaseSEOArgs extends GetManyArgs {}
@InputType()
export class NewBaseSEOInput implements Partial<BaseSEOModel> {
@Field(() => TranslatableComponent, { nullable: true })
title?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
teaser?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => TranslatableComponent, { nullable: true })
content?: TranslatableComponent;
@Field(() => SEOField, { nullable: true })
seo?: SEOField;
@Field({ nullable: true })
urls?: TranslatableComponent;
}
@InputType()
export class EditBaseSEOInput implements Partial<BaseSEOModel> {
@Field(() => TranslatableComponent, { nullable: true })
title?: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
teaser?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => TranslatableComponent, { nullable: true })
content?: TranslatableComponent;
@Field(() => SEOField, { nullable: true })
seo?: SEOField;
@Field({ nullable: true })
urls?: TranslatableComponent;
}

View File

@ -0,0 +1,107 @@
import { ObjectType, Field, ID, ArgsType, InputType, Int } from 'type-graphql';
import { BaseGraphModel } from '@seed/graphql/BaseGraphModel';
import ImageComponent, { SEOSimpleField } from '@seed/interfaces/components';
import { GetArgs, GetManyArgs } from '@seed/graphql/Request';
import { Permission } from '@seed/interfaces/permission';
import { AccountTypeEnum } from '@src/accounts/account.components';
const permissions: Permission = {
c: [AccountTypeEnum.admin],
r: [AccountTypeEnum.public],
w: [AccountTypeEnum.admin],
d: [AccountTypeEnum.admin],
};
@ObjectType()
export default class BaseSEOSimpleModel extends BaseGraphModel {
public constructor(collectionName?: string, perm?: Permission) {
const cName = collectionName ? collectionName : 'articles';
const permission = perm ? perm : permissions;
super({
// ...init,
collectionName: cName,
permissions: permission,
});
}
@Field({ nullable: true })
title?: string;
@Field({ nullable: true })
teaser?: string;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [ImageComponent], { nullable: true })
extraImages?: ImageComponent[];
@Field({ nullable: true })
content?: string;
@Field(() => SEOSimpleField, { nullable: true })
seo?: SEOSimpleField;
@Field({ nullable: true })
urls?: string;
searchOptions(): string[] {
return [];
}
filterOptions(): string[] {
return [];
}
}
@ArgsType()
export class BaseSEOArgs extends GetManyArgs {}
@InputType()
export class NewBaseSEOSimpleInput implements Partial<BaseSEOSimpleModel> {
@Field({ nullable: true })
title?: string;
@Field({ nullable: true })
teaser?: string;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [ImageComponent], { nullable: true })
extraImages?: ImageComponent[];
@Field({ nullable: true })
content?: string;
@Field(() => SEOSimpleField, { nullable: true })
seo?: SEOSimpleField;
@Field({ nullable: true })
urls?: string;
}
@InputType()
export class EditBaseSEOSimpleInput implements Partial<BaseSEOSimpleModel> {
@Field({ nullable: true })
title?: string;
@Field({ nullable: true })
teaser?: string;
@Field(() => ImageComponent, { nullable: true })
cover?: ImageComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
@Field(() => [ImageComponent], { nullable: true })
extraImages?: ImageComponent[];
@Field({ nullable: true })
content?: string;
@Field(() => SEOSimpleField, { nullable: true })
seo?: SEOSimpleField;
@Field({ nullable: true })
urls?: string;
}

View File

@ -0,0 +1,143 @@
import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql';
import { ApolloContext } from '@seed/interfaces/context';
import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { totalPublic } from '../Middleware';
export const createBasePublicResolver = <T extends ClassType<any>, ARGS extends ClassType<any>>(
domain: string,
objectTypeCls: T,
argsType: ARGS,
pub = false,
): any => {
@Resolver({ isAbstract: true })
abstract class BasePublicResolver {
/*
*/
/*
*/
@Query(() => objectTypeCls, { name: `${domain}GetOne` })
@totalPublic(pub)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
return getOneGeneric(model, id, ctx);
}
@Query(() => [objectTypeCls], { name: `${domain}GetMany` })
@totalPublic(pub)
async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new objectTypeCls();
return getManyGenericWithArgs(model, args, ctx);
}
@Query(() => Number, { name: `${domain}GetCount` })
@totalPublic(pub)
async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<number> {
const model = new objectTypeCls();
return getCountGeneric(model, args, ctx);
}
}
return BasePublicResolver;
};
export const createBaseMyResolver = <T extends ClassType<any>, ARGS extends ClassType<any>>(
domain: string,
objectTypeCls: T,
argsType: ARGS,
): any => {
@Resolver({ isAbstract: true })
abstract class BaseMyResolver {
/*
*/
/*
*/
@Query(() => objectTypeCls, { name: `${domain}GetOneMine` })
@Authorized()
async getMineOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
return getOneGeneric(model, id, ctx);
}
@Query(() => [objectTypeCls], { name: `${domain}GetManyMine` })
@Authorized()
async getMine(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new objectTypeCls();
return getManyGenericWithArgs(model, args, ctx);
}
}
return BaseMyResolver;
};
export const createBaseReadOnlyResolver = <T extends ClassType<any>, ARGS extends ClassType<any>>(
domain: string,
objectTypeCls: T,
argsType: ARGS,
auth: AccountTypeEnum[],
): any => {
@Resolver({ isAbstract: true })
abstract class BaseReadOnlyResolver {
/*
*/
/*
*/
@Query(() => objectTypeCls, { name: `${domain}GetOne` })
@Authorized(auth)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
return getOneGeneric(model, id, ctx);
}
@Query(() => [objectTypeCls], { name: `${domain}GetMany` })
@Authorized(auth)
async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new objectTypeCls();
return getManyGenericWithArgs(model, args, ctx);
}
}
return BaseReadOnlyResolver;
};

View File

@ -0,0 +1,100 @@
import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql';
import { ApolloContext } from '@seed/interfaces/context';
import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { checkOrganisation as CheckOrganisation } from '../Middleware';
export const createBaseResolver = <T extends ClassType<any>, ARGS extends ClassType<any>, NEW extends ClassType<any>, EDIT extends ClassType<any>>(
domain: string,
objectTypeCls: T,
argsType: ARGS,
newInput: NEW,
editInput: EDIT,
auth: AccountTypeEnum[],
organisationCheck = false,
): any => {
@Resolver({ isAbstract: true })
abstract class BaseResolver {
/*
*/
/*
*/
@Query(() => objectTypeCls, { name: `${domain}GetOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
return getOneGeneric(model, id, ctx);
}
@Query(() => [objectTypeCls], { name: `${domain}GetMany` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new objectTypeCls();
return getManyGenericWithArgs(model, args, ctx);
}
@Query(() => Number, { name: `${domain}GetCount` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<number> {
const model = new objectTypeCls();
return getCountGeneric(model, args, ctx);
}
/*
*/
@Mutation(() => objectTypeCls, { name: `${domain}AddOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async addOne(@Arg('input', (type) => newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onCreate) await model.onCreate(input);
return addOneGeneric(model, input, ctx);
}
@Mutation(() => objectTypeCls, { name: `${domain}EditOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async editOne(@Arg('id') id: string, @Arg('input', (type) => editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onUpdate) await model.onUpdate(input);
return editOneGeneric(model, id, input, ctx);
}
@Mutation(() => objectTypeCls, { name: `${domain}DeleteOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onDelete) await model.onDelete();
return deleteOneGeneric(model, id, ctx);
}
}
return BaseResolver;
};

View File

@ -0,0 +1,97 @@
import { Query, Arg, Resolver, Ctx, Args, Mutation, ClassType } from 'type-graphql';
import { ApolloContext } from '@seed/interfaces/context';
import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService';
import { EngineMiddleware, EngineMiddlewareInput } from '../MiddlewareV2';
export const createGenericQueryResolver = <T extends ClassType<any>, ARGS extends ClassType<any>>(init: {
domain: string;
modelName: T;
argsType: ARGS;
customMiddlewareInput: EngineMiddlewareInput;
}): any => {
@Resolver({ isAbstract: true })
abstract class BaseQueryResolver {
/*
*/
/*
*/
@Query(() => init.modelName, { name: `${init.domain}GetOne` })
@EngineMiddleware(init.customMiddlewareInput)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new init.modelName();
return getOneGeneric(model, id, ctx);
}
@Query(() => [init.modelName], { name: `${init.domain}GetMany` })
@EngineMiddleware(init.customMiddlewareInput)
async getMany(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new init.modelName();
return getManyGenericWithArgs(model, args, ctx);
}
@Query(() => Number, { name: `${init.domain}GetCount` })
@EngineMiddleware(init.customMiddlewareInput)
async getCount(@Args(() => init.argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<number> {
const model = new init.modelName();
return getCountGeneric(model, args, ctx);
}
}
return BaseQueryResolver;
};
export const createGenericMutationResolver = <T extends ClassType<any>, NEW extends ClassType<any>, EDIT extends ClassType<any>>(init: {
domain: string;
modelName: T;
newInput: NEW;
editInput: EDIT;
customMiddlewareInput: EngineMiddlewareInput;
}): any => {
@Resolver({ isAbstract: true })
abstract class BaseMutationResolver {
/*
*/
@Mutation(() => init.modelName, { name: `${init.domain}AddOne` })
@EngineMiddleware(init.customMiddlewareInput)
async addOne(@Arg('input', () => init.newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new init.modelName();
return addOneGeneric(model, input, ctx);
}
@Mutation(() => init.modelName, { name: `${init.domain}EditOne` })
@EngineMiddleware(init.customMiddlewareInput)
async editOne(@Arg('id') id: string, @Arg('input', () => init.editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new init.modelName();
return editOneGeneric(model, id, input, ctx);
}
@Mutation(() => init.modelName, { name: `${init.domain}DeleteOne` })
@EngineMiddleware(init.customMiddlewareInput)
async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new init.modelName();
return deleteOneGeneric(model, id, ctx);
}
}
return BaseMutationResolver;
};

View File

@ -0,0 +1,100 @@
import { Query, Arg, Int, Resolver, Authorized, Ctx, Args, Mutation, ClassType } from 'type-graphql';
import { ApolloContext } from '@seed/interfaces/context';
import { getOneGeneric, getManyGenericWithArgs, addOneGeneric, editOneGeneric, deleteOneGeneric, getCountGeneric } from '../BaseService';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { checkOrganisation as CheckOrganisation } from '../Middleware';
export const createBaseResolver = <T extends ClassType<any>, ARGS extends ClassType<any>, NEW extends ClassType<any>, EDIT extends ClassType<any>>(
domain: string,
objectTypeCls: T,
argsType: ARGS,
newInput: NEW,
editInput: EDIT,
auth: AccountTypeEnum[],
organisationCheck = false,
): any => {
@Resolver({ isAbstract: true })
abstract class BaseResolver {
/*
*/
/*
*/
@Query(() => objectTypeCls, { name: `${domain}GetOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
return getOneGeneric(model, id, ctx);
}
@Query(() => [objectTypeCls], { name: `${domain}GetMany` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getMany(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<T[]> {
const model = new objectTypeCls();
return getManyGenericWithArgs(model, args, ctx);
}
@Query(() => Number, { name: `${domain}GetCount` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async getCount(@Args((type) => argsType) args: ARGS, @Ctx() ctx: ApolloContext): Promise<number> {
const model = new objectTypeCls();
return getCountGeneric(model, args, ctx);
}
/*
*/
@Mutation(() => objectTypeCls, { name: `${domain}AddOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async addOne(@Arg('input', (type) => newInput) input: NEW, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onCreate) await model.onCreate(input);
return addOneGeneric(model, input, ctx);
}
@Mutation(() => objectTypeCls, { name: `${domain}EditOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async editOne(@Arg('id') id: string, @Arg('input', (type) => editInput) input: EDIT, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onUpdate) await model.onUpdate(input);
return editOneGeneric(model, id, input, ctx);
}
@Mutation(() => objectTypeCls, { name: `${domain}DeleteOne` })
@Authorized(auth)
@CheckOrganisation(organisationCheck)
async deleteOne(@Arg('id') id: string, @Ctx() ctx: ApolloContext): Promise<T> {
const model = new objectTypeCls();
if (model.onDelete) await model.onDelete();
return deleteOneGeneric(model, id, ctx);
}
}
return BaseResolver;
};

View File

@ -0,0 +1,12 @@
import { ApolloContextLoadersOnly } from '@seed/interfaces/context';
import Loaders from '@src/__indexes/__loaders';
export const createContext = async (): Promise<ApolloContextLoadersOnly> => {
return {
event: {} as any,
context: {} as any,
ctx: {
loaders: new Loaders(),
},
};
};

43
lib/seed/helpers/Error.ts Normal file
View File

@ -0,0 +1,43 @@
import { ErrorsConfig } from '@config/config';
import { SettingsCache } from '@seed/graphql/Settings';
import { ApolloError } from 'apollo-server-lambda';
import { UtilStringHelper } from './Utils.string';
type ErrorKeys = keyof typeof ErrorsConfig;
const utilStringHelper = new UtilStringHelper();
export const newError = (code: ErrorKeys, data?: any): ApolloError => {
const errorConfig = SettingsCache.getInstance().cache.errorsContent;
const message = errorConfig[code].message;
const settings = errorConfig[code].translations;
const translations = {
subject: {},
body: {},
};
if (settings) {
if (settings.body) {
for (const key in settings.body) {
if (Object.prototype.hasOwnProperty.call(settings.body, key)) {
translations.body[key] = utilStringHelper.interpolate(settings.body[key], data);
}
}
}
if (settings.subject) {
for (const key in settings.subject) {
if (Object.prototype.hasOwnProperty.call(settings.subject, key)) {
translations.subject[key] = utilStringHelper.interpolate(settings.subject[key], data);
}
}
}
}
return new ApolloError(utilStringHelper.interpolate(message, data), code.toString(), {
translations,
data,
});
};

View File

@ -0,0 +1,64 @@
import fetch from 'node-fetch';
import querystring from 'querystring';
// convert object to a query string
export const apiCall = async (url: string, method: 'POST' | 'PUT' | 'PATCH' | 'GET' | 'DELETE', headers?: any, body?: any): Promise<any> => {
const q: any = {
method: method,
headers: {
'Content-Type': 'application/json',
},
};
if (headers) q.headers = headers;
if (body) {
if (typeof body === 'string') q.body = body;
else q.body = JSON.stringify(body);
}
const response = await fetch(url, q);
const rs = await response.json();
if (!response.ok) throw { error: rs };
return rs;
};
export const fetchCall = async (init: {
url: string;
method: 'POST' | 'PUT' | 'PATCH' | 'GET' | 'DELETE';
query?: any;
headers?: any;
body?: any;
raw?: boolean;
}): Promise<any> => {
let { url } = init;
const { method, query, headers, body, raw } = init;
const q: any = {
method: method,
headers: {
'Content-Type': 'application/json',
},
};
if (query) url = url + '?' + querystring.stringify(query, undefined, undefined, { encodeURIComponent: encodeURIComponent });
if (headers) q.headers = headers;
if (body) {
if (typeof body === 'string') q.body = body;
else q.body = JSON.stringify(body);
}
console.log('fetchCall', url);
const response = await fetch(url, q);
let rs;
try {
rs = await response.json();
} catch (error) {
rs = await response.text();
}
if (!response.ok) throw { error: rs };
return rs;
};

View File

@ -0,0 +1,29 @@
import { GetArgs } from '@seed/graphql/Request';
const DEFAULT_LIMIT = 100;
const DEFAULT_OFFSET = 0;
export interface QueryPagination {
limit: number;
skip: number;
sort?: {
[k: string]: 1 | -1;
};
}
export const parsePaginationOptions = (obj?: GetArgs): QueryPagination => {
const limit = obj ? (obj.limit ? obj.limit : DEFAULT_LIMIT) : DEFAULT_LIMIT;
const skip = obj ? (obj.skip ? obj.skip : DEFAULT_OFFSET) : DEFAULT_OFFSET;
if (obj && obj.sort) {
const sort = {};
const sortArray = ('' + obj.sort).split(',');
for (let index = 0; index < sortArray.length; index++) {
const sortTmp = ('' + obj.sort).split(' ');
if (sortTmp[1].toLowerCase() === 'asc') (sort as any)[sortTmp[0]] = 1;
if (sortTmp[1].toLowerCase() === 'desc') (sort as any)[sortTmp[0]] = -1;
}
return { limit, skip, sort };
}
return { limit, skip };
};

View File

@ -0,0 +1,28 @@
import { TranslatableComponent } from '@src/__components/components';
import _ from 'lodash';
export const getAllTranslationsToString = (str: string, input: TranslatableComponent): string => {
for (const key in input) {
if (Object.prototype.hasOwnProperty.call(input, key)) {
str = str + '#' + input[key].toLowerCase();
}
}
return str;
};
export const getSearchString = (...inputs: (string | TranslatableComponent)[]): string => {
let str = '';
for (let index = 0; index < inputs.length; index++) {
const element = inputs[index];
if (_.isString(element)) str = str + '#' + element.toLowerCase();
else str = getAllTranslationsToString(str, element);
}
return str;
};
//escape regex => for special characters in email
export const escapeRegex = (regex) => {
return regex.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
}

View File

@ -0,0 +1,44 @@
import fetch from 'node-fetch';
export const graphRequest = async (query, option): Promise<any> => {
console.log('[TESTS] Query', query);
let params;
if (option.authHeader)
params = {
body: JSON.stringify({ query }),
method: 'POST',
headers: {
authorization: option.authHeader,
'Content-Type': 'application/json',
},
};
else
params = {
body: JSON.stringify({ query }),
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
};
const response = await fetch(option.url, params);
if (!response.ok) {
return new Promise((resolve, reject) => {
response
.text()
.then((text) => {
try {
console.log('[TESTS] Error', JSON.parse(text));
reject(JSON.parse(text));
} catch (err) {
console.log('[TESTS] Error', text);
reject(text);
}
})
.catch(reject);
});
}
const json = await response.json();
return json.data;
};

View File

@ -0,0 +1,14 @@
import _ from 'lodash';
import { newError } from './Error';
export const isAllThere = (listIds: string[], data: any[], modelName = 'modelName'): void => {
const ids = _.map(data, '_id');
const nIds = _.difference(listIds, ids);
if (nIds.length > 0) throw newError(4041, { id: nIds.join(','), model: modelName });
return;
};
export const hasDuplicates = (array: any[]): boolean => {
return _.uniq(array).length !== array.length;
};

View File

@ -0,0 +1,88 @@
import { DateTimeRangeComponent, AvailabilityComponent, DateRangeComponent, HourComponent } from '@seed/interfaces/components.dates';
import _ from 'lodash';
import { Interval, DateTime } from 'luxon';
export function checkIfDatesAreInAvailabilities(
dates: DateTimeRangeComponent[],
availabilities: AvailabilityComponent,
options?: { zone: string },
): boolean {
const zone = options?.zone || process.env.TIMEZONE || 'utc';
const intervalsExceptions = getExceptionsIntervalFromAvailabilities(availabilities, zone);
let isInException = false;
if (intervalsExceptions.length > 0) {
isInException = _.some(dates, (d) => {
return checkIfDateRangeIsInIntervals(d, intervalsExceptions);
});
}
const intervalDates = getDatesIntervalFromAvailabilities(availabilities, zone);
let isInDates = true;
if (intervalDates.length > 0) {
isInDates = _.every(dates, (d) => {
return checkIfDateRangeIsInIntervals(d, intervalDates);
});
}
return !isInException && isInDates;
}
function checkIfDateRangeIsInIntervals(dateR: DateTimeRangeComponent, intervals: Interval[]): boolean {
const interval = Interval.fromDateTimes(dateR.startDate, dateR.endDate);
return _.some(intervals, (i) => i.overlaps(interval));
}
function getExceptionsIntervalFromAvailabilities(availabilities: AvailabilityComponent, zone: string): Interval[] {
const exceptions = availabilities.exceptions;
const intervalsExceptions: Interval[] = [];
// Create all ezxceptions intervals
if (exceptions) {
exceptions.forEach((e) => {
createIntervalFromDatesAndHours(e.dates, zone, e.hours, intervalsExceptions);
});
}
return intervalsExceptions;
}
function getDatesIntervalFromAvailabilities(availabilities: AvailabilityComponent, zone: string): Interval[] {
const dates = availabilities.dates;
const hours = availabilities.hours;
const datesExceptions: Interval[] = [];
// Create all ezxceptions dates
if (dates) {
createIntervalFromDatesAndHours(dates, zone, hours, datesExceptions);
}
return datesExceptions;
}
function createIntervalFromDatesAndHours(dates: DateRangeComponent, zone: string, hours: HourComponent[] | undefined, datesExceptions: Interval[]) {
let dateStart = DateTime.fromJSDate(dates.startDate, { zone });
const dateEnd = DateTime.fromJSDate(dates.endDate, { zone });
if (hours) {
while (dateStart < dateEnd) {
hours.forEach((h) => {
const hStart = parseInt(h.from.split(':')[0]);
const mStart = parseInt(h.from.split(':')[1]);
const hEnd = parseInt(h.to.split(':')[0]);
const mEnd = parseInt(h.to.split(':')[1]);
datesExceptions.push(
Interval.fromDateTimes(dateStart.set({ hour: hStart, minute: mStart }), dateStart.set({ hour: hEnd, minute: mEnd })),
);
});
dateStart = dateStart.plus({ day: 1 });
}
// if all day
} else {
datesExceptions.push(Interval.fromDateTimes(DateTime.fromJSDate(dates.startDate, { zone }), DateTime.fromJSDate(dates.endDate, { zone })));
}
}

View File

@ -0,0 +1,310 @@
import {
AvailabilityComponent,
DateRangeComponent,
DateTimeRangeComponent,
HourComponent,
SlotAvailablity,
SlotComponent,
} from '@seed/interfaces/components.dates';
import _ from 'lodash';
import { DateTime, Interval } from 'luxon';
interface hoursRange {
startHours: string;
startHoursInNumber: number;
endHours: string;
endHoursInNumber: number;
}
/*
breakTime: [
['11:00', '14:00'],
['16:00', '18:00'],
],
*/
export function getNumberOfWeekendDays(dateBegin: DateTime, dateEnd: DateTime): number {
let weekendDays = 0;
let begin = dateBegin;
while (begin <= dateEnd) {
if (begin.weekday == 6 || begin.weekday == 7) weekendDays = weekendDays + 1;
begin = begin.plus({ day: 1 });
}
return weekendDays;
}
function isInTimeInterval(slotTime: DateTime, slotInterval: number, timeRange: hoursRange[]): boolean {
const startTime = slotTime;
const endTime = slotTime.plus({ minutes: slotInterval });
const startTimeInNumber = startTime.hour * 60 + startTime.minute;
const endTimeInNumber = endTime.hour * 60 + endTime.minute;
const isInInterval = timeRange.some((br) => {
return startTimeInNumber < br.endHoursInNumber && endTimeInNumber > br.startHoursInNumber;
// return (date.isSameOrAfter(startTime) && date.isSameOrBefore(endTime)) || (date.isSameOrAfter(startTime) && date.isSame(endTime));
});
return isInInterval;
}
export const getMinutesFromTimeInString = (str: string): number => {
const timeArray = str.split(':');
return parseInt(timeArray[0]) * 60 + parseInt(timeArray[1]);
};
export const getDateNoTZ = (old: Date): Date => {
const userTimezoneOffset = old.getTimezoneOffset() * 60000;
return new Date(old.getTime() - userTimezoneOffset);
};
export const slotToDatesRange = (slot: SlotComponent): DateRangeComponent[] => {
const dates: DateRangeComponent[] = [];
const { startDate, endDate, startTime, endTime } = slot;
const startTimes = startTime.split(':');
const endTimes = endTime.split(':');
let beginDate = DateTime.fromJSDate(startDate).startOf('day');
const stopDate = DateTime.fromJSDate(endDate).endOf('day');
while (beginDate < stopDate) {
dates.push({
startDate: beginDate
.set({ hour: parseInt(startTimes[0]) })
.set({ minute: parseInt(startTimes[1]) })
.toJSDate(),
endDate: beginDate
.set({ hour: parseInt(endTimes[0]) })
.set({ minute: parseInt(endTimes[1]) })
// Remove one seconds for overlapping
.minus({ second: 1 })
.toJSDate(),
});
beginDate = beginDate.plus({ day: 1 });
}
return dates;
};
export const getOneDaySlots = (input: {
day: DateTime;
dayUnavailability: DateTimeRangeComponent[];
slotInfo: string[][];
slotInterval: number;
slotDuration: number;
options?: {
zone?: string;
};
}): string[] => {
const { day, dayUnavailability, slotInfo, slotDuration, slotInterval, options } = input;
const zone = options?.zone || process.env.TIMEZONE || 'utc';
let currentDayMoment = day;
// Check if currentDay = today
const currentDayStart = currentDayMoment.toISODate();
const todayStart = DateTime.local().toISODate();
if (currentDayStart != todayStart) {
currentDayMoment = day.startOf('day');
}
const currentDayTimeInNumber = currentDayMoment.hour * 60 + currentDayMoment.minute;
console.log('current day', currentDayMoment.toISO(), currentDayMoment.weekday);
// Getting the slots for that day
const daySlot = slotInfo[day.weekday - 1];
const currentDaySlots: {
startTime: DateTime;
endTime: DateTime;
}[] = [];
if (daySlot && daySlot.length > 0) {
if (_.isArray(daySlot[0])) {
_.each(daySlot, (d) => {
currentDaySlots.push({
startTime: DateTime.fromFormat(d[0], 'HH:mm', { zone }),
endTime: DateTime.fromFormat(d[1], 'HH:mm', { zone }),
});
});
} else {
currentDaySlots.push({
startTime: DateTime.fromFormat(daySlot[0], 'HH:mm', { zone }),
endTime: DateTime.fromFormat(daySlot[1], 'HH:mm', { zone }),
});
}
}
const thisDaysUnavailabilities: {
startHours: string;
startHoursInNumber: number;
endHours: string;
endHoursInNumber: number;
}[] = getUnavailabilityOfThisDay(dayUnavailability, currentDayMoment);
const currentDateSlots: string[] = [];
console.log('thisDaysUnavailabilities', thisDaysUnavailabilities);
currentDaySlots.forEach((element) => {
let { startTime, endTime } = element;
while (startTime < endTime) {
if (
!(startTime.hour * 60 + startTime.minute < currentDayTimeInNumber) &&
!isInTimeInterval(startTime, slotDuration, thisDaysUnavailabilities) &&
startTime.hour * 60 + startTime.minute + slotDuration <= endTime.hour * 60 + endTime.minute
) {
currentDateSlots.push(startTime.toFormat('HH:mm'));
}
startTime = startTime.plus({ minutes: slotInterval });
}
});
return currentDateSlots;
};
export const getMultipleDaysSlots = (input: {
datesWanted: DateRangeComponent;
datesUnavailable: DateTimeRangeComponent[];
slotInterval: number;
slotInfo: string[][];
slotDuration: number;
options?: {
zone?: string;
};
}): SlotAvailablity[] => {
const returnSlots: SlotAvailablity[] = [];
const { datesWanted, datesUnavailable, slotInterval, slotInfo, slotDuration, options } = input;
const { startDate, endDate } = datesWanted;
const zone = options?.zone || process.env.TIMEZONE || 'utc';
let beginDate = DateTime.fromJSDate(startDate, { zone });
const stopDate = DateTime.fromJSDate(endDate, { zone });
// Two loops
// Looping into the wanted dates and keeping an index for the slotInfo
while (beginDate < stopDate) {
// Get the slot info from the array of array
const slots = getOneDaySlots({
day: beginDate,
dayUnavailability: datesUnavailable,
slotInfo,
slotInterval,
slotDuration,
options: { zone },
});
// Adding it to the array
returnSlots.push({
date: beginDate.toJSDate(),
slots,
});
beginDate = beginDate.plus({ day: 1 }).startOf('day');
}
return returnSlots;
};
export function getUnavailabilityOfThisDay(dayUnavailability: DateTimeRangeComponent[], currentDayMoment: DateTime) {
const thisDaysUnavailabilities: {
startHours: string;
startHoursInNumber: number;
endHours: string;
endHoursInNumber: number;
}[] = [];
dayUnavailability.forEach((element) => {
const startDate = element.startDate;
const endDate = element.endDate;
// If there are some reservations in that day or in multiple days
const isSameThanStartDate = currentDayMoment.hasSame(startDate, 'day');
const isSameThanEndDate = currentDayMoment.hasSame(endDate, 'day');
const startHours = startDate.toFormat('HH:mm');
const endHours = endDate.toFormat('HH:mm');
const starts = startHours.split(':');
const ends = endHours.split(':');
const startDateDay = startDate.startOf('day');
const endDateDay = startDate.endOf('day');
const startHoursInNumber = parseInt(starts[0]) * 60 + parseInt(starts[1]);
const endHoursInNumber = parseInt(ends[0]) * 60 + parseInt(ends[1]);
// If the reservation only last a day
if (isSameThanStartDate && isSameThanEndDate) {
thisDaysUnavailabilities.push({
startHours,
endHours,
startHoursInNumber,
endHoursInNumber,
});
}
// If the reservation last multiple days
else if (currentDayMoment >= startDateDay && currentDayMoment <= endDateDay) {
// check if start date
if (isSameThanStartDate)
thisDaysUnavailabilities.push({
startHours,
endHours: '23:59',
startHoursInNumber,
endHoursInNumber: 23 * 60 + 59,
});
else if (isSameThanEndDate)
thisDaysUnavailabilities.push({
startHours: '00:00',
endHours,
startHoursInNumber: 0,
endHoursInNumber,
});
else
thisDaysUnavailabilities.push({
startHours: '00:00',
endHours: '23:59',
startHoursInNumber: 0,
endHoursInNumber: 23 * 60 + 59,
});
}
});
return thisDaysUnavailabilities;
}
export function isTimeSlotInDates(dates: DateTimeRangeComponent[], startTime: string, endTime: string) {
const startTimesInMinutes = getMinutesFromTimeInString(startTime);
const endTimesInMinutes = getMinutesFromTimeInString(endTime);
// Check for each date if the minutes are in it
return dates.some((br) => {
const brStartTimeInMinutes = br.startDate.hour * 60 + br.startDate.minute;
const brEndTimeInMinutes = br.endDate.hour * 60 + br.endDate.minute;
return startTimesInMinutes < brEndTimeInMinutes && endTimesInMinutes > brStartTimeInMinutes;
});
}
export function converDateRangeToDateTime(
dates: DateRangeComponent[],
options?: {
zone?: string;
},
): DateTimeRangeComponent[] {
const results: DateTimeRangeComponent[] = [];
const zone = options?.zone || process.env.TIMEZONE || 'utc';
dates.forEach((element) => {
results.push({
startDate: DateTime.fromJSDate(element.startDate, { zone }),
endDate: DateTime.fromJSDate(element.endDate, { zone }),
});
});
return results;
}

View File

@ -0,0 +1,38 @@
import _ from 'lodash';
export class UtilStringHelper {
interpolate = function(
string: string,
data: any,
replace?: {
withEol?: boolean;
},
): any {
let finalString = string;
if (!replace || !replace.withEol) finalString = finalString = finalString.replace(/\s{2,}/g, '');
// finalString.replace(/(?:\r\n|\r|\n)/g, '<br>');
// // else
// finalString = finalString.replace(/\s{2,}/g, ''); // finalString.replace(/(?:\r\n|\r|\n)/g, '');
try {
const compiled = _.template(finalString);
if (finalString.includes('${data.')) return compiled({ data });
return compiled(data);
} catch (error) {
console.error(error);
const names = Object.keys(data);
const vals = Object.values(data) as any;
for (let index = 0; index < names.length; index++) {
finalString = finalString.replace('${' + names[index] + '}', vals[index]);
}
return finalString;
// return new Function(...names, "return String.raw`${this}`")(...vals);
}
};
removeTabAndReturn = function(string: string): string {
return string.replace(/[\n\t]+/g, '');
};
}

64
lib/seed/helpers/Utils.ts Normal file
View File

@ -0,0 +1,64 @@
import _ from 'lodash';
import { UtilStringHelper } from './Utils.string';
const StringHelper = new UtilStringHelper();
export const interpolate = StringHelper.interpolate;
export const sleep = async (ms: number): Promise<void> => {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
};
export const promiseAll = async (promises: Promise<any>[]): Promise<any> => {
const results = await Promise.allSettled(promises);
results.forEach((element) => {
if (element.status == 'rejected') console.error('[ASYNC - ERROR]', element.reason);
});
console.log('[PROMISE ALL - DONE]');
return results;
};
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
const isNullBlankOrUndefined = function(o) {
return typeof o === 'undefined' || o == null || o === '';
};
/**
* Deep diff between two object, using lodash
* @param {Object} object Object compared
* @param {Object} base Object to compare with
* @return {Object} Return a new object who represent the diff
*/
export const differenceBetweenObject = (object, base, ignoreBlanks = false): any => {
if (!_.isObject(object) || _.isDate(object) || Array.isArray(object)) return object; // special case dates & array
return _.transform(object as any, (result, value, key) => {
if (!_.isEqual(value, base[key])) {
if (ignoreBlanks && isNullBlankOrUndefined(value) && isNullBlankOrUndefined(base[key])) return;
result[key] = _.isObject(value) && _.isObject(base[key]) ? differenceBetweenObject(value, base[key]) : value;
}
});
};
export const generateCodeNumber = () => {
return Math.floor(100000 + Math.random() * 900000);
};
export const clog = (data: any): void => {
console.log(JSON.stringify(data, null, 2));
};
export const nullToUndefined = (value: any): any | undefined => {
if (_.isString(value) || _.isBoolean(value) || _.isDate(value) || _.isFinite(value) || _.isInteger(value)) return value;
if (_.isArray(value)) return (value as any[]).map(nullToUndefined);
if (_.isObject(value)) return _.mapValues(value as any, nullToUndefined);
if (value === null) return undefined;
return value;
};

View File

@ -0,0 +1,114 @@
import { InputType, Int, Field, ArgsType, ObjectType } from 'type-graphql';
import { IsDate, IsMilitaryTime } from 'class-validator';
import { DateTime } from 'luxon';
@ObjectType()
@InputType('ScheduleInput')
export class ScheduleComponent {
@Field(() => [[String]], { nullable: true })
mon?: string[][];
@Field(() => [[String]], { nullable: true })
tue?: string[][];
@Field(() => [[String]], { nullable: true })
wed?: string[][];
@Field(() => [[String]], { nullable: true })
thu?: string[][];
@Field(() => [[String]], { nullable: true })
fri?: string[][];
@Field(() => [[String]], { nullable: true })
sat?: string[][];
@Field(() => [[String]], { nullable: true })
sun?: string[][];
}
@ObjectType()
@InputType('HoursInput')
export class HourComponent {
@Field()
from: string;
@Field()
to: string;
}
@ObjectType()
@InputType('DateRangeComponentInput')
@ArgsType()
export class DateRangeComponent {
@Field()
@IsDate()
startDate: Date;
@Field()
@IsDate()
endDate: Date;
}
export class DateTimeRangeComponent {
startDate: DateTime;
endDate: DateTime;
}
@ObjectType()
@InputType('SlotAvailablityInput')
export class SlotAvailablity {
@Field()
@IsDate()
date: Date;
@Field(() => [String])
slots: string[];
}
@ArgsType()
export class SlotAvailablityRequest extends DateRangeComponent {
@Field(() => Int)
interval: number;
@Field(() => Int)
duration: number;
}
@ObjectType()
@InputType('SlotComponentInput')
export class SlotComponent {
@Field()
@IsDate()
startDate: Date;
@Field()
@IsDate()
endDate: Date;
@Field()
@IsMilitaryTime()
startTime: string;
@Field()
@IsMilitaryTime()
endTime: string;
}
@ObjectType()
@InputType('BaseAvailabilityComponentInput')
export class BaseAvailabilityComponent {
@Field(() => DateRangeComponent)
dates: DateRangeComponent;
@Field(() => [HourComponent], { nullable: true })
hours?: HourComponent[];
}
@ObjectType()
@InputType('DateExceptionComponentInput')
export class DateExceptionComponent extends BaseAvailabilityComponent {
@Field({ nullable: true })
allDay?: boolean;
}
@ObjectType()
@InputType('AvailabilityInput')
export class AvailabilityComponent extends BaseAvailabilityComponent {
@Field(() => [DateExceptionComponent], { nullable: true })
exceptions?: DateExceptionComponent[];
@Field({ nullable: true })
noWeekend?: boolean;
}

View File

@ -0,0 +1,99 @@
/*
*/
/*
*/
export const seedErrorConfig = {
/* HTTP like errors */
404: 'The document was not found',
4040: 'The ressource ${obj.ressourceId} for the model ${obj.ressourceModel} does not exists',
4041: 'The id "${id}" for the model "${model}" was not found',
/* ************* */
/* General & DB */
/* ************* */
1000: 'General database error',
1001: 'The document was not deleted',
1002: 'This action is not allowed',
/* ************* */
/* Permissions - Accounts & Organisation */
/* ************* */
// Accounts
2000: 'Your permissions level are not allowed',
2002: 'Authorization headers are required for this action',
2003: 'The account is already registered',
2004: 'We have no records of your account in our database',
2005: 'There was an error creating your account',
2006: 'The password confirmation you entered is incorrect',
20060: 'The old confirmation you entered is incorrect',
2007: 'There was an error updating your permission',
// Firebase
2010: 'Your login failed. Please verify your credentials',
2011: 'We had issues with your token. Please retry',
2012: 'We had issues creating your account. Please retry',
2013: 'There was an issue with the autentication provider',
2015: "Linking your account didn't work. Please contact administrator",
// Organisations
2100: 'Your permissions on the organisation level are not allowed',
2101: "Your account doesn't operate in that organisation",
2102: 'Organisation headers are required for this action',
// Api keys
2200: 'Your api keys are not in our system',
2202: 'Api keys are required for this action',
// Codes
2300: 'The code you entered does not exists',
2301: 'The code you entered has expired',
2302: 'Too many attempts, please retry in 15 mins',
/* ************* */
/* Security */
/* ************* */
2400: 'You need to complete security protocols before doing that action',
/* ************* */
/* DataValidation */
/* ************* */
3000: 'There are some inputs missing',
3001: 'This is a validation error',
3005: 'There was an error with the errors range time',
// -- Forms
3100: 'Items are required for that form type',
3101: 'The form has duplicate name entries',
3102: "The section '${sName}' for this form doesn't exists",
3103: "The question '${qName}' for the section '${sName}' for this form doesn't exists",
3104: "The answers of the section '${sectionName}' doesn't exists",
31040: "The answer ${fieldName} of the section '${sectionName}' doesn't exists",
3105: 'The answer "${name}" for this field "${formName}" is not acceptable',
3106: "The tab ${tabName} for this form doesn't exists",
3107: 'Please select a section to answer',
3108: 'Some input are missing, ${name} for section ${sectionName}',
3109: 'Data integrity breach, ${name} should be ${type}',
3110: 'Only one answer (key) please',
// -- Files
3200: 'System cannot read the extension of the file',
3201: 'The file extensions are not allowed - allowed : ${allowedMime}',
};

View File

@ -0,0 +1,192 @@
import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql';
import { Max, IsNotEmpty } from 'class-validator';
import { registerEnumType } from 'type-graphql';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { TranslatableComponent } from '@src/__components/components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { IsRefExist } from '@seed/engine/decorators/db.guard';
import { ScheduleComponent } from './components.dates';
/*
*/
/*
*/
/*
* ADDRESS
*/
@InputType('LocComponentInput')
@ObjectType()
export class LocComponent {
@Field()
type: string;
@Field(() => [Number])
coordinates: number[];
}
@InputType('AddressInput')
@ObjectType()
export class AddressComponent {
@Field({ nullable: true })
number?: string;
@Field({ nullable: true })
street?: string;
@Field({ nullable: true })
streetBis?: string;
@Field({ nullable: true })
floor?: string;
@Field({ nullable: true })
box?: string;
@Field({ nullable: true })
zip?: string;
@Field({ nullable: true })
state?: string;
@Field()
city: string;
@Field()
country: string;
returnFullAddress() {
return `${this.number} ${this.street}, ${this.zip} ${this.city} ${this.country}`
}
}
@InputType('AddressStrictInput')
@ObjectType()
export class AddressStrictComponent {
@Field()
number: string;
@Field()
street: string;
@Field()
zip: string;
@Field()
city: string;
@Field()
country: string;
@Field({ nullable: true })
streetBis?: string;
@Field({ nullable: true })
floor?: string;
@Field({ nullable: true })
box?: string;
@Field({ nullable: true })
state?: string;
returnFullAddress() {
return `${this.number} ${this.street}, ${this.zip} ${this.city} ${this.country}`
}
}
@InputType('PlaceInput')
@ObjectType()
export class PlaceComponent {
@Field()
placeId: string;
@Field({ nullable: true })
address: AddressComponent;
@Field(() => LocComponent)
loc: LocComponent;
@Field()
formattedAddress: string;
}
@InputType('PlaceComponentOptionnalInput')
@ObjectType()
export class PlaceComponentOptionnal {
@Field({ nullable: true })
placeId?: string;
@Field(() => AddressComponent, { nullable: true })
address: AddressComponent;
@Field(() => LocComponent, { nullable: true })
loc?: LocComponent;
@Field({ nullable: true })
formattedAddress?: string;
}
@InputType('PlaceComponentOptionnalWithAddressInput')
@ObjectType()
export class PlaceComponentOptionnalWithAddress {
@Field({ nullable: true })
placeId?: string;
@Field(() => AddressStrictComponent)
address: AddressStrictComponent;
@Field(() => LocComponent, { nullable: true })
loc?: LocComponent;
@Field({ nullable: true })
formattedAddress?: string;
}
@InputType('GeolocSearchInput')
@ObjectType()
@ArgsType()
export class GeolocSearchComponent {
@Field()
longitude: number;
@Field()
latitude: number;
@Max(10000)
@Field(() => Int, { nullable: true })
radius = 5000;
}
@InputType('GeolocAddressSearchInput')
@ObjectType()
export class GeolocAddressSearchComponent {
@Field()
formattedAddress: string;
@Max(10000)
@Field(() => Int, { nullable: true })
radius = 5000;
}
@InputType('GeolocPlaceSearchInput')
@ObjectType()
export class GeolocPlaceSearchComponent {
@Field()
placeId: string;
@Max(10000)
@Field(() => Int, { nullable: true })
radius = 5000;
}
@ObjectType()
export class GeolocDistComponent {
@Field({ nullable: true })
calculated?: number;
@Field(() => LocComponent, { nullable: true })
location?: LocComponent;
}
@InputType('PlaceContactInformationInput')
@ObjectType()
export class PlaceContactInformation {
@Field()
phoneNumber: string;
@Field()
email: string;
@Field(() => ScheduleComponent)
openingHours: ScheduleComponent;
}

View File

@ -0,0 +1,24 @@
/*
*/
export enum EngineNotificationEnum {
'emailCodeSignIn' = 'emailCodeSignIn',
'magicLink' = 'magicLink',
'resetPassword' = 'resetPassword',
'adminAccountAddOne' = 'adminAccountAddOne',
}
/*
*/

View File

@ -0,0 +1,46 @@
import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql';
import { Max, IsDate, IsMilitaryTime, IsNotEmpty } from 'class-validator';
import { registerEnumType } from 'type-graphql';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { TranslatableComponent } from '@src/__components/components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { IsRefExist } from '@seed/engine/decorators/db.guard';
import { DateTime } from 'luxon';
/*
*/
/*
*/
@ObjectType()
@InputType('GenericValueLabelComponentInput')
export class GenericValueLabelComponent {
@Field()
value: string;
@Field()
label: TranslatableComponent;
}
@ObjectType()
@InputType('GenericTitleSubtitleComponentInput')
export class GenericTitleSubtitleComponent {
@Field(() => TranslatableComponent, { nullable: true })
title: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
subtitle: TranslatableComponent;
}

View File

@ -0,0 +1,318 @@
import { InputType, Int, Field, ArgsType, ObjectType, Args } from 'type-graphql';
import { Max, IsNotEmpty } from 'class-validator';
import { registerEnumType } from 'type-graphql';
import { AccountTypeEnum } from '@src/accounts/account.components';
import { TranslatableComponent } from '@src/__components/components';
import { ModelCollectionEnum } from '@src/__indexes/__collections';
import { IsRefExist } from '@seed/engine/decorators/db.guard';
/*
*/
export enum SettingsType {
emails = 'emails',
ac = 'ac',
env = 'env',
instagram = 'instagram',
errors = 'errors',
others = 'others',
}
registerEnumType(SettingsType, {
name: 'SettingsType',
});
export enum AccountGenderEnum {
m = 'm',
f = 'f',
other = 'other',
}
registerEnumType(AccountGenderEnum, {
name: 'AccountGenderEnum',
});
export enum deviceOs {
ios = 'ios',
android = 'android',
}
registerEnumType(deviceOs, {
name: 'deviceOs',
});
/*
* Permissions
*/
export enum PermissionType {
r = 'r',
w = 'w',
d = 'd',
}
registerEnumType(PermissionType, {
name: 'PermissionType',
});
/*
* Date stuff
*/
export enum RangeType {
strict = 'strict', // IS << A - B >> IE
intersect = 'intersect', // IS << A - IE << B ||
intersectLarge = 'intersectLarge',
included = 'included', // IS << A - B >> IE
}
registerEnumType(RangeType, {
name: 'RangeType',
});
export enum DateTabType {
today = 'today',
tomorrow = 'tomorrow',
upcomming = 'upcomming',
past = 'past',
}
registerEnumType(DateTabType, {
name: 'DateTabType',
});
export enum SizeTypeEnum {
cm = 'cm',
inch = 'inch',
}
registerEnumType(SizeTypeEnum, {
name: 'SizeTypeEnum',
});
export enum WeightTypeEnum {
lbs = 'lbs',
g = 'g',
}
registerEnumType(WeightTypeEnum, {
name: 'WeightTypeEnum',
});
export enum QualityEnum {
verygood = 'verygood',
good = 'good',
neutral = 'neutral',
poor = 'poor',
verypoor = 'verypoor',
}
registerEnumType(QualityEnum, {
name: 'QualityEnum',
});
export enum StatusEnum {
draft = 'draft',
processing = 'processing',
validated = 'validated',
cancelled = 'cancelled',
expired = 'expired',
}
registerEnumType(StatusEnum, {
name: 'StatusEnum',
});
// export enum IntervalEnum {
// '15' = '15',
// '30' = '30',
// '45' = '45',
// '60' = '60',
// '90' = '90',
// '120' = '120',
// }
// registerEnumType(IntervalEnum, {
// name: 'IntervalEnum',
// });
/*
*/
@ObjectType()
export class EnginePathComponent {
@Field(() => ModelCollectionEnum)
@IsNotEmpty()
ressourceModel: ModelCollectionEnum | string;
@Field()
@IsNotEmpty()
// @IsRefExist()
ressourceId: string;
}
/*
* AUTH
*
*/
@ObjectType()
export class FirebaseTokenResult {
@Field()
localId: string;
@Field({ nullable: true })
email?: string;
@Field({ nullable: true })
displayName?: string;
@Field()
idToken: string;
@Field({ nullable: true })
registered?: boolean;
@Field()
refreshToken: string;
@Field()
expiresIn: string;
}
/*
* MISC
*/
@InputType('PositionInInput')
export class PositionInInterface {
@Field()
_id: string;
@Field(() => Int)
order: number;
}
[];
// @ObjectType()
// @InputType('TranslatableInput')
// export class TranslatableComponent {
// @Field()
// en: string;
// }
/*
* RESULTS
*/
@ObjectType('IdResult')
export class IdResult {
@Field()
_id: string;
}
@ObjectType()
export class SimpleResult {
@Field()
message: string;
}
/*
* DEVICES
*/
@ObjectType()
@InputType('DeviceInput')
export class DeviceComponent {
@Field()
uid: string;
@Field(() => deviceOs)
os: deviceOs;
@Field()
pushToken: string;
@Field()
deviceName: string;
}
/*
* IMAGES
*/
@ObjectType()
@InputType('ImageInput')
export default class ImageComponent {
@Field({ nullable: true })
title?: string;
@Field({ nullable: true })
fileType?: string;
@Field()
large: string;
@Field({ nullable: true })
medium?: string;
@Field({ nullable: true })
small?: string;
}
@ObjectType()
@InputType('NewPermissionInputC')
export class NewPermissionInput {
@Field(() => PermissionType)
permissionType: PermissionType;
@Field(() => AccountTypeEnum)
permission: AccountTypeEnum;
}
@ObjectType()
@InputType('SEOInput')
export class SEOField {
@Field(() => TranslatableComponent)
title: TranslatableComponent;
@Field(() => TranslatableComponent)
description: TranslatableComponent;
@Field(() => TranslatableComponent, { nullable: true })
keywords?: TranslatableComponent;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
}
@ObjectType()
@InputType('SEOSimpleInput')
export class SEOSimpleField {
@Field()
title: string;
@Field()
description: string;
@Field({ nullable: true })
keywords?: string;
@Field(() => ImageComponent, { nullable: true })
thumbnail?: ImageComponent;
}
@ObjectType()
@InputType('SizeComponentInput')
export class SizeComponent {
@Field()
unit: SizeTypeEnum;
@Field()
value: number;
}
@ObjectType()
@InputType('WeightComponentInput')
export class WeightComponent {
@Field()
unit: WeightTypeEnum;
@Field()
value: number;
}
@ObjectType()
@InputType('CustomTagsInput')
export class CustomTagsInterface {
@Field()
key: string;
@Field()
value: string;
}

Some files were not shown because too many files have changed in this diff Show More