Compare commits

...

8 Commits

Author SHA1 Message Date
Ryukemeister fcabcf7266 update post request 2023-10-27 18:11:54 +05:30
Ryukemeister 25f4273124 add endpoints for booking post request 2023-10-26 18:50:18 +05:30
Ryukemeister 630a1f38a1 add routes for booking 2023-10-26 13:05:07 +05:30
Ryukemeister 035191125e cleanup 2023-10-25 15:11:04 +05:30
Ryukemeister 9cd771f7e7 clenup 2023-10-25 15:08:37 +05:30
Ryukemeister ee60f5611d add routes for schedule, eventType and teams 2023-10-25 15:04:03 +05:30
Morgan Vernay 95c2bf9b54 wip poc 2023-10-23 17:36:41 +03:00
Morgan Vernay a77839f0e5 poc: create initial skeleton of api 2023-10-23 12:43:57 +03:00
44 changed files with 9857 additions and 0 deletions

65
apps/platform/api/.gitignore vendored Normal file
View File

@ -0,0 +1,65 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# 0x
profile-*
# mac files
.DS_Store
# vim swap files
*.swp
# webstorm
.idea
# vscode
.vscode
*code-workspace
# clinic
profile*
*clinic*
*flamegraph*
# generated code
examples/typescript-server.js
test/types/index.js
# compiled app
dist

View File

@ -0,0 +1,11 @@
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'all',
bracketSpacing: true,
jsxBracketSameLine: false,
arrowParens: 'always',
};

5
apps/platform/api/.taprc Normal file
View File

@ -0,0 +1,5 @@
test-env: [
TS_NODE_FILES=true,
TS_NODE_PROJECT=./test/tsconfig.json
]
timeout: 120

View File

@ -0,0 +1,23 @@
# Getting Started with [Fastify-CLI](https://www.npmjs.com/package/fastify-cli)
This project was bootstrapped with Fastify-CLI.
## Available Scripts
In the project directory, you can run:
### `npm run dev`
To start the app in dev mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
### `npm start`
For production mode
### `npm run test`
Run the test cases.
## Learn More
To learn Fastify, check out the [Fastify documentation](https://www.fastify.io/docs/latest/).

9076
apps/platform/api/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
{
"name": "api",
"version": "1.0.0",
"description": "This project was bootstrapped with Fastify-CLI.",
"main": "app.ts",
"directories": {
"test": "test"
},
"scripts": {
"test": "npm run build:ts && tsc -p test/tsconfig.json && tap --ts \"test/**/*.test.ts\"",
"start": "npm run build:ts && fastify start -l info dist/app.js",
"build:ts": "tsc",
"watch:ts": "tsc -w",
"dev": "npm run build:ts && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch:ts\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@fastify/autoload": "^5.0.0",
"@fastify/sensible": "^5.0.0",
"fastify": "^4.0.0",
"fastify-cli": "^5.8.0",
"fastify-plugin": "^4.0.0",
"kysely": "^0.26.3",
"pg": "^8.11.3",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/node": "^20.4.4",
"@types/pg": "^8.10.7",
"@types/tap": "^15.0.5",
"concurrently": "^7.0.0",
"fastify-tsconfig": "^1.0.1",
"kysely-codegen": "^0.11.0",
"prisma-kysely": "^1.7.1",
"tap": "^16.1.0",
"ts-node": "^10.4.0",
"typescript": "^5.2.2"
}
}

View File

@ -0,0 +1,4 @@
import type { Selectable } from 'kysely';
import type { ApiKey } from 'kysely-codegen';
export type ApiKeyType = Selectable<ApiKey>;

View File

@ -0,0 +1,4 @@
import { db } from '../../lib/db';
export const getApiKeyByHash = (hash: string) =>
db.selectFrom('ApiKey').selectAll().where('hashedKey', '=', hash).executeTakeFirst();

View File

@ -0,0 +1,11 @@
import { hashAPIKey } from '../../utils/apiKey';
import type { ApiKeyType } from '../entities/types';
export const getApiKeyService = async (
{ getApiKeyByHash }: { getApiKeyByHash: (key: string) => Promise<ApiKeyType | undefined> },
apiKey: string,
) => {
const strippedApiKey = `${apiKey}`.replace(process.env.API_KEY_PREFIX || 'cal_', '');
const hashedKey = hashAPIKey(strippedApiKey);
return await getApiKeyByHash(hashedKey);
};

View File

@ -0,0 +1,32 @@
import type { AutoloadPluginOptions } from '@fastify/autoload';
import AutoLoad from '@fastify/autoload';
import type { FastifyPluginAsync, FastifyServerOptions } from 'fastify';
import { join } from 'path';
export interface AppOptions extends FastifyServerOptions, Partial<AutoloadPluginOptions> {}
// Pass --options via CLI arguments in command to enable these options.
const options: AppOptions = {};
const app: FastifyPluginAsync<AppOptions> = async (fastify, opts): Promise<void> => {
// Place here your custom code!
// Do not touch the following lines
// This loads all plugins defined in plugins
// those should be support plugins that are reused
// through your application
void fastify.register(AutoLoad, {
dir: join(__dirname, 'plugins'),
options: opts,
});
// This loads all plugins defined in routes
// define your routes in one of these
void fastify.register(AutoLoad, {
dir: join(__dirname, 'routes'),
options: opts,
});
};
export default app;
export { app, options };

View File

@ -0,0 +1,4 @@
import { db } from '../../lib/db';
export const getUserAvailability = (userId: number) =>
db.selectFrom('Availability').selectAll().where('userId', '=', userId).executeTakeFirst();

View File

@ -0,0 +1,44 @@
import { db } from '../../lib/db';
export const createBooking = async ({
name,
email,
timeZone,
}: {
name: string;
email: string;
timeZone: string;
}) => {
await db
.insertInto('Attendee')
.values({
id: 6969,
email: 'sahalrajiv690000000@gmail.com',
name: 'Ryukeeeeee',
timeZone: 'Europe/London',
bookingId: 16,
locale: 'en',
})
.execute();
// db.insertInto('Booking')
// .values({
// responses: {
// email: '{{$randomExampleEmail}}',
// name: '{{$randomFullName}}',
// notes: '{{$randomCatchPhrase}}',
// guests: [],
// phone: '{{$randomPhoneNumber}}',
// },
// start: '{{start}}',
// end: '{{end}}',
// eventTypeId: 3,
// timeZone: 'America/Mazatlan',
// language: 'en',
// location: '',
// metadata: {},
// hasHashedBookingLink: false,
// hashedLink: null,
// })
// .executeTakeFirst();
};

View File

@ -0,0 +1,5 @@
import { db } from '../../lib/db';
export const getAllBooking = async () => {
return await db.selectFrom('Booking').selectAll().execute();
};

View File

@ -0,0 +1,5 @@
import { db } from '../../lib/db';
export const getBookingById = async (id: number) => {
return await db.selectFrom('Booking').selectAll().where('id', '=', id).executeTakeFirst();
};

View File

@ -0,0 +1,5 @@
import { db } from '../../lib/db';
export const getAllEventTypes = async () => {
return await db.selectFrom('EventType').selectAll().execute();
};

View File

@ -0,0 +1,9 @@
import { db } from '../../lib/db';
export const getEventTypeById = async (id: number) => {
return await db
.selectFrom('EventType')
.select(['userId', 'title', 'description', 'length', 'hidden'])
.where('id', '=', id)
.executeTakeFirst();
};

View File

@ -0,0 +1,21 @@
import { Kysely, PostgresDialect } from 'kysely';
import type { DB } from 'kysely-codegen';
// this is the Database interface we defined earlier
import { Pool } from 'pg';
export const db = (function () {
const dialect = new PostgresDialect({
pool: new Pool({
database: 'calendso',
host: 'localhost',
user: 'postgres',
password: '',
ssl: false,
port: 5450,
max: 10,
}),
});
return new Kysely<DB>({
dialect,
});
})();

View File

@ -0,0 +1,16 @@
# Plugins Folder
Plugins define behavior that is common to all the routes in your
application. Authentication, caching, templates, and all the other cross
cutting concerns should be handled by plugins placed in this folder.
Files in this folder are typically defined through the
[`fastify-plugin`](https://github.com/fastify/fastify-plugin) module,
making them non-encapsulated. They can define decorators and set hooks
that will then be used in the rest of your application.
Check out:
* [The hitchhiker's guide to plugins](https://www.fastify.io/docs/latest/Guides/Plugins-Guide/)
* [Fastify decorators](https://www.fastify.io/docs/latest/Reference/Decorators/).
* [Fastify lifecycle](https://www.fastify.io/docs/latest/Reference/Lifecycle/).

View File

@ -0,0 +1,40 @@
import { getApiKeyByHash } from '../api-keys/repository/getApiKeyByHash';
import { getApiKeyService } from '../api-keys/services/getApiKey';
import { getUserById } from '../users/repository/getUserById';
import { dateNotInPast } from '../utils/date';
import fp from 'fastify-plugin';
export default fp(async (fastify, opts) => {
fastify.addHook<{ Querystring: { apiKey?: string } }>('preHandler', async (request, reply) => {
// Check for authentication here
const { apiKey } = request.query;
if (!apiKey) {
return reply.code(401).send({ error: 'Unauthorized' });
}
const key = await getApiKeyService({ getApiKeyByHash }, apiKey);
if (!key) {
return reply.code(401).send({ error: 'Your apiKey is not valid' });
}
if (key.expiresAt && dateNotInPast(key.expiresAt)) {
return reply.code(401).send({ error: 'This apiKey is expired' });
}
if (!key.userId) return reply.code(404).send({ error: 'No user found for this apiKey' });
const user = await getUserById(key.userId);
if (user) {
request.user = { ...user, id: key.userId, isAdmin: user.role === 'ADMIN' };
}
return;
});
});
declare module 'fastify' {
export interface FastifyRequest {
user: Awaited<ReturnType<typeof getUserById>> & { isAdmin: boolean; id: number };
}
}

View File

@ -0,0 +1,12 @@
import type { SensibleOptions } from '@fastify/sensible';
import sensible from '@fastify/sensible';
import fp from 'fastify-plugin';
/**
* This plugins adds some utilities to handle http errors
*
* @see https://github.com/fastify/fastify-sensible
*/
export default fp<SensibleOptions>(async (fastify) => {
fastify.register(sensible);
});

View File

@ -0,0 +1,20 @@
import fp from 'fastify-plugin';
export interface SupportPluginOptions {
example?: string;
}
// The use of fastify-plugin is required to be able
// to export the decorators to the outer scope
export default fp<SupportPluginOptions>(async (fastify, opts) => {
fastify.decorate('someSupport', function () {
return 'hugs';
});
});
// When using .decorate you have to specify added properties for Typescript
declare module 'fastify' {
export interface FastifyInstance {
someSupport(): string;
}
}

View File

@ -0,0 +1,24 @@
# Routes Folder
Routes define endpoints within your application. Fastify provides an
easy path to a microservice architecture, in the future you might want
to independently deploy some of those.
In this folder you should define all the routes that define the endpoints
of your web application.
Each service is a [Fastify
plugin](https://www.fastify.io/docs/latest/Reference/Plugins/), it is
encapsulated (it can have its own independent plugins) and it is
typically stored in a file; be careful to group your routes logically,
e.g. all `/users` routes in a `users.js` file. We have added
a `root.js` file for you with a '/' root added.
If a single file become too large, create a folder and add a `index.js` file there:
this file must be a Fastify plugin, and it will be loaded automatically
by the application. You can now add as many files as you want inside that folder.
In this way you can create complex routes within a single monolith,
and eventually extract them.
If you need to share functionality between routes, place that
functionality into the `plugins` folder, and share it via
[decorators](https://www.fastify.io/docs/latest/Reference/Decorators/).

View File

@ -0,0 +1,20 @@
import { getAllBooking } from '../../booking/repository/getAllBooking';
import { getBookingById } from '../../booking/repository/getBookingById';
import type { FastifyPluginAsync } from 'fastify';
const get: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async (request, reply) => {
const booking = getAllBooking();
return booking;
});
fastify.get<{ Params: { id: number } }>('/:id', async (request, reply) => {
const { id } = request.params;
const booking = getBookingById(id);
return booking;
});
};
export default get;

View File

@ -0,0 +1,24 @@
import { createBooking } from '../../booking/repository/createBooking';
import type { FastifyPluginAsync } from 'fastify';
const schema = {
body: {
properties: {
username: { type: 'string' },
},
required: ['username'],
},
};
const post: FastifyPluginAsync = async (fastify, opts) => {
fastify.post<{ Params: { id: number } }>('/post/:id', { schema }, async (request, reply) => {
const { id } = request.params;
const book = createBooking({ id });
console.log(book);
return 'Booking created successfully';
});
};
export default post;

View File

@ -0,0 +1,24 @@
import { getAllEventTypes } from '../../eventType/repository/getAllEventTypes';
import { getEventTypeById } from '../../eventType/repository/getEventTypeById';
import type { FastifyPluginAsync } from 'fastify';
const get: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async (request, reply) => {
const eventType = getAllEventTypes();
return eventType;
});
fastify.get<{ Params: { id: number } }>('/:id', async (request, reply) => {
try {
const { id } = request.params;
const eventTypes = getEventTypeById(id);
return eventTypes;
} catch (err) {
console.error(err);
}
});
};
export default get;

View File

@ -0,0 +1,96 @@
import type { FastifyPluginAsync } from 'fastify';
// const optsss = {
// schema: {
// body: {
// type: 'object',
// properties: {
// someKey: { type: 'string' },
// },
// },
// },
// };
// const bodyJsonSchema = {
// type: 'object',
// required: ['requiredKey'],
// properties: {
// someKey: { type: 'string' },
// someOtherKey: { type: 'number' },
// requiredKey: {
// type: 'array',
// maxItems: 3,
// items: { type: 'integer' },
// },
// nullableKey: { type: ['number', 'null'] }, // or { type: 'number', nullable: true }
// multipleTypesKey: { type: ['boolean', 'number'] },
// multipleRestrictedTypesKey: {
// oneOf: [
// { type: 'string', maxLength: 5 },
// { type: 'number', minimum: 10 },
// ],
// },
// enumKey: {
// type: 'string',
// enum: ['John', 'Foo'],
// },
// notTypeKey: {
// not: { type: 'array' },
// },
// },
// };
// const queryStringJsonSchema = {
// type: 'object',
// properties: {
// name: { type: 'string' },
// excitement: { type: 'integer' },
// },
// };
// const paramsJsonSchema = {
// type: 'object',
// properties: {
// par1: { type: 'string' },
// par2: { type: 'number' },
// },
// };
// const headersJsonSchema = {
// type: 'object',
// properties: {
// 'x-foo': { type: 'string' },
// },
// required: ['x-foo'],
// };
// const schema = {
// body: bodyJsonSchema,
// querystring: queryStringJsonSchema,
// params: paramsJsonSchema,
// headers: headersJsonSchema,
// };
const schema = {
body: {
properties: {
username: { type: 'string' },
age: { type: 'number' },
},
required: ['age'],
},
};
const post: FastifyPluginAsync = async (fastify, opts) => {
fastify.post('/', { schema }, async (request, reply) => {
try {
fastify.log.info('Hiiiiiii');
return `Hiii`;
} catch (err) {
console.error(err);
}
});
};
export default post;

View File

@ -0,0 +1,9 @@
import type { FastifyPluginAsync } from 'fastify';
const root: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get('/', async function (request, reply) {
return { root: true };
});
};
export default root;

View File

@ -0,0 +1,28 @@
import { getAllSchedules } from '../../schedule/repository/getAllSchedules';
import { getScheduleById } from '../../schedule/repository/getScheduleById';
import type { FastifyPluginAsync } from 'fastify';
const get: FastifyPluginAsync = async (fastify, opts) => {
fastify.get('/', async (request, reply) => {
try {
const schedule = getAllSchedules();
return schedule;
} catch (err) {
console.error(err);
}
});
fastify.get<{ Params: { id: number } }>('/:id', async (request, reply) => {
try {
const { id } = request.params;
const schedule = getScheduleById(id);
return schedule;
} catch (err) {
console.error(err);
}
});
};
export default get;

View File

@ -0,0 +1,19 @@
import { getTeamById } from '../../teams/repository/getTeamById';
// import * as TeamRepo from '../teams/repository/getTeamById';
import type { FastifyPluginAsync } from 'fastify';
const get: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.get<{ Params: { id: number } }>('/:id', async (request, reply) => {
try {
const { id } = request.params;
const team = await getTeamById(id);
return team;
} catch (err) {
console.error(err);
return 'error';
}
});
};
export default get;

View File

@ -0,0 +1,37 @@
import * as UserRepo from '../../users/repository';
import type { FastifyPluginAsync } from 'fastify';
const get: FastifyPluginAsync = async (fastify, opts): Promise<void> => {
fastify.register(import('../../plugins/auth'));
fastify.get<{ Params: { id: number } }>('/', async function (request, reply) {
try {
return `Users`;
} catch (err) {
console.error(err);
return 'error';
}
});
fastify.get('/me', async function (request, reply) {
try {
return await UserRepo.getUserById(request.user.id);
} catch (err) {
console.error(err);
return 'error';
}
});
fastify.get<{ Params: { id: number } }>('/:id', async function (request, reply) {
try {
const { id } = request.params;
const user = await UserRepo.getUserById(id);
return `this is an getUserById: ${user?.name}`;
} catch (err) {
console.error(err);
return 'error';
}
});
};
export default get;

View File

@ -0,0 +1,5 @@
import { db } from '../../lib/db';
export const getAllSchedules = async () => {
return await db.selectFrom('Schedule').selectAll().execute();
};

View File

@ -0,0 +1,5 @@
import { db } from '../../lib/db';
export const getScheduleById = async (id: number) => {
return await db.selectFrom('Schedule').selectAll().where('id', '=', id).executeTakeFirst();
};

View File

@ -0,0 +1,9 @@
import { db } from '../../lib/db';
export const getTeamById = async (id: number) => {
return await db
.selectFrom('Team')
.select(['name', 'slug', 'bio', 'timeZone'])
.where('id', '=', id)
.executeTakeFirst();
};

View File

@ -0,0 +1,9 @@
import { db } from '../../lib/db';
export const getUserById = async (id: number) => {
return await db
.selectFrom('users')
.select(['name', 'email', 'role'])
.where('id', '=', id)
.executeTakeFirst();
};

View File

@ -0,0 +1 @@
export { getUserById } from './getUserById';

View File

@ -0,0 +1,4 @@
import { createHash } from 'crypto';
export const hashAPIKey = (apiKey: string): string =>
createHash('sha256').update(apiKey).digest('hex');

View File

@ -0,0 +1,6 @@
export const dateNotInPast = function (date: Date) {
const now = new Date();
if (now.setHours(0, 0, 0, 0) > date.setHours(0, 0, 0, 0)) {
return true;
}
};

View File

@ -0,0 +1,32 @@
// This file contains code that we reuse between our tests.
import * as helper from 'fastify-cli/helper.js';
import * as path from 'path';
import type * as tap from 'tap';
export type Test = (typeof tap)['Test']['prototype'];
const AppPath = path.join(__dirname, '..', 'src', 'app.ts');
// Fill in this config with all the configurations
// needed for testing the application
async function config() {
return {};
}
// Automatically build and tear down our instance
async function build(t: Test) {
// you can set all the options supported by the fastify CLI command
const argv = [AppPath];
// fastify-plugin ensures that all decorators
// are exposed for testing purposes, this is
// different from the production setup
const app = await helper.build(argv, await config());
// Tear down our app after we are done
t.teardown(() => void app.close());
return app;
}
export { config, build };

View File

@ -0,0 +1,11 @@
import Support from '../../src/plugins/support';
import Fastify from 'fastify';
import { test } from 'tap';
test('support works standalone', async (t) => {
const fastify = Fastify();
void fastify.register(Support);
await fastify.ready();
t.equal(fastify.someSupport(), 'hugs');
});

View File

@ -0,0 +1,12 @@
import { build } from '../helper';
import { test } from 'tap';
test('example is loaded', async (t) => {
const app = await build(t);
const res = await app.inject({
url: '/example',
});
t.equal(res.payload, 'this is an example');
});

View File

@ -0,0 +1,11 @@
import { build } from '../helper';
import { test } from 'tap';
test('default root route', async (t) => {
const app = await build(t);
const res = await app.inject({
url: '/',
});
t.same(JSON.parse(res.payload), { root: true });
});

View File

@ -0,0 +1,8 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"baseUrl": ".",
"noEmit": true
},
"include": ["../src/**/*.ts", "**/*.ts"]
}

View File

@ -0,0 +1,9 @@
{
"extends": "fastify-tsconfig",
"compilerOptions": {
"outDir": "dist",
"sourceMap": true
},
"include": ["src/**/*.ts"],
"exclude": ["./node_modules"]
}