>[0],
10 | APIGatewayProxyResult
11 | > {
12 | const before: middy.MiddlewareFn<
13 | APIGatewayProxyEvent,
14 | APIGatewayProxyResult
15 | > = async (request) => {
16 | const authHeader = request.event.headers['Authorization'];
17 |
18 | if (authHeader) {
19 | const token = authHeader.split(' ')[1];
20 |
21 | try {
22 | const data = jwt.verify(token, env.jwtSecret);
23 | (request.context as unknown as UserContext).user =
24 | data as UserContext['user'];
25 |
26 | return Promise.resolve();
27 | } catch (error) {
28 | return httpError(error, { statusCode: 401 });
29 | }
30 | }
31 |
32 | return httpError('Missing token', { statusCode: 401 });
33 | };
34 |
35 | return {
36 | before,
37 | };
38 | }
39 |
--------------------------------------------------------------------------------
/libs/http/src/lib/handlers.ts:
--------------------------------------------------------------------------------
1 | import middy from '@middy/core';
2 | import middyJsonBodyParser from '@middy/http-json-body-parser';
3 | import { authMiddleware } from './auth.middleware';
4 | import { EventParams, Handler } from './types';
5 |
6 | export function createHandler<
7 | P extends EventParams,
8 | isProtected extends boolean = false
9 | >(handler: Handler) {
10 | return middy(handler).use(middyJsonBodyParser());
11 | }
12 |
13 | export function createProtectedHandler
(
14 | handler: Handler
15 | ) {
16 | return createHandler(handler).use(authMiddleware());
17 | }
18 |
--------------------------------------------------------------------------------
/libs/http/src/lib/response.ts:
--------------------------------------------------------------------------------
1 | import { APIGatewayProxyResult } from 'aws-lambda';
2 |
3 | const corsHeaders = {
4 | // Change this to your domains
5 | 'Access-Control-Allow-Origin': '*',
6 | // Change this to your headers
7 | 'Access-Control-Allow-Headers': '*',
8 | 'Access-Control-Max-Age': 86400,
9 | }
10 |
11 | export function httpResponse(
12 | data: Record,
13 | { statusCode = 200, ...rest }: Omit = {
14 | statusCode: 200,
15 | }
16 | ): APIGatewayProxyResult {
17 | return {
18 | body: JSON.stringify({ data }),
19 | statusCode,
20 | ...rest,
21 | headers: {
22 | ...rest.headers,
23 | ...corsHeaders
24 | },
25 | };
26 | }
27 |
28 | export function httpError(
29 | error: any,
30 | { statusCode = 400, ...rest }: Omit = {
31 | statusCode: 200,
32 | }
33 | ): APIGatewayProxyResult {
34 | return {
35 | body: JSON.stringify({ error }),
36 | statusCode,
37 | ...rest,
38 | headers: {
39 | ...rest.headers,
40 | ...corsHeaders
41 | },
42 | };
43 | }
44 |
--------------------------------------------------------------------------------
/libs/http/src/lib/schema-validator.middleware.ts:
--------------------------------------------------------------------------------
1 | import { BaseSchema, ValidationError } from 'yup';
2 | import middy from '@middy/core';
3 | import { APIGatewayProxyResult } from 'aws-lambda';
4 | import { httpError } from './response';
5 | import { BodyParams, EventParams, Handler, QueryParams } from './types';
6 |
7 | export function schemaValidator(schema: {
8 | body?: BaseSchema
;
9 | queryStringParameters?: BaseSchema<
10 | BaseSchema
11 | >;
12 | }): middy.MiddlewareObj>[0], APIGatewayProxyResult> {
13 | const before: middy.MiddlewareFn<
14 | Parameters>[0],
15 | APIGatewayProxyResult
16 | > = async (request) => {
17 | try {
18 | const { body, queryStringParameters } = request.event;
19 |
20 | if (schema.body) {
21 | schema.body.validateSync(body);
22 | }
23 |
24 | if (schema.queryStringParameters) {
25 | schema.queryStringParameters.validateSync(queryStringParameters ?? {});
26 | }
27 |
28 | return Promise.resolve();
29 | } catch (e) {
30 | return httpError(e instanceof ValidationError ? e.errors : []);
31 | }
32 | };
33 |
34 | return {
35 | before,
36 | };
37 | }
38 |
--------------------------------------------------------------------------------
/libs/http/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | APIGatewayProxyEvent,
3 | APIGatewayProxyResult,
4 | Callback,
5 | Context,
6 | } from 'aws-lambda';
7 |
8 | export interface BodyParams<
9 | T extends Record = Record
10 | > {
11 | body: T;
12 | }
13 |
14 | export interface QueryParams<
15 | T extends Record = Record
16 | > {
17 | queryStringParameters: T;
18 | }
19 |
20 | export interface PathParams<
21 | T extends Record = Record
22 | > {
23 | pathParameters: T;
24 | }
25 |
26 | export type EventParams = BodyParams | QueryParams | PathParams;
27 |
28 | export type Handler<
29 | P extends EventParams,
30 | isProtected extends boolean = true
31 | > = (
32 | event: Omit<
33 | APIGatewayProxyEvent,
34 | 'body' | 'pathParameters' | 'queryStringParameters'
35 | > & {
36 | body: P extends BodyParams ? P['body'] : null;
37 | pathParameters: P extends PathParams ? P['pathParameters'] : null;
38 | queryStringParameters: P extends QueryParams
39 | ? P['queryStringParameters']
40 | : null;
41 | },
42 | context: isProtected extends true ? Context & UserContext : Context,
43 | callback: Callback
44 | ) => void | Promise;
45 |
46 | export type UserContext = {
47 | user: {
48 | id: string;
49 | };
50 | };
51 |
--------------------------------------------------------------------------------
/libs/http/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "compilerOptions": {
4 | "module": "commonjs",
5 | "forceConsistentCasingInFileNames": true,
6 | "strict": true,
7 | "noImplicitOverride": true,
8 | "noPropertyAccessFromIndexSignature": true,
9 | "noImplicitReturns": true,
10 | "noFallthroughCasesInSwitch": true
11 | },
12 | "files": [],
13 | "include": [],
14 | "references": [
15 | {
16 | "path": "./tsconfig.lib.json"
17 | },
18 | {
19 | "path": "./tsconfig.spec.json"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/libs/http/tsconfig.lib.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "declaration": true,
6 | "types": []
7 | },
8 | "include": ["**/*.ts"],
9 | "exclude": ["**/*.spec.ts", "**/*.test.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/libs/http/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/logo.png
--------------------------------------------------------------------------------
/migrations.json:
--------------------------------------------------------------------------------
1 | {
2 | "migrations": [
3 | {
4 | "cli": "nx",
5 | "version": "14.0.6",
6 | "description": "Remove root property from project.json files",
7 | "factory": "./src/migrations/update-14-0-6/remove-roots",
8 | "package": "nx",
9 | "name": "14-0-6-remove-root"
10 | },
11 | {
12 | "version": "14.0.0-beta.0",
13 | "description": "Changes the presets in nx.json to come from the nx package",
14 | "cli": "nx",
15 | "implementation": "./src/migrations/update-14-0-0/change-nx-json-presets",
16 | "package": "@nrwl/workspace",
17 | "name": "14-0-0-change-nx-json-presets"
18 | },
19 | {
20 | "version": "14.0.0-beta.0",
21 | "description": "Migrates from @nrwl/workspace:run-script to nx:run-script",
22 | "cli": "nx",
23 | "implementation": "./src/migrations/update-14-0-0/change-npm-script-executor",
24 | "package": "@nrwl/workspace",
25 | "name": "14-0-0-change-npm-script-executor"
26 | },
27 | {
28 | "version": "14.0.0-beta.2",
29 | "cli": "nx",
30 | "description": "Update move jest config files to .ts files.",
31 | "factory": "./src/migrations/update-14-0-0/update-jest-config-ext",
32 | "package": "@nrwl/jest",
33 | "name": "update-jest-config-extensions"
34 | }
35 | ]
36 | }
37 |
--------------------------------------------------------------------------------
/nx.json:
--------------------------------------------------------------------------------
1 | {
2 | "npmScope": "app",
3 | "affected": {
4 | "defaultBase": "main"
5 | },
6 | "implicitDependencies": {
7 | "workspace.json": "*",
8 | "package.json": {
9 | "dependencies": "*",
10 | "devDependencies": "*"
11 | },
12 | "tsconfig.base.json": "*",
13 | "tslint.json": "*",
14 | ".eslintrc.json": "*",
15 | "nx.json": "*"
16 | },
17 | "tasksRunnerOptions": {
18 | "default": {
19 | "runner": "@nrwl/workspace/tasks-runners/default",
20 | "options": {
21 | "cacheableOperations": ["build", "lint", "test"]
22 | }
23 | }
24 | },
25 | "workspaceLayout": {
26 | "appsDir": "services"
27 | },
28 | "cli": {
29 | "defaultCollection": "@nrwl/node"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nx-serverless",
3 | "version": "1.0.0",
4 | "private": true,
5 | "scripts": {
6 | "nx": "nx",
7 | "localstack": "docker-compose up",
8 | "build": "nx build",
9 | "serve": "nx run-many --target=serve --all",
10 | "test": "nx run-many --target=test --all",
11 | "lint": "nx run-many --target=lint --all",
12 | "deploy": "nx run-many --target=deploy --all",
13 | "affected": "nx affected",
14 | "format": "nx format:write",
15 | "update": "nx migrate latest",
16 | "workspace-generator": "nx workspace-generator",
17 | "test:affected": "nx affected:test --base=origin/main --head=HEAD",
18 | "deploy:affected": "nx affected --target=deploy --all",
19 | "lint:affected": "nx affected:lint --base=origin/main --head=HEAD",
20 | "g:http-handler": " npx nx workspace-generator http-handler",
21 | "g:handler": " npx nx workspace-generator handler",
22 | "g:service": " npx nx workspace-generator service",
23 | "g:model": " npx nx workspace-generator model"
24 | },
25 | "dependencies": {
26 | "@aws-sdk/util-dynamodb": "3.80.0",
27 | "@middy/core": "2.5.7",
28 | "@middy/http-json-body-parser": "2.5.7",
29 | "jsonwebtoken": "8.5.1",
30 | "middy": "0.36.0",
31 | "ulid": "2.3.0",
32 | "yup": "0.32.11",
33 | "tslib": "2.0.0"
34 | },
35 | "devDependencies": {
36 | "ts-morph": "15.0.0",
37 | "@aws-sdk/types": "3.55.0",
38 | "@jest/transform": "27.5.1",
39 | "@nrwl/cli": "14.1.2",
40 | "@nrwl/devkit": "14.1.2",
41 | "@nrwl/eslint-plugin-nx": "14.1.2",
42 | "@nrwl/jest": "14.1.2",
43 | "@nrwl/linter": "14.1.2",
44 | "@nrwl/node": "14.1.2",
45 | "@nrwl/nx-cloud": "14.0.3",
46 | "@nrwl/tao": "14.1.2",
47 | "@nrwl/workspace": "14.1.2",
48 | "@types/aws-lambda": "8.10.93",
49 | "@types/jest": "27.4.1",
50 | "@types/jsonwebtoken": "8.5.8",
51 | "@types/node": "15.12.4",
52 | "@types/serverless": "3.0.2",
53 | "@types/terser-webpack-plugin": "5.0.4",
54 | "@types/webpack": "5.28.0",
55 | "@typescript-eslint/eslint-plugin": "5.19.0",
56 | "@typescript-eslint/parser": "5.19.0",
57 | "esbuild": "0.14.36",
58 | "eslint": "8.13.0",
59 | "eslint-config-prettier": "8.5.0",
60 | "husky": "7.0.4",
61 | "jest": "27.5.1",
62 | "lambda-event-mock": "1.5.0",
63 | "lint-staged": "12.3.8",
64 | "prettier": "2.6.2",
65 | "serverless": "3.18.1",
66 | "serverless-esbuild": "1.27.1",
67 | "serverless-jest-plugin": "0.4.0",
68 | "serverless-localstack": "0.4.36",
69 | "serverless-offline": "8.8.0",
70 | "ts-jest": "27.1.4",
71 | "ts-loader": "9.2.8",
72 | "ts-node": "9.1.1",
73 | "typescript": "4.6.3"
74 | },
75 | "engines": {
76 | "node": ">=16"
77 | },
78 | "lint-staged": {
79 | "*.{js,jsx,ts,tsx}": [
80 | "eslint --fix",
81 | "prettier --write"
82 | ],
83 | "*.{md,json,yml,yaml,html}": [
84 | "prettier --write"
85 | ]
86 | }
87 | }
--------------------------------------------------------------------------------
/plugins.js:
--------------------------------------------------------------------------------
1 | const { readFileSync } = require('fs');
2 |
3 | const envPlugin = {
4 | name: 'env',
5 | setup(build) {
6 | build.onResolve({ filter: /@app\/env$/ }, (args) => {
7 | return {
8 | path: args.path,
9 | namespace: 'env-ns',
10 | };
11 | });
12 |
13 | build.onLoad({ filter: /.*/, namespace: 'env-ns' }, () => {
14 | const envFile = process.env.NODE_ENV ? `.${process.env.NODE_ENV}` : '';
15 | const content = readFileSync(
16 | `../../environments/environment${envFile}.ts`,
17 | 'utf8'
18 | );
19 |
20 | return {
21 | contents: content,
22 | resolveDir: '../../environments',
23 | loader: 'ts',
24 | };
25 | });
26 | },
27 | };
28 |
29 | module.exports = [envPlugin];
30 |
--------------------------------------------------------------------------------
/serverless.base.ts:
--------------------------------------------------------------------------------
1 | import type { Serverless } from 'serverless/aws';
2 | import { env, envName } from './environments/environment.serverless';
3 |
4 | console.log(`-------------- USING ENV: ${env.name} ----------------`);
5 |
6 | export const baseServerlessConfigProvider: Serverless['provider'] = {
7 | name: 'aws',
8 | runtime: 'nodejs16.x',
9 | memorySize: 128,
10 | profile: env.profile,
11 | stage: env.name,
12 | environment: {
13 | NODE_ENV: envName,
14 | AWS_NODEJS_CONNECTION_REUSE_ENABLED: '1',
15 | },
16 | region: env.region,
17 | };
18 |
19 | export const baseServerlessConfig: Partial = {
20 | frameworkVersion: '3',
21 | service: 'base',
22 | package: {
23 | individually: true,
24 | excludeDevDependencies: true,
25 | },
26 | plugins: ['serverless-esbuild', 'serverless-offline'],
27 | custom: {
28 | esbuild: {
29 | bundle: true,
30 | minify: env.name !== 'dev',
31 | target: ['es2020'],
32 | sourcemap: env.name !== 'dev',
33 | sourcesContent: false,
34 | plugins: '../../plugins.js',
35 | define: { 'require.resolve': undefined },
36 | },
37 | },
38 | provider: {
39 | ...baseServerlessConfigProvider,
40 | apiGateway: {
41 | minimumCompressionSize: 1024,
42 | // @ts-ignore
43 | restApiId: {
44 | 'Fn::ImportValue': `${env.name}-AppApiGW-restApiId`,
45 | },
46 | // @ts-ignore
47 | restApiRootResourceId: {
48 | 'Fn::ImportValue': `${env.name}-AppApiGW-rootResourceId`,
49 | },
50 | },
51 | },
52 | };
53 |
--------------------------------------------------------------------------------
/services/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/services/.gitkeep
--------------------------------------------------------------------------------
/services/auth/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/services/auth/README.md:
--------------------------------------------------------------------------------
1 | # auth
2 |
3 | This stack was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test auth` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/services/auth/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'auth',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | coverageDirectory: '../../coverage/services/auth',
11 | };
12 |
--------------------------------------------------------------------------------
/services/auth/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "services/auth",
3 | "projectType": "application",
4 | "sourceRoot": "services/auth/src",
5 | "targets": {
6 | "build": {
7 | "executor": "@nrwl/workspace:run-commands",
8 | "options": {
9 | "cwd": "services/auth",
10 | "color": true,
11 | "command": "sls package"
12 | }
13 | },
14 | "serve": {
15 | "executor": "@nrwl/workspace:run-commands",
16 | "options": {
17 | "cwd": "services/auth",
18 | "color": true,
19 | "command": "sls offline start"
20 | }
21 | },
22 | "deploy": {
23 | "executor": "@nrwl/workspace:run-commands",
24 | "options": {
25 | "cwd": "services/auth",
26 | "color": true,
27 | "command": "sls deploy --verbose"
28 | },
29 | "dependsOn": [
30 | {
31 | "target": "deploy",
32 | "projects": "dependencies"
33 | }
34 | ]
35 | },
36 | "remove": {
37 | "executor": "@nrwl/workspace:run-commands",
38 | "options": {
39 | "cwd": "services/auth",
40 | "color": true,
41 | "command": "sls remove"
42 | }
43 | },
44 | "lint": {
45 | "executor": "@nrwl/linter:eslint",
46 | "options": {
47 | "lintFilePatterns": ["services/auth/**/*.ts"]
48 | }
49 | },
50 | "test": {
51 | "executor": "@nrwl/jest:jest",
52 | "outputs": ["coverage/services/auth"],
53 | "options": {
54 | "jestConfig": "services/auth/jest.config.js",
55 | "passWithNoTests": true
56 | }
57 | }
58 | },
59 | "tags": ["service"],
60 | "implicitDependencies": ["core"]
61 | }
62 |
--------------------------------------------------------------------------------
/services/auth/serverless.ts:
--------------------------------------------------------------------------------
1 | import type { Serverless } from 'serverless/aws';
2 | import { baseServerlessConfig } from '../../serverless.base';
3 | import { tableResource } from '../../environments/environment.serverless';
4 |
5 | const serverlessConfig: Partial = {
6 | ...baseServerlessConfig,
7 | service: 'auth',
8 | provider: {
9 | ...baseServerlessConfig.provider,
10 | iam: {
11 | role: {
12 | statements: [
13 | {
14 | Effect: 'Allow',
15 | Action: ['dynamodb:PutItem'],
16 | Resource: tableResource,
17 | },
18 | ],
19 | },
20 | },
21 | },
22 | custom: {
23 | ...baseServerlessConfig.custom,
24 | 'serverless-offline': {
25 | lambdaPort: 3000,
26 | httpPort: 3001,
27 | },
28 | },
29 | functions: {
30 | 'sign-up': {
31 | handler: 'src/sign-up/sign-up-handler.main',
32 | events: [
33 | {
34 | http: {
35 | method: 'post',
36 | path: 'auth/sign-up',
37 | },
38 | },
39 | ],
40 | },
41 | },
42 | };
43 |
44 | module.exports = serverlessConfig;
45 |
--------------------------------------------------------------------------------
/services/auth/src/auth.utils.ts:
--------------------------------------------------------------------------------
1 | import { sign } from 'jsonwebtoken';
2 | import { env } from '@app/env';
3 |
4 | export function createJWT(id: string) {
5 | return sign(
6 | {
7 | id,
8 | },
9 | env.jwtSecret,
10 | { expiresIn: '5d' }
11 | );
12 | }
13 |
--------------------------------------------------------------------------------
/services/auth/src/sign-up/sign-in-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('sign-up', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBeTruthy();
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/auth/src/sign-up/sign-up-handler.ts:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup';
2 | import { BodyParams } from '@app/http/types';
3 | import { createHandler } from '@app/http/handlers';
4 | import { httpResponse, httpError } from '@app/http/response';
5 | import { schemaValidator } from '@app/http/schema-validator.middleware';
6 | import { createJWT } from '../auth.utils';
7 | import { createUser, User } from '@app/users/user.model';
8 |
9 | type Params = BodyParams<{ email: string; name: string }>;
10 |
11 | export const main = createHandler(async (event) => {
12 | const { email, name } = event.body;
13 |
14 | try {
15 | await createUser(new User({ email, name }));
16 |
17 | return httpResponse({
18 | token: createJWT(email),
19 | });
20 | } catch (error) {
21 | return httpError(error);
22 | }
23 | });
24 |
25 | main.use([
26 | schemaValidator({
27 | body: object({
28 | email: string().email().required(),
29 | name: string().required(),
30 | }),
31 | }),
32 | ]);
33 |
--------------------------------------------------------------------------------
/services/auth/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/auth/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/services/auth/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/core/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/services/core/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'core',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | coverageDirectory: '../../coverage/stacks/api',
11 | };
12 |
--------------------------------------------------------------------------------
/services/core/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "services/core",
3 | "projectType": "application",
4 | "sourceRoot": "services/core/src",
5 | "targets": {
6 | "deploy": {
7 | "executor": "@nrwl/workspace:run-commands",
8 | "options": {
9 | "cwd": "services/core",
10 | "color": true,
11 | "command": "sls deploy --verbose"
12 | }
13 | },
14 | "remove": {
15 | "executor": "@nrwl/workspace:run-commands",
16 | "options": {
17 | "cwd": "services/core",
18 | "color": true,
19 | "command": "sls remove"
20 | }
21 | }
22 | },
23 | "tags": ["services"]
24 | }
25 |
--------------------------------------------------------------------------------
/services/core/serverless.ts:
--------------------------------------------------------------------------------
1 | import { env } from '../../environments/environment.serverless';
2 | import type { Serverless } from 'serverless/aws';
3 | import { baseServerlessConfigProvider } from '../../serverless.base';
4 |
5 | const serverlessConfig: Partial = {
6 | provider: baseServerlessConfigProvider,
7 | plugins: ['serverless-localstack'],
8 | service: 'core',
9 | custom: {
10 | localstack: {
11 | stages: ['local'],
12 | lambda: {
13 | mountCode: 'True',
14 | },
15 | },
16 | },
17 | resources: {
18 | Resources: {
19 | AppApiGW: {
20 | Type: 'AWS::ApiGateway::RestApi',
21 | Properties: {
22 | Name: `${env.name}-AppApiGW`,
23 | },
24 | },
25 | AppTable: {
26 | Type: 'AWS::DynamoDB::Table',
27 | Properties: {
28 | TableName: env.dynamo.tableName,
29 | AttributeDefinitions: [
30 | {
31 | AttributeName: 'PK',
32 | AttributeType: 'S',
33 | },
34 | {
35 | AttributeName: 'SK',
36 | AttributeType: 'S',
37 | },
38 | ],
39 | KeySchema: [
40 | {
41 | AttributeName: 'PK',
42 | KeyType: 'HASH',
43 | },
44 | {
45 | AttributeName: 'SK',
46 | KeyType: 'RANGE',
47 | },
48 | ],
49 | ProvisionedThroughput: {
50 | ReadCapacityUnits: 1,
51 | WriteCapacityUnits: 1,
52 | },
53 | },
54 | },
55 | },
56 | Outputs: {
57 | ApiGatewayRestApiId: {
58 | Value: {
59 | Ref: 'AppApiGW',
60 | },
61 | Export: {
62 | Name: `${env.name}-AppApiGW-restApiId`,
63 | },
64 | },
65 | ApiGatewayRestApiRootResourceId: {
66 | Value: {
67 | 'Fn::GetAtt': ['AppApiGW', 'RootResourceId'],
68 | },
69 | Export: {
70 | Name: `${env.name}-AppApiGW-rootResourceId`,
71 | },
72 | },
73 | },
74 | },
75 | };
76 |
77 | module.exports = serverlessConfig;
78 |
--------------------------------------------------------------------------------
/services/core/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/services/core/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/todos/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/services/todos/README.md:
--------------------------------------------------------------------------------
1 | # todos
2 |
3 | This stack was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test todos` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/services/todos/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'todos',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | coverageDirectory: '../../coverage/services/todos',
11 | };
12 |
--------------------------------------------------------------------------------
/services/todos/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "services/todos",
3 | "projectType": "application",
4 | "sourceRoot": "services/todos/src",
5 | "targets": {
6 | "build": {
7 | "executor": "@nrwl/workspace:run-commands",
8 | "options": {
9 | "cwd": "services/todos",
10 | "color": true,
11 | "command": "sls package"
12 | }
13 | },
14 | "serve": {
15 | "executor": "@nrwl/workspace:run-commands",
16 | "options": {
17 | "cwd": "services/todos",
18 | "color": true,
19 | "command": "sls offline start"
20 | }
21 | },
22 | "deploy": {
23 | "executor": "@nrwl/workspace:run-commands",
24 | "options": {
25 | "cwd": "services/todos",
26 | "color": true,
27 | "command": "sls deploy --verbose"
28 | },
29 | "dependsOn": [
30 | {
31 | "target": "deploy",
32 | "projects": "dependencies"
33 | }
34 | ]
35 | },
36 | "remove": {
37 | "executor": "@nrwl/workspace:run-commands",
38 | "options": {
39 | "cwd": "services/todos",
40 | "color": true,
41 | "command": "sls remove"
42 | }
43 | },
44 | "lint": {
45 | "executor": "@nrwl/linter:eslint",
46 | "options": {
47 | "lintFilePatterns": ["services/todos/**/*.ts"]
48 | }
49 | },
50 | "test": {
51 | "executor": "@nrwl/jest:jest",
52 | "outputs": ["coverage/services/todos"],
53 | "options": {
54 | "jestConfig": "services/todos/jest.config.js",
55 | "passWithNoTests": true
56 | }
57 | }
58 | },
59 | "tags": ["service"],
60 | "implicitDependencies": ["core"]
61 | }
62 |
--------------------------------------------------------------------------------
/services/todos/serverless.ts:
--------------------------------------------------------------------------------
1 | import { tableResource } from '../../environments/environment.serverless';
2 | import type { Serverless } from 'serverless/aws';
3 | import { baseServerlessConfig } from '../../serverless.base';
4 |
5 | const serverlessConfig: Partial = {
6 | ...baseServerlessConfig,
7 | service: `todos`,
8 | provider: {
9 | ...baseServerlessConfig.provider,
10 | iam: {
11 | role: {
12 | statements: [
13 | {
14 | Effect: 'Allow',
15 | Action: [
16 | 'dynamodb:Query',
17 | 'dynamodb:GetItem',
18 | 'dynamodb:PutItem',
19 | 'dynamodb:UpdateItem',
20 | ],
21 | Resource: tableResource,
22 | },
23 | ],
24 | },
25 | },
26 | },
27 | custom: {
28 | ...baseServerlessConfig.custom,
29 | 'serverless-offline': {
30 | lambdaPort: 3004,
31 | httpPort: 3005,
32 | },
33 | },
34 | functions: {
35 | 'get-todos': {
36 | handler: 'src/get-todos/get-todos-handler.main',
37 | events: [
38 | {
39 | http: {
40 | method: 'get',
41 | path: 'todos',
42 | },
43 | },
44 | ],
45 | },
46 | 'get-todo': {
47 | handler: 'src/get-todo/get-todo-handler.main',
48 | events: [
49 | {
50 | http: {
51 | method: 'get',
52 | path: 'todos/{id}',
53 | },
54 | },
55 | ],
56 | },
57 | 'create-todo': {
58 | handler: 'src/create-todo/create-todo-handler.main',
59 | events: [
60 | {
61 | http: {
62 | method: 'post',
63 | path: 'todos',
64 | },
65 | },
66 | ],
67 | },
68 | 'update-todo': {
69 | handler: 'src/update-todo/update-todo-handler.main',
70 | events: [
71 | {
72 | http: {
73 | method: 'put',
74 | path: 'todos/{id}',
75 | },
76 | },
77 | ],
78 | },
79 | },
80 | };
81 |
82 | module.exports = serverlessConfig;
83 |
--------------------------------------------------------------------------------
/services/todos/src/create-todo/create-todo-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('create-todo', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/todos/src/create-todo/create-todo-handler.ts:
--------------------------------------------------------------------------------
1 | import { BodyParams } from '@app/http/types';
2 | import { createProtectedHandler } from '@app/http/handlers';
3 | import { httpError, httpResponse } from '@app/http/response';
4 | import { schemaValidator } from '@app/http/schema-validator.middleware';
5 | import { createTodo, Todo } from '../todo.model';
6 | import { ulid } from 'ulid';
7 | import { UserKeys } from '@app/users/user.model';
8 | import { object, string } from 'yup';
9 |
10 | type Params = BodyParams<{ title: string }>;
11 |
12 | export const main = createProtectedHandler(async (event, context) => {
13 | const userKeys = new UserKeys(context.user.id);
14 |
15 | const todo = new Todo(
16 | { id: ulid(), completed: false, title: event.body.title },
17 | userKeys
18 | );
19 |
20 | try {
21 | const newTodo = await createTodo(todo);
22 |
23 | return httpResponse({
24 | todo: newTodo,
25 | });
26 | } catch (e) {
27 | return httpError(e);
28 | }
29 | });
30 |
31 | main.use([
32 | schemaValidator({
33 | body: object({
34 | title: string().required(),
35 | }),
36 | }),
37 | ]);
38 |
--------------------------------------------------------------------------------
/services/todos/src/get-todo/get-todo-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('get-todo', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/todos/src/get-todo/get-todo-handler.ts:
--------------------------------------------------------------------------------
1 | import { PathParams } from '@app/http/types';
2 | import { createProtectedHandler } from '@app/http/handlers';
3 | import { httpError, httpResponse } from '@app/http/response';
4 | import { UserKeys } from '@app/users/user.model';
5 | import { getTodo, TodoKeys } from '../todo.model';
6 |
7 | type Params = PathParams<{ id: string }>;
8 |
9 | export const main = createProtectedHandler(async (event, context) => {
10 | const userKeys = new UserKeys(context.user.id);
11 | const todoKeys = new TodoKeys(event.pathParameters.id, userKeys);
12 |
13 | try {
14 | const todo = await getTodo(todoKeys);
15 |
16 | return httpResponse({
17 | todo,
18 | });
19 | } catch (e) {
20 | return httpError(e);
21 | }
22 | });
23 |
--------------------------------------------------------------------------------
/services/todos/src/get-todos/get-todos-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('get-todos', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/todos/src/get-todos/get-todos-handler.ts:
--------------------------------------------------------------------------------
1 | import { QueryParams } from '@app/http/types';
2 | import { createProtectedHandler } from '@app/http/handlers';
3 | import { httpError, httpResponse } from '@app/http/response';
4 | import { UserKeys } from '@app/users/user.model';
5 | import { getTodos } from '../todo.model';
6 |
7 | type Params = QueryParams<{ searchTerm: string }>;
8 |
9 | export const main = createProtectedHandler(async (event, context) => {
10 | const userKeys = new UserKeys(context.user.id);
11 |
12 | try {
13 | const todos = await getTodos(userKeys);
14 |
15 | return httpResponse({
16 | todos,
17 | });
18 | } catch (e) {
19 | return httpError(e);
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/services/todos/src/todo.model.ts:
--------------------------------------------------------------------------------
1 | import { createItem, query, updateItem, getItem } from '@app/db/operations';
2 | import { Item, ItemKeys } from '@app/db/item';
3 | import { DynamoDB } from 'aws-sdk';
4 | import { UserKeys } from '@app/users/user.model';
5 |
6 | export interface TodoModel {
7 | id: string;
8 | title: string;
9 | completed: boolean;
10 | }
11 |
12 | export class TodoKeys extends ItemKeys {
13 | static ENTITY_TYPE = 'TODO';
14 |
15 | constructor(private todoId: string, private userKeys: UserKeys) {
16 | super();
17 | }
18 |
19 | get pk() {
20 | return this.userKeys.pk;
21 | }
22 |
23 | get sk() {
24 | return `${TodoKeys.ENTITY_TYPE}#${this.todoId}`;
25 | }
26 | }
27 |
28 | export class Todo extends Item {
29 | constructor(private todo: TodoModel, private userKeys: UserKeys) {
30 | super();
31 | }
32 |
33 | get keys() {
34 | return new TodoKeys(this.todo.id, this.userKeys);
35 | }
36 |
37 | static fromItem(attributeMap: DynamoDB.AttributeMap): TodoModel {
38 | return {
39 | id: attributeMap.id.S,
40 | title: attributeMap.title.S,
41 | completed: attributeMap.completed.BOOL,
42 | };
43 | }
44 |
45 | toItem() {
46 | return this.marshall(this.todo);
47 | }
48 | }
49 |
50 | export async function createTodo(todo: Todo): Promise {
51 | await createItem(todo);
52 |
53 | return Todo.fromItem(todo.toItem());
54 | }
55 |
56 | export async function getTodo(todoKeys: TodoKeys) {
57 | const result = await getItem(todoKeys);
58 |
59 | return Todo.fromItem(result.Item);
60 | }
61 |
62 | export async function updateTodo(
63 | todoKeys: TodoKeys,
64 | completed: TodoModel['completed']
65 | ) {
66 | await updateItem(todoKeys, {
67 | UpdateExpression: 'SET #completed = :completed',
68 | ExpressionAttributeValues: {
69 | ':completed': { BOOL: completed },
70 | },
71 | ExpressionAttributeNames: {
72 | '#completed': 'completed',
73 | },
74 | });
75 |
76 | return { success: true };
77 | }
78 |
79 | export async function getTodos(userKeys: UserKeys) {
80 | const result = await query({
81 | KeyConditionExpression: `PK = :PK AND begins_with(SK, :SK)`,
82 | ExpressionAttributeValues: {
83 | ':PK': { S: userKeys.pk },
84 | ':SK': { S: TodoKeys.ENTITY_TYPE },
85 | },
86 | });
87 |
88 | return result.Items.map(Todo.fromItem);
89 | }
90 |
--------------------------------------------------------------------------------
/services/todos/src/update-todo/update-todo-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('update-todo', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/todos/src/update-todo/update-todo-handler.ts:
--------------------------------------------------------------------------------
1 | import { PathParams, BodyParams } from '@app/http/types';
2 | import { createProtectedHandler } from '@app/http/handlers';
3 | import { httpError, httpResponse } from '@app/http/response';
4 | import { UserKeys } from '@app/users/user.model';
5 | import { TodoKeys, TodoModel, updateTodo } from '../todo.model';
6 |
7 | type Params = PathParams<{ id: string }> &
8 | BodyParams<{ completed: TodoModel['completed'] }>;
9 |
10 | export const main = createProtectedHandler(async (event, context) => {
11 | const userKeys = new UserKeys(context.user.id);
12 | const todoKeys = new TodoKeys(event.pathParameters.id, userKeys);
13 |
14 | try {
15 | const result = await updateTodo(todoKeys, event.body.completed);
16 |
17 | return httpResponse(result);
18 | } catch (e) {
19 | return httpError(e);
20 | }
21 | });
22 |
--------------------------------------------------------------------------------
/services/todos/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/todos/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/services/todos/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/users/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/services/users/README.md:
--------------------------------------------------------------------------------
1 | # users
2 |
3 | This stack was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test users` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/services/users/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: 'users',
3 | preset: '../../jest.preset.js',
4 | globals: {
5 | 'ts-jest': {
6 | tsconfig: '/tsconfig.spec.json',
7 | },
8 | },
9 | testEnvironment: 'node',
10 | coverageDirectory: '../../coverage/services/users',
11 | };
12 |
--------------------------------------------------------------------------------
/services/users/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": "services/users",
3 | "projectType": "application",
4 | "sourceRoot": "services/users/src",
5 | "targets": {
6 | "build": {
7 | "executor": "@nrwl/workspace:run-commands",
8 | "options": {
9 | "cwd": "services/users",
10 | "color": true,
11 | "command": "sls package"
12 | }
13 | },
14 | "serve": {
15 | "executor": "@nrwl/workspace:run-commands",
16 | "options": {
17 | "cwd": "services/users",
18 | "color": true,
19 | "command": "sls offline start"
20 | }
21 | },
22 | "deploy": {
23 | "executor": "@nrwl/workspace:run-commands",
24 | "options": {
25 | "cwd": "services/users",
26 | "color": true,
27 | "command": "sls deploy --verbose"
28 | },
29 | "dependsOn": [
30 | {
31 | "target": "deploy",
32 | "projects": "dependencies"
33 | }
34 | ]
35 | },
36 | "remove": {
37 | "executor": "@nrwl/workspace:run-commands",
38 | "options": {
39 | "cwd": "services/users",
40 | "color": true,
41 | "command": "sls remove"
42 | }
43 | },
44 | "lint": {
45 | "executor": "@nrwl/linter:eslint",
46 | "options": {
47 | "lintFilePatterns": ["services/users/**/*.ts"]
48 | }
49 | },
50 | "test": {
51 | "executor": "@nrwl/jest:jest",
52 | "outputs": ["coverage/services/users"],
53 | "options": {
54 | "jestConfig": "services/users/jest.config.js",
55 | "passWithNoTests": true
56 | }
57 | }
58 | },
59 | "tags": ["service"],
60 | "implicitDependencies": ["core"]
61 | }
62 |
--------------------------------------------------------------------------------
/services/users/serverless.ts:
--------------------------------------------------------------------------------
1 | import type { Serverless } from 'serverless/aws';
2 | import { baseServerlessConfig } from '../../serverless.base';
3 | import { tableResource } from '../../environments/environment.serverless';
4 |
5 | const serverlessConfig: Partial = {
6 | ...baseServerlessConfig,
7 | service: 'users',
8 | provider: {
9 | ...baseServerlessConfig.provider,
10 | iam: {
11 | role: {
12 | statements: [
13 | {
14 | Effect: 'Allow',
15 | Action: ['dynamodb:GetItem'],
16 | Resource: tableResource,
17 | },
18 | ],
19 | },
20 | },
21 | },
22 | custom: {
23 | ...baseServerlessConfig.custom,
24 | 'serverless-offline': {
25 | lambdaPort: 3002,
26 | httpPort: 3003,
27 | },
28 | },
29 | functions: {
30 | 'get-user': {
31 | handler: 'src/get-user/get-user-handler.main',
32 | events: [
33 | {
34 | http: {
35 | method: 'get',
36 | path: 'user',
37 | },
38 | },
39 | ],
40 | },
41 | },
42 | };
43 |
44 | module.exports = serverlessConfig;
45 |
--------------------------------------------------------------------------------
/services/users/src/get-user/get-user-handler.spec.ts:
--------------------------------------------------------------------------------
1 | describe('get-user', () => {
2 | it('should do something useful', async () => {
3 | expect(true).toBe(true);
4 | });
5 | });
6 |
--------------------------------------------------------------------------------
/services/users/src/get-user/get-user-handler.ts:
--------------------------------------------------------------------------------
1 | import { createProtectedHandler } from '@app/http/handlers';
2 | import { httpError, httpResponse } from '@app/http/response';
3 |
4 | import { getUser, UserKeys } from '../user.model';
5 |
6 | export const main = createProtectedHandler(async (_, context) => {
7 | try {
8 | const user = await getUser(new UserKeys(context.user.id));
9 |
10 | return httpResponse({
11 | user,
12 | });
13 | } catch (e) {
14 | return httpError(e);
15 | }
16 | });
17 |
--------------------------------------------------------------------------------
/services/users/src/user.model.ts:
--------------------------------------------------------------------------------
1 | import { createItem, getItem } from '@app/db/operations';
2 | import { Item, ItemKeys } from '@app/db/item';
3 | import { DynamoDB } from 'aws-sdk';
4 |
5 | export interface UserModel {
6 | email: string;
7 | name: string;
8 | }
9 |
10 | export class UserKeys extends ItemKeys {
11 | static ENTITY_TYPE = 'USER';
12 |
13 | constructor(private email: string) {
14 | super();
15 | }
16 |
17 | get pk() {
18 | return `${UserKeys.ENTITY_TYPE}#${this.email}`;
19 | }
20 |
21 | get sk() {
22 | return this.pk;
23 | }
24 | }
25 |
26 | export class User extends Item {
27 | constructor(private user: UserModel) {
28 | super();
29 | }
30 |
31 | static fromItem(attributeMap: DynamoDB.AttributeMap): UserModel {
32 | return {
33 | email: attributeMap.email.S,
34 | name: attributeMap.name.S,
35 | };
36 | }
37 |
38 | get keys() {
39 | return new UserKeys(this.user.email);
40 | }
41 |
42 | toItem() {
43 | return this.marshall(this.user);
44 | }
45 | }
46 |
47 | export async function createUser(user: User): Promise {
48 | await createItem(user);
49 |
50 | return User.fromItem(user.toItem());
51 | }
52 |
53 | export async function getUser(userKeys: UserKeys): Promise {
54 | const result = await getItem(userKeys);
55 |
56 | return User.fromItem(result.Item);
57 | }
58 |
--------------------------------------------------------------------------------
/services/users/src/users.types.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/services/users/src/users.types.ts
--------------------------------------------------------------------------------
/services/users/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/services/users/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | },
12 | {
13 | "path": "./tsconfig.spec.json"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/services/users/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.test.ts", "**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tools/generators/handler/files/__fileName__/__fileName__-handler.spec.ts__tmpl__:
--------------------------------------------------------------------------------
1 | import { main } from './<%= name %>-handler';
2 |
3 | describe('<%= name %>', () => {
4 |
5 | it('should do something useful', async () => {
6 | expect(main).toBe(true);
7 | })
8 |
9 | });
10 |
--------------------------------------------------------------------------------
/tools/generators/handler/files/__fileName__/__fileName__-handler.ts__tmpl__:
--------------------------------------------------------------------------------
1 | export const main = async (event) => {
2 |
3 | };
4 |
--------------------------------------------------------------------------------
/tools/generators/handler/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tree,
3 | formatFiles,
4 | installPackagesTask,
5 | names,
6 | generateFiles,
7 | joinPathFragments,
8 | } from '@nrwl/devkit';
9 |
10 | interface Schema {
11 | name: string;
12 | project: string;
13 | }
14 |
15 | export default async function (tree: Tree, schema: Schema) {
16 | const serviceRoot = `services/${schema.project}/src`;
17 |
18 | const { fileName } = names(schema.name);
19 |
20 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceRoot, {
21 | ...schema,
22 | tmpl: '',
23 | fileName,
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/tools/generators/handler/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema",
3 | "cli": "nx",
4 | "$id": "handler",
5 | "type": "object",
6 | "properties": {
7 | "name": {
8 | "type": "string",
9 | "description": "Handler name",
10 | "x-prompt": "What is the name of the handler?",
11 | "$default": {
12 | "$source": "argv",
13 | "index": 0
14 | }
15 | },
16 | "project": {
17 | "type": "string",
18 | "description": "Project name",
19 | "x-prompt": "What is the name of the project?"
20 | }
21 | },
22 | "required": [
23 | "name",
24 | "project"
25 | ]
26 | }
--------------------------------------------------------------------------------
/tools/generators/http-handler/files/__fileName__/__fileName__-handler.spec.ts__tmpl__:
--------------------------------------------------------------------------------
1 | import lambdaEventMock from 'lambda-event-mock';
2 | import { Context } from 'aws-lambda';
3 | import { main } from './<%= name %>-handler';
4 |
5 | describe('<%= name %>', () => {
6 |
7 | it('should do something useful', async () => {
8 |
9 | const event = lambdaEventMock.apiGateway()
10 | .path('/todos')
11 | .queryStringParameters({
12 | searchTerm: 'foo',
13 | })
14 | .method('GET')
15 | .build();
16 |
17 | const { body } = await main(event, {} as any);
18 |
19 | expect(JSON.parse(body).data.searchTerm).toEqual('foo')
20 | })
21 |
22 | });
23 |
--------------------------------------------------------------------------------
/tools/generators/http-handler/files/__fileName__/__fileName__-handler.ts__tmpl__:
--------------------------------------------------------------------------------
1 | import type { QueryParams } from '@app/http/types';
2 | import { createProtectedHandler } from '@app/http/handlers';
3 | import { httpResponse } from '@app/http/response';
4 |
5 | type Params = QueryParams<{ searchTerm: string }>;
6 |
7 | export const main = createProtectedHandler(async (event) => {
8 |
9 | return httpResponse({
10 | searchTerm: event.queryStringParameters?.searchTerm,
11 | })
12 | });
13 |
--------------------------------------------------------------------------------
/tools/generators/http-handler/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tree,
3 | names,
4 | generateFiles,
5 | joinPathFragments,
6 | getProjects,
7 | logger
8 | } from '@nrwl/devkit';
9 |
10 | import {
11 | Project,
12 | ScriptTarget,
13 | SyntaxKind,
14 | ObjectLiteralExpression,
15 | Writers,
16 | PropertyAssignment,
17 | } from 'ts-morph';
18 |
19 | const project = new Project({
20 | compilerOptions: {
21 | target: ScriptTarget.ES2020,
22 | },
23 | });
24 |
25 | interface Schema {
26 | name: string;
27 | project: string;
28 | method: string;
29 | path: string;
30 | }
31 |
32 | export default async function (tree: Tree, schema: Schema) {
33 | if (!getProjects(tree).has(schema.project)) {
34 | logger.error(`Project ${schema.project} does not exist.`);
35 |
36 | return;
37 | }
38 |
39 | const root = `services/${schema.project}`;
40 | const serviceSource = joinPathFragments(root, 'src');
41 |
42 | const n = names(schema.name);
43 | const serverlessPath = joinPathFragments(`services/${schema.project}`, 'serverless.ts');
44 | const serverless = tree.read(serverlessPath).toString()
45 |
46 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceSource, {
47 | ...schema,
48 | tmpl: '',
49 | fileName: n.fileName,
50 | });
51 |
52 | const sourceFile = project.createSourceFile('serverless.ts', serverless);
53 | const dec = sourceFile.getVariableDeclaration('serverlessConfig');
54 | const objectLiteralExpression = dec!.getInitializerIfKindOrThrow(
55 | SyntaxKind.ObjectLiteralExpression
56 | ) as ObjectLiteralExpression;
57 |
58 | const funcProp = objectLiteralExpression.getProperty(
59 | 'functions'
60 | ) as PropertyAssignment;
61 |
62 | const funcValue = funcProp.getInitializer() as ObjectLiteralExpression;
63 |
64 | funcValue.addPropertyAssignment({
65 | initializer: (writer) => {
66 | return Writers.object({
67 | handler: `'src/${n.fileName}/${n.fileName}-handler.main'`,
68 | events: (writer) => {
69 | writer.write('[');
70 | Writers.object({
71 | http: Writers.object({
72 | method: `'${schema.method.toLowerCase()}'`,
73 | path: `'${schema.path}'`,
74 | }),
75 | })(writer);
76 | writer.write(']');
77 | },
78 | })(writer);
79 | },
80 | name: `'${n.fileName}'`,
81 | });
82 |
83 | sourceFile.formatText({ indentSize: 2 });
84 |
85 | tree.write(serverlessPath, sourceFile.getText());
86 | }
87 |
--------------------------------------------------------------------------------
/tools/generators/http-handler/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema",
3 | "cli": "nx",
4 | "$id": "http-handler",
5 | "type": "object",
6 | "properties": {
7 | "name": {
8 | "type": "string",
9 | "description": "Handler name",
10 | "x-prompt": "What is the name of the handler?",
11 | "$default": {
12 | "$source": "argv",
13 | "index": 0
14 | }
15 | },
16 | "project": {
17 | "type": "string",
18 | "description": "Project name",
19 | "x-prompt": "What is the name of the project?"
20 | },
21 | "method": {
22 | "description": "The HTTP method",
23 | "type": "string",
24 | "default": "GET",
25 | "enum": [
26 | "GET",
27 | "POST",
28 | "PUT",
29 | "DELETE",
30 | "PATCH",
31 | "HEAD",
32 | "OPTIONS"
33 | ],
34 | "x-prompt": {
35 | "message": "Which HTTP method you want to use?",
36 | "type": "list",
37 | "items": [
38 | {
39 | "value": "GET",
40 | "label": "GET"
41 | },
42 | {
43 | "value": "POST",
44 | "label": "POST"
45 | },
46 | {
47 | "value": "PUT",
48 | "label": "PUT"
49 | },
50 | {
51 | "value": "DELETE",
52 | "label": "DELETE"
53 | },
54 | {
55 | "value": "PATCH",
56 | "label": "PATCH"
57 | },
58 | {
59 | "value": "HEAD",
60 | "label": "HEAD"
61 | },
62 | {
63 | "value": "OPTIONS",
64 | "label": "OPTIONS"
65 | }
66 | ]
67 | }
68 | },
69 | "path": {
70 | "type": "string",
71 | "description": "Path of the handler",
72 | "x-prompt": "What is the handler path?"
73 | }
74 | },
75 | "required": [
76 | "name",
77 | "project",
78 | "path",
79 | "method"
80 | ]
81 | }
--------------------------------------------------------------------------------
/tools/generators/model/files/__fileName__.model.ts__tmpl__:
--------------------------------------------------------------------------------
1 | import {
2 | createItem,
3 | getItem,
4 | } from '@app/db/operations';
5 | import { Item, ItemKeys } from '@app/db/item';
6 | import { DynamoDB } from 'aws-sdk';
7 |
8 | export interface <%= className %>Model {
9 | id: string;
10 | }
11 |
12 | export class <%= className %>Keys extends ItemKeys {
13 | static ENTITY_TYPE = '<%= constantName %>';
14 |
15 | constructor(private id: string) {
16 | super();
17 | }
18 |
19 | get pk() {
20 | return `${<%= className %>Keys.ENTITY_TYPE}#${this.id}`;
21 | }
22 |
23 | get sk() {
24 | return this.pk;
25 | }
26 | }
27 |
28 | export class <%= className %> extends Item<<%= className %>Model> {
29 | constructor(public <%= propertyName %>: <%= className %>Model) {
30 | super();
31 | }
32 |
33 | get keys() {
34 | return new <%= className %>Keys(this.<%= propertyName %>.id);
35 | }
36 |
37 | static fromItem(attributeMap: DynamoDB.AttributeMap): <%= className %>Model {
38 | return {
39 | id: attributeMap.id.S
40 | };
41 | }
42 |
43 | toItem() {
44 | return this.marshall(this.<%= propertyName %>);
45 | }
46 | }
47 |
48 |
49 | export async function create<%= className %>(<%= propertyName %>: <%= className %>): Promise<<%= className %>Model> {
50 | await createItem(<%= propertyName %>);
51 |
52 | return <%= className %>.fromItem(<%= propertyName %>.toItem());
53 | }
54 |
55 | export async function get<%= className %>(<%= propertyName %>Keys: <%= className %>Keys) {
56 | const result = await getItem(<%= propertyName %>Keys);
57 |
58 | return <%= className %>.fromItem(result.Item);
59 | }
60 |
--------------------------------------------------------------------------------
/tools/generators/model/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Tree,
3 | formatFiles,
4 | installPackagesTask,
5 | names,
6 | generateFiles,
7 | joinPathFragments,
8 | } from '@nrwl/devkit';
9 |
10 | interface Schema {
11 | name: string;
12 | project: string;
13 | }
14 |
15 | export default async function (tree: Tree, schema: Schema) {
16 | const serviceRoot = `services/${schema.project}/src`;
17 |
18 | generateFiles(tree, joinPathFragments(__dirname, './files'), serviceRoot, {
19 | ...schema,
20 | tmpl: '',
21 | ...names(schema.name),
22 | });
23 | }
24 |
--------------------------------------------------------------------------------
/tools/generators/model/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/schema",
3 | "cli": "nx",
4 | "$id": "model",
5 | "type": "object",
6 | "properties": {
7 | "name": {
8 | "type": "string",
9 | "description": "Model name",
10 | "x-prompt": "What is the name of the model?",
11 | "$default": {
12 | "$source": "argv",
13 | "index": 0
14 | }
15 | },
16 | "project": {
17 | "type": "string",
18 | "description": "Project name",
19 | "x-prompt": "What is the name of the project?"
20 | }
21 | },
22 | "required": [
23 | "name",
24 | "project"
25 | ]
26 | }
--------------------------------------------------------------------------------
/tools/generators/service/files/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../.eslintrc.json",
3 | "ignorePatterns": ["!**/*"],
4 | "rules": {}
5 | }
6 |
--------------------------------------------------------------------------------
/tools/generators/service/files/README.md:
--------------------------------------------------------------------------------
1 | # <%= name %>
2 |
3 | This stack was generated with [Nx](https://nx.dev).
4 |
5 | ## Running unit tests
6 |
7 | Run `nx test <%= name %>` to execute the unit tests via [Jest](https://jestjs.io).
8 |
--------------------------------------------------------------------------------
/tools/generators/service/files/serverless.ts__tmpl__:
--------------------------------------------------------------------------------
1 | import type { Serverless } from 'serverless/aws';
2 | import { baseServerlessConfig } from '../../serverless.base';
3 |
4 | const serverlessConfig: Partial = {
5 | ...baseServerlessConfig,
6 | service: '<%= name %>',
7 | custom: {
8 | ...baseServerlessConfig.custom,
9 | 'serverless-offline': {
10 | lambdaPort: 3005,
11 | httpPort: 3006,
12 | },
13 | },
14 | functions: {},
15 | }
16 |
17 | module.exports = serverlessConfig;
--------------------------------------------------------------------------------
/tools/generators/service/files/src/__fileName__.types.ts__tmpl__:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/tools/generators/service/files/src/__fileName__.types.ts__tmpl__
--------------------------------------------------------------------------------
/tools/generators/service/files/src/__fileName__.utils.ts__tmpl__:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ngneat/nx-serverless/34ded7c4083c2962bad956cafa0772a59899b21e/tools/generators/service/files/src/__fileName__.utils.ts__tmpl__
--------------------------------------------------------------------------------
/tools/generators/service/files/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "types": ["node"]
6 | },
7 | "exclude": ["**/*.spec.ts"],
8 | "include": ["**/*.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tools/generators/service/files/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.base.json",
3 | "files": [],
4 | "include": [],
5 | "references": [
6 | {
7 | "path": "./tsconfig.app.json"
8 | },
9 | {
10 | "path": "./tsconfig.spec.json"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/tools/generators/service/files/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "../../dist/out-tsc",
5 | "module": "commonjs",
6 | "types": ["jest", "node"]
7 | },
8 | "include": ["**/*.spec.ts", "**/*.d.ts"]
9 | }
10 |
--------------------------------------------------------------------------------
/tools/generators/service/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | formatFiles,
3 | generateFiles,
4 | installPackagesTask,
5 | joinPathFragments,
6 | names,
7 | Tree,
8 | } from '@nrwl/devkit';
9 | import { Schema } from './schema';
10 | import { addJest } from './jest-config';
11 | import { addWorkspaceConfig } from './workspace-config';
12 |
13 | export default async (host: Tree, schema: Schema) => {
14 | const serviceRoot = `services/${schema.name}`;
15 |
16 | const { fileName } = names(schema.name);
17 |
18 | generateFiles(host, joinPathFragments(__dirname, './files'), serviceRoot, {
19 | ...schema,
20 | tmpl: '',
21 | fileName,
22 | });
23 |
24 | addWorkspaceConfig(host, schema.name, serviceRoot);
25 |
26 | await addJest(host, schema.name);
27 |
28 | await formatFiles(host);
29 |
30 | return () => {
31 | installPackagesTask(host);
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/tools/generators/service/jest-config.ts:
--------------------------------------------------------------------------------
1 | import { Tree } from '@nrwl/devkit';
2 | import { jestProjectGenerator } from '@nrwl/jest';
3 | import { JestProjectSchema } from '@nrwl/jest/src/generators/jest-project/schema';
4 |
5 | export const addJest = async (host: Tree, projectName: string) => {
6 | await jestProjectGenerator(host, {
7 | project: projectName,
8 | setupFile: 'none',
9 | testEnvironment: 'node',
10 | skipSerializers: false,
11 | skipSetupFile: false,
12 | supportTsx: false,
13 | babelJest: false,
14 | skipFormat: true,
15 | });
16 | };
17 |
--------------------------------------------------------------------------------
/tools/generators/service/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "cli": "nx",
3 | "id": "service",
4 | "type": "object",
5 | "properties": {
6 | "name": {
7 | "type": "string",
8 | "description": "Service name",
9 | "x-prompt": "What is the name of the service?",
10 | "$default": {
11 | "$source": "argv",
12 | "index": 0
13 | }
14 | }
15 | },
16 | "required": [
17 | "name"
18 | ]
19 | }
--------------------------------------------------------------------------------
/tools/generators/service/schema.ts:
--------------------------------------------------------------------------------
1 | export type Schema = {
2 | readonly name: string;
3 | };
4 |
--------------------------------------------------------------------------------
/tools/generators/service/workspace-config.ts:
--------------------------------------------------------------------------------
1 | import { addProjectConfiguration, Tree } from '@nrwl/devkit';
2 |
3 | const buildRunCommandConfig = (dir: string, command: string) => ({
4 | executor: '@nrwl/workspace:run-commands',
5 | options: {
6 | cwd: dir,
7 | color: true,
8 | command: command,
9 | },
10 | });
11 |
12 | export const addWorkspaceConfig = (
13 | host: Tree,
14 | projectName: string,
15 | serviceRoot: string
16 | ) => {
17 | addProjectConfiguration(host, projectName, {
18 | root: serviceRoot,
19 | projectType: 'application',
20 | sourceRoot: `${serviceRoot}/src`,
21 | targets: {
22 | build: {
23 | ...buildRunCommandConfig(serviceRoot, 'sls package'),
24 | },
25 | serve: {
26 | ...buildRunCommandConfig(serviceRoot, 'sls offline start'),
27 | },
28 | deploy: {
29 | ...buildRunCommandConfig(serviceRoot, 'sls deploy --verbose'),
30 | dependsOn: [
31 | {
32 | target: 'deploy',
33 | projects: 'dependencies',
34 | },
35 | ],
36 | },
37 | remove: {
38 | ...buildRunCommandConfig(serviceRoot, 'sls remove'),
39 | },
40 | lint: {
41 | executor: '@nrwl/linter:eslint',
42 | options: {
43 | lintFilePatterns: [`${serviceRoot}/**/*.ts`],
44 | },
45 | },
46 | },
47 | tags: ['service'],
48 | implicitDependencies: ['core'],
49 | });
50 | };
51 |
--------------------------------------------------------------------------------
/tools/tsconfig.tools.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.base.json",
3 | "compilerOptions": {
4 | "outDir": "../dist/out-tsc/tools",
5 | "rootDir": ".",
6 | "module": "commonjs",
7 | "target": "ES2020",
8 | "types": ["node"],
9 | "importHelpers": false
10 | },
11 | "include": ["**/*.ts"]
12 | }
13 |
--------------------------------------------------------------------------------
/tsconfig.base.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "rootDir": ".",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "allowSyntheticDefaultImports": true,
11 | "esModuleInterop": true,
12 | "importHelpers": false,
13 | "target": "ES2020",
14 | "module": "commonjs",
15 | "lib": ["esnext"],
16 | "skipLibCheck": true,
17 | "skipDefaultLibCheck": true,
18 | "baseUrl": ".",
19 | "paths": {
20 | "@app/auth/*": ["libs/auth/src/lib/*"],
21 | "@app/db/*": ["libs/db/src/lib/*"],
22 | "@app/env": ["environments/environment.ts"],
23 | "@app/http/*": ["libs/http/src/lib/*"],
24 | "@app/users/*": ["services/users/src/*"]
25 | }
26 | },
27 | "exclude": ["node_modules", "tmp"]
28 | }
29 |
--------------------------------------------------------------------------------
/workspace.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": 2,
3 | "projects": {
4 | "auth": "services/auth",
5 | "core": "services/core",
6 | "db": "libs/db",
7 | "http": "libs/http",
8 | "todos": "services/todos",
9 | "users": "services/users"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------