@ackee/create-node-app 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/AUTHORS +2 -1
  2. package/README.md +26 -18
  3. package/docs/development.md +42 -0
  4. package/lib/Bootstrap.js +106 -65
  5. package/lib/Bootstrap.js.map +1 -1
  6. package/lib/Builder.js +111 -0
  7. package/lib/Builder.js.map +1 -0
  8. package/lib/Files.js +21 -0
  9. package/lib/Files.js.map +1 -0
  10. package/lib/Logger.js +26 -8
  11. package/lib/Logger.js.map +1 -1
  12. package/lib/Mergers/ConfigMerger.js +22 -0
  13. package/lib/Mergers/ConfigMerger.js.map +1 -0
  14. package/lib/Mergers/ContainerMerger.js +172 -0
  15. package/lib/Mergers/ContainerMerger.js.map +1 -0
  16. package/lib/Mergers/EnvJsoncMerger.js +20 -0
  17. package/lib/Mergers/EnvJsoncMerger.js.map +1 -0
  18. package/lib/Mergers/Merger.js +36 -0
  19. package/lib/Mergers/Merger.js.map +1 -0
  20. package/lib/Mergers/PackageJsonMerger.js +36 -0
  21. package/lib/Mergers/PackageJsonMerger.js.map +1 -0
  22. package/lib/Npm.js +40 -12
  23. package/lib/Npm.js.map +1 -1
  24. package/lib/PackageJson.js +4 -4
  25. package/lib/PackageJson.js.map +1 -1
  26. package/lib/StarterLoader.js +86 -0
  27. package/lib/StarterLoader.js.map +1 -0
  28. package/package.json +8 -5
  29. package/src/Bootstrap.ts +123 -82
  30. package/src/Builder.ts +172 -0
  31. package/src/Files.ts +22 -0
  32. package/src/Logger.ts +26 -7
  33. package/src/Mergers/ConfigMerger.ts +28 -0
  34. package/src/Mergers/ContainerMerger.ts +241 -0
  35. package/src/Mergers/EnvJsoncMerger.ts +24 -0
  36. package/src/Mergers/Merger.ts +51 -0
  37. package/src/Mergers/PackageJsonMerger.ts +45 -0
  38. package/src/Npm.ts +60 -15
  39. package/src/PackageJson.ts +6 -4
  40. package/src/Starter.ts +2 -2
  41. package/src/StarterLoader.ts +148 -0
  42. package/starter/{cloudrun → _base}/.env.jsonc +1 -5
  43. package/starter/{cloudrun → _base}/.eslintrc.cjs +3 -2
  44. package/starter/_base/README.md +53 -0
  45. package/starter/_base/package.json +45 -0
  46. package/starter/{cloudrun → _base}/src/adapters/pino.logger.ts +1 -1
  47. package/starter/_base/src/config.ts +16 -0
  48. package/starter/{cloudrun-graphql → _base}/src/container.ts +3 -1
  49. package/starter/_base/src/index.ts +14 -0
  50. package/starter/{cloudrun → _base}/src/view/cli/README.md +2 -6
  51. package/starter/api/graphql/.env.jsonc +8 -0
  52. package/starter/{cloudrun-graphql → api/graphql}/.eslintrc.cjs +4 -5
  53. package/starter/api/graphql/node-app.jsonc +6 -0
  54. package/starter/api/graphql/package.json +36 -0
  55. package/starter/{cloudrun-graphql → api/graphql}/src/config.ts +0 -4
  56. package/starter/api/graphql/src/index.ts +11 -0
  57. package/starter/{cloudrun-graphql → api/graphql}/src/test/helloWorld.test.ts +14 -3
  58. package/starter/api/graphql/src/view/graphql/context-factory.ts +13 -0
  59. package/starter/{cloudrun-graphql → api/graphql}/src/view/server.ts +16 -6
  60. package/starter/api/rest/.env.jsonc +6 -0
  61. package/starter/api/rest/.eslintrc.cjs +8 -0
  62. package/starter/api/rest/node-app.jsonc +6 -0
  63. package/starter/api/rest/package.json +25 -0
  64. package/starter/{cloudrun → api/rest}/src/config.ts +0 -5
  65. package/starter/api/rest/src/container.ts +13 -0
  66. package/starter/{cloudrun → api/rest}/src/index.ts +4 -4
  67. package/starter/{cloudrun → api/rest}/src/test/health-check.test.ts +3 -5
  68. package/starter/{cloudrun → api/rest}/src/test/util/openapi-test.util.ts +3 -3
  69. package/starter/{cloudrun → api/rest}/src/view/rest/middleware/error-handler.ts +1 -1
  70. package/starter/{cloudrun → api/rest}/src/view/rest/routes.ts +1 -1
  71. package/starter/{cloudrun → api/rest}/src/view/rest/util/openapi.util.ts +19 -19
  72. package/starter/{cloudrun → api/rest}/src/view/server.ts +6 -4
  73. package/starter/infra/postgresql-knex/.env.jsonc +5 -0
  74. package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.yml +1 -1
  75. package/starter/infra/postgresql-knex/knexfile.ts +16 -0
  76. package/starter/infra/postgresql-knex/node-app.jsonc +6 -0
  77. package/starter/infra/postgresql-knex/package.json +13 -0
  78. package/starter/infra/postgresql-knex/src/adapters/knex.database.test.ts +21 -0
  79. package/starter/infra/postgresql-knex/src/adapters/knex.database.ts +14 -0
  80. package/starter/infra/postgresql-knex/src/adapters/repositories/migration.repository.ts +24 -0
  81. package/starter/infra/postgresql-knex/src/config.ts +14 -0
  82. package/starter/infra/postgresql-knex/src/container.ts +23 -0
  83. package/starter/infra/postgresql-knex/src/db/migration.template.ts +4 -0
  84. package/starter/infra/postgresql-knex/src/db/migrations/.gitkeep +0 -0
  85. package/starter/infra/postgresql-knex/src/db/seed.template.ts +3 -0
  86. package/starter/infra/postgresql-knex/src/db/seeds/.gitkeep +0 -0
  87. package/starter/infra/postgresql-knex/src/domain/ports/database.d.ts +4 -0
  88. package/starter/infra/postgresql-knex/src/domain/ports/repositories/migration.repository.d.ts +9 -0
  89. package/starter/infra/postgresql-knex/src/test/setup.ts +16 -0
  90. package/starter/{shared → pipeline/cloudrun-gitlab}/.gitlab-ci.yml +15 -6
  91. package/starter/pipeline/cloudrun-gitlab/node-app.jsonc +6 -0
  92. package/tsconfig.tsbuildinfo +1 -1
  93. package/lib/Toolbelt.js +0 -102
  94. package/lib/Toolbelt.js.map +0 -1
  95. package/lib/cloudrun/CloudRunStarter.js +0 -127
  96. package/lib/cloudrun/CloudRunStarter.js.map +0 -1
  97. package/lib/cloudrun-graphql/GraphQLStarter.js +0 -118
  98. package/lib/cloudrun-graphql/GraphQLStarter.js.map +0 -1
  99. package/src/Toolbelt.ts +0 -132
  100. package/src/cloudrun/CloudRunStarter.ts +0 -182
  101. package/src/cloudrun-graphql/GraphQLStarter.ts +0 -182
  102. package/starter/cloudrun/README.md +0 -69
  103. package/starter/cloudrun/src/container.ts +0 -18
  104. package/starter/cloudrun/src/context.ts +0 -39
  105. package/starter/cloudrun/src/domain/errors/codes.ts +0 -9
  106. package/starter/cloudrun/src/domain/errors/errors.ts +0 -25
  107. package/starter/cloudrun/src/domain/ports/logger.d.ts +0 -21
  108. package/starter/cloudrun-graphql/.env.jsonc +0 -12
  109. package/starter/cloudrun-graphql/README.md +0 -53
  110. package/starter/cloudrun-graphql/src/adapters/pino.logger.ts +0 -44
  111. package/starter/cloudrun-graphql/src/index.ts +0 -11
  112. package/starter/shared/.gitignore_ +0 -5
  113. package/starter/shared/ci-branch-config/common.env +0 -7
  114. package/starter/shared/ci-branch-config/development.env +0 -7
  115. package/starter/shared/ci-branch-config/master.env +0 -7
  116. package/starter/shared/ci-branch-config/stage.env +0 -7
  117. package/starter/shared/docker-compose/docker-compose.override.yml +0 -5
  118. package/starter/shared/jest.config.js +0 -12
  119. /package/starter/{shared → _base}/.dockerignore +0 -0
  120. /package/starter/{cloudrun → _base}/.eslint.tsconfig.json +0 -0
  121. /package/starter/{shared → _base}/.mocha-junit-config.json +0 -0
  122. /package/starter/{shared → _base}/.mocharc.json +0 -0
  123. /package/starter/{shared → _base}/.nvmrc +0 -0
  124. /package/starter/{shared → _base}/Dockerfile +0 -0
  125. /package/starter/{shared → _base}/prettier.config.cjs +0 -0
  126. /package/starter/{cloudrun-graphql → _base}/src/context.ts +0 -0
  127. /package/starter/{cloudrun-graphql → _base}/src/domain/errors/codes.ts +0 -0
  128. /package/starter/{cloudrun-graphql → _base}/src/domain/errors/errors.ts +0 -0
  129. /package/starter/{cloudrun-graphql → _base}/src/domain/ports/logger.d.ts +0 -0
  130. /package/starter/{shared → _base}/src/test/setup.ts +0 -0
  131. /package/starter/{cloudrun → _base}/src/view/cli/cli.ts +0 -0
  132. /package/starter/{shared → _base}/tsconfig.json +0 -0
  133. /package/starter/{cloudrun-graphql → api/graphql}/.eslint.tsconfig.json +0 -0
  134. /package/starter/{cloudrun-graphql → api/graphql}/codegen.yml +0 -0
  135. /package/starter/{cloudrun-graphql → api/graphql}/src/view/controller.ts +0 -0
  136. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers/greeting.resolver.ts +0 -0
  137. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/resolvers.ts +0 -0
  138. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema/schema.graphql +0 -0
  139. /package/starter/{cloudrun-graphql → api/graphql}/src/view/graphql/schema.ts +0 -0
  140. /package/starter/{cloudrun → api/rest}/src/domain/health-check.service.ts +0 -0
  141. /package/starter/{cloudrun → api/rest}/src/view/cli/openapi/generate.ts +0 -0
  142. /package/starter/{cloudrun/src/view/rest/controller → api/rest/src/view/rest/controllers}/health-check.controller.ts +0 -0
  143. /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/context-middleware.ts +0 -0
  144. /package/starter/{cloudrun → api/rest}/src/view/rest/middleware/request-logger.ts +0 -0
  145. /package/starter/{cloudrun → api/rest}/src/view/rest/request.d.ts +0 -0
  146. /package/starter/{cloudrun → api/rest}/src/view/rest/spec/openapi.yml +0 -0
  147. /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose-entrypoint.sh +0 -0
  148. /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.ci.yml +0 -0
  149. /package/starter/{shared → infra/postgresql-knex}/docker-compose/docker-compose.local.yml +0 -0
@@ -0,0 +1,53 @@
1
+ # Create-Node-App Project
2
+
3
+ Node.js application bootstrapped with [create-node-app](https://github.com/AckeeCZ/create-node-app)
4
+
5
+ ## 🏗️ Architecture
6
+
7
+ All domain logic should live inside the [domain folder](src/domain) and should be accessed only via [container](src/container.ts). Each external dependency should be referenced by its own port inside the [domain/port](src/domain/ports) folder. These ports are also part of the container and can be implemented by any external dependency inside the [adapters](src/adapters/) folder.
8
+
9
+ The application should be accessed through the [view](src/view/) layer of the application. All UI and interfaces should be defined there. The main entrypoint is defined in the [index.ts](src/index.ts) file.
10
+
11
+ Each domain service should always accept [RequestContext](src/context.ts) as the first parameter. Part of the context is also the container with all of the application dependencies.
12
+
13
+ ## 👷 Development
14
+
15
+ Application configuration is handled by [Configuru](https://github.com/AckeeCZ/configuru). All env options that can be changed or need to be set up are described in [.env.jsonc](.env.jsonc).
16
+
17
+ The whole codebase is checked and improved using lint and prettier, run `lint:fix` and `prettier:fix` before committing changes to git.
18
+
19
+ ```bash
20
+ npm run lint:fix
21
+ npm run prettier:fix
22
+ ```
23
+
24
+ ## ✅ Tests
25
+
26
+ Test are written using [mocha](https://github.com/mochajs/mocha). They are maintained in two ways:
27
+
28
+ - **unit tests** should be next to the tested file with `test.ts` suffix,
29
+ - **integration tests** should be located inside the [test](src/test/) folder.
30
+
31
+ Use mocked container during integration tests.
32
+
33
+ If you need any prerequisites during tests, use the [setup.ts](src/test/setup.ts) file.
34
+
35
+ To run tests use the test command:
36
+
37
+ ```bash
38
+ npm run test
39
+ ```
40
+
41
+ ## 🚀 Quick start
42
+
43
+ 1. Build the code
44
+
45
+ ```bash
46
+ npm run build
47
+ ```
48
+
49
+ 2. Start the entrypoint / server
50
+
51
+ ```bash
52
+ npm run start
53
+ ```
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "app",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "test": "mocha",
7
+ "build": "tsc",
8
+ "start": "node -r source-map-support/register dist/index.js",
9
+ "ci-test:no-coverage": "npm run test -- --parallel=false -R mocha-multi-reporters --reporter-options configFile=.mocha-junit-config.json",
10
+ "ci-test": "nyc -a -r cobertura --report-dir output npm run ci-test:no-coverage",
11
+ "prettier": "prettier --check --write '**/*.{ts,js,json,jsonc,md}'",
12
+ "lint": "eslint '**/*.ts' -f codeframe --fix",
13
+ "codestyle": "npm run lint && npm run prettier",
14
+ "ci-lint": "npm run lint -- -f checkstyle -o ./output/checkstyle-result.xml",
15
+ "cli": "tsx ./src/view/cli/cli.ts"
16
+ },
17
+ "keywords": [],
18
+ "author": "",
19
+ "license": "ISC",
20
+ "description": "",
21
+ "dependencies": {
22
+ "configuru": "^1.0.0",
23
+ "pino": "^9.0.0",
24
+ "source-map-support": "^0.5.0"
25
+ },
26
+ "devDependencies": {
27
+ "@ackee/styleguide-backend-config": "^1.0.1",
28
+ "@istanbuljs/nyc-config-typescript": "^1.0.0",
29
+ "@types/mocha": "^10.0.0",
30
+ "@types/node": "^24.0.0",
31
+ "eslint": "^8.0.0",
32
+ "eslint-formatter-gitlab": "^5.0.0",
33
+ "mocha": "^11.0.0",
34
+ "mocha-junit-reporter": "^2.0.0",
35
+ "mocha-multi-reporters": "^1.0.0",
36
+ "nyc": "^17.0.0",
37
+ "pino-pretty": "^13.0.0",
38
+ "prettier": "^3.0.0",
39
+ "ts-node": "^10.0.0",
40
+ "tsx": "^4.0.0",
41
+ "typescript": "^5.0.0",
42
+ "yargs": "^18.0.0"
43
+ },
44
+ "type": "module"
45
+ }
@@ -1,4 +1,4 @@
1
- import { pino as pinoLogger } from 'pino'
1
+ import pinoLogger from 'pino'
2
2
  import { LoggerFactoryPort } from '../domain/ports/logger.d.js'
3
3
 
4
4
  // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
@@ -0,0 +1,16 @@
1
+ import { createLoader, maskedValues, values } from 'configuru'
2
+ import { Level } from 'pino'
3
+
4
+ const loader = createLoader({
5
+ defaultConfigPath: '.env.jsonc',
6
+ })
7
+
8
+ const configSchema = {
9
+ logger: {
10
+ defaultLevel: loader.custom(x => x as Level)('LOGGER_DEFAULT_LEVEL'),
11
+ pretty: loader.bool('LOGGER_PRETTY'),
12
+ },
13
+ }
14
+
15
+ export const config = values(configSchema)
16
+ export const safeConfig = maskedValues(configSchema)
@@ -6,7 +6,9 @@ export interface Container {
6
6
  logger: LoggerPort
7
7
  }
8
8
 
9
- export const createContainer = (): Container => {
9
+ export type ContainerFactory = () => Promise<Container>
10
+
11
+ export const createContainer: ContainerFactory = async () => {
10
12
  const logger = pinoLoggerFactory.create(config.logger)
11
13
 
12
14
  return {
@@ -0,0 +1,14 @@
1
+ import { createContainer } from './container.js'
2
+ import { RequestContext } from './context.js'
3
+
4
+ const entrypoint = async () => {
5
+ const ctx: RequestContext = {
6
+ container: await createContainer(),
7
+ type: 'api-user',
8
+ user: null,
9
+ }
10
+
11
+ ctx.container.logger.info('Hello World! 🎉')
12
+ }
13
+
14
+ void entrypoint()
@@ -4,13 +4,9 @@ Cli tool to operate scripts that can use internal services or to add development
4
4
 
5
5
  ## 👷 Development
6
6
 
7
- The CLI tool is build around `yargs` library. Each command needs its own `ts` file in current folder or in subfolder. The entrypoint `Cli.ts` scans the sub folders for `ts` files and intrerprets them as a command under the module called same as the folder.
7
+ The CLI tool is build around `yargs` library. Each command needs its own `ts` file in current folder or in subfolder. The entrypoint `Cli.ts` scans the sub folders for `ts` files and intrerprets them as a command under the module called same as the folder. Each command should export only functions and variables according to `CommandDefinition` in [cli.ts file](./cli.ts).
8
8
 
9
- For example, current [openapi folder](./openapi) creates a new module `openapi` and assigns a new command called `generate` based on contents in [generate.ts](./openapi/generate.ts) file.
10
-
11
- Each command should export only functions and variables according to `CommandDefinition` in [cli.ts file](./cli.ts).
12
-
13
- Take a look at the [generate.ts](./openapi/generate.ts) for example how to implement own command.
9
+ For example, if you add `openapi` folder, it will creates a new module `openapi`. If you add a new command called `generate.ts`, it will also assign a new generate command to this module.
14
10
 
15
11
  ## ⚠️ Usage in production scripts
16
12
 
@@ -0,0 +1,8 @@
1
+ {
2
+ // Server will run on this port
3
+ "SERVER_PORT": 3000,
4
+ // Response development errors for debugging
5
+ "SERVER_ALLOW_RESPONSE_ERRORS": false,
6
+ // Enable GraphQL introspection
7
+ "SERVER_ENABLE_INTROSPECTION": true
8
+ }
@@ -5,7 +5,7 @@ const defaultConfig = {
5
5
 
6
6
  module.exports = {
7
7
  root: true,
8
- ignorePatterns: ['dist', 'docs', 'src/generated'],
8
+ ignorePatterns: ['dist', 'src/openapi', 'docs', 'knexfile.ts'],
9
9
  overrides: [
10
10
  {
11
11
  ...omit(defaultConfig, ['ignorePatterns']),
@@ -14,10 +14,9 @@ module.exports = {
14
14
  project: '.eslint.tsconfig.json',
15
15
  },
16
16
  rules: {
17
- ...defaultConfig['rules'],
18
- '@typescript-eslint/strict-boolean-expressions': 0,
19
- '@typescript-eslint/no-misused-promises': 'warn',
20
- },
17
+ ...defaultConfig.rules,
18
+ '@typescript-eslint/no-empty-object-type': 0,
19
+ }
21
20
  },
22
21
  {
23
22
  files: ['**/*.graphql'],
@@ -0,0 +1,6 @@
1
+ {
2
+ "module": "API",
3
+ "id": "graphql",
4
+ "name": "GraphQL",
5
+ "prebuild": ["generate:api"]
6
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "app-api-graphql",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "generate:api": "graphql-codegen --config codegen.yml && npm run codestyle",
7
+ "build:copy-schema": "mkdir -p ./dist/view/graphql/schema && cp -r ./src/view/graphql/schema ./dist/view/graphql/schema",
8
+ "build": "npm run build:copy-schema && tsc"
9
+ },
10
+ "keywords": [],
11
+ "author": "",
12
+ "license": "ISC",
13
+ "description": "",
14
+ "dependencies": {
15
+ "@as-integrations/express5": "^1.1.2",
16
+ "@apollo/server": "^4.1.0",
17
+ "@graphql-tools/load-files": "^7.0.0",
18
+ "@graphql-tools/merge": "^9.0.0",
19
+ "@graphql-tools/schema": "^10.0.0",
20
+ "lodash-es": "^4.17.21",
21
+ "express": "^5.0.0",
22
+ "graphql": "^16.0.0",
23
+ "node-healthz": "^2.0.0",
24
+ "pino-http": "^10.0.0"
25
+ },
26
+ "devDependencies": {
27
+ "@graphql-codegen/cli": "^5.0.0",
28
+ "@graphql-codegen/typescript": "^4.0.0",
29
+ "@graphql-codegen/typescript-resolvers": "^4.0.0",
30
+ "@graphql-eslint/eslint-plugin": "^4.4.0",
31
+ "@types/express": "^5.0.0",
32
+ "@types/supertest": "^6.0.0",
33
+ "supertest": "^7.0.0"
34
+ },
35
+ "type": "module"
36
+ }
@@ -11,10 +11,6 @@ const configSchema = {
11
11
  allowResponseErrors: loader.bool('SERVER_ALLOW_RESPONSE_ERRORS'),
12
12
  enableIntrospection: loader.bool('SERVER_ENABLE_INTROSPECTION'),
13
13
  },
14
- logger: {
15
- defaultLevel: loader.custom(x => x as any as Level)('LOGGER_DEFAULT_LEVEL'),
16
- pretty: loader.bool('LOGGER_PRETTY'),
17
- },
18
14
  }
19
15
 
20
16
  export const config = values(configSchema)
@@ -0,0 +1,11 @@
1
+ import { config, safeConfig } from './config.js'
2
+ import { createAppServer, startServer } from './view/server.js'
3
+ import { pinoLoggerFactory } from './adapters/pino.logger.js'
4
+ import { createContainer } from './container.js'
5
+
6
+ const logger = pinoLoggerFactory.create(config.logger)
7
+
8
+ logger.info({ config: safeConfig }, 'Loaded config')
9
+
10
+ const appServer = createAppServer()
11
+ void startServer({ ...appServer, logger, containerFactory: createContainer })
@@ -1,16 +1,27 @@
1
1
  import assert from 'node:assert'
2
- import { describe, it } from 'mocha'
2
+ import { describe, before, test } from 'mocha'
3
3
  import { createAppServer } from '../view/server.js'
4
4
  import { gql } from 'graphql-tag'
5
+ import { ApolloServer } from '@apollo/server'
5
6
 
6
7
  describe('Hello world', () => {
7
- it('should return greeting', async () => {
8
+ let server: ApolloServer
9
+
10
+ before(async () => {
11
+ const { server: appServer } = createAppServer()
12
+ server = appServer
13
+ })
14
+
15
+ after(async () => {
16
+ await server.stop()
17
+ })
18
+
19
+ test('should return greeting', async () => {
8
20
  const query = gql`
9
21
  query Hello {
10
22
  greeting
11
23
  }
12
24
  `
13
- const { server } = createAppServer()
14
25
  const res = await server.executeOperation({ query })
15
26
 
16
27
  assert(res.body.kind === 'single')
@@ -0,0 +1,13 @@
1
+ import { RequestContext } from '../../context.js'
2
+ import { ContainerFactory } from '../../container.js'
3
+
4
+ export const contextFactory = async (
5
+ containerFactory: ContainerFactory
6
+ ): Promise<RequestContext> => {
7
+ const container = await containerFactory()
8
+ return {
9
+ type: 'api-user',
10
+ container,
11
+ user: null, // Add UserContext if Auth is implemented
12
+ }
13
+ }
@@ -6,7 +6,9 @@ import { schema } from './graphql/schema.js'
6
6
  import express from 'express'
7
7
  import { ctrl } from './controller.js'
8
8
  import { expressMiddleware } from '@as-integrations/express5'
9
- import { Container } from '../container.js'
9
+ import { LoggerPort } from '../domain/ports/logger.d.js'
10
+ import { ContainerFactory } from '../container.js'
11
+ import { contextFactory } from './graphql/context-factory.js'
10
12
 
11
13
  export const clientSchema = makeExecutableSchema({
12
14
  typeDefs: schema,
@@ -29,13 +31,21 @@ export const createAppServer = () => {
29
31
  export async function startServer({
30
32
  app,
31
33
  server,
32
- container,
33
- }: ReturnType<typeof createAppServer> & { container: Container }) {
34
- const { logger } = container
35
-
34
+ logger,
35
+ containerFactory,
36
+ }: ReturnType<typeof createAppServer> & {
37
+ logger: LoggerPort
38
+ containerFactory: ContainerFactory
39
+ }) {
36
40
  await server.start()
37
41
 
38
- app.use('/api/graphql', ctrl.json, expressMiddleware(server))
42
+ app.use(
43
+ '/api/graphql',
44
+ ctrl.json,
45
+ expressMiddleware(server, {
46
+ context: () => contextFactory(containerFactory),
47
+ })
48
+ )
39
49
 
40
50
  app.listen({ port: config.server.port }, () =>
41
51
  logger.info({}, `Server started, port=${config.server.port}`)
@@ -0,0 +1,6 @@
1
+ {
2
+ // API server listening port.
3
+ "SERVER_PORT": 3000,
4
+ // Boolean to remove sensitive info from http error responses
5
+ "ENABLE_PRODUCTION_HTTP_ERROR_RESPONSES": false
6
+ }
@@ -0,0 +1,8 @@
1
+ module.exports = {
2
+ ...require('@ackee/styleguide-backend-config/eslint'),
3
+ root: true,
4
+ ignorePatterns: ['dist', 'src/openapi', 'docs', 'knexfile.ts'],
5
+ parserOptions: {
6
+ project: '.eslint.tsconfig.json',
7
+ },
8
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "module": "API",
3
+ "id": "rest",
4
+ "name": "RESTful",
5
+ "prebuild": ["generate:api"]
6
+ }
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "app-api-rest",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "scripts": {
6
+ "generate:api": "npm run cli openapi generate src/view/rest/spec/openapi.yml && npm run codestyle"
7
+ },
8
+ "keywords": [],
9
+ "author": "",
10
+ "license": "ISC",
11
+ "description": "",
12
+ "dependencies": {
13
+ "express": "^5.0.0",
14
+ "node-healthz": "^2.0.0",
15
+ "pino-http": "^10.0.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/express": "^5.0.0",
19
+ "@types/supertest": "^6.0.0",
20
+ "openapi-typescript": "^7.0.0",
21
+ "supertest": "^7.0.0",
22
+ "yaml": "^2.0.0"
23
+ },
24
+ "type": "module"
25
+ }
@@ -1,15 +1,10 @@
1
1
  import { createLoader, maskedValues, values } from 'configuru'
2
- import { Level } from 'pino'
3
2
 
4
3
  const loader = createLoader({
5
4
  defaultConfigPath: '.env.jsonc',
6
5
  })
7
6
 
8
7
  const configSchema = {
9
- logger: {
10
- defaultLevel: loader.custom(x => x as Level)('LOGGER_DEFAULT_LEVEL'),
11
- pretty: loader.bool('LOGGER_PRETTY'),
12
- },
13
8
  server: {
14
9
  port: loader.number('SERVER_PORT'),
15
10
  enableProductionHttpErrorResponses: loader.bool(
@@ -0,0 +1,13 @@
1
+ import { healthCheckService } from './domain/health-check.service.js'
2
+
3
+ export interface Container {
4
+ healthCheckService: typeof healthCheckService
5
+ }
6
+
7
+ export type ContainerFactory = () => Promise<Container>
8
+
9
+ export const createContainer: ContainerFactory = async () => {
10
+ return {
11
+ healthCheckService,
12
+ }
13
+ }
@@ -1,11 +1,11 @@
1
1
  import { config } from './config.js'
2
- import { createContainer } from './container.js'
3
2
  import { createServer } from './view/server.js'
3
+ import { pinoLoggerFactory } from './adapters/pino.logger.js'
4
+ import { createContainer } from './container.js'
4
5
 
5
- const appContainer = createContainer()
6
- const { logger } = appContainer
6
+ const logger = pinoLoggerFactory.create(config.logger)
7
7
 
8
- const server = createServer(appContainer)
8
+ const server = await createServer(createContainer)
9
9
 
10
10
  server.listen(config.server.port, () => {
11
11
  logger.info(
@@ -5,12 +5,10 @@ import { createContainer } from '../container.js'
5
5
  import { requestTyped } from './util/openapi-test.util.js'
6
6
 
7
7
  describe('Health Check API', () => {
8
- let server: ReturnType<typeof createServer>
9
- let container: ReturnType<typeof createContainer>
8
+ let server: Awaited<ReturnType<typeof createServer>>
10
9
 
11
- before(() => {
12
- container = createContainer()
13
- server = createServer(container)
10
+ before(async () => {
11
+ server = await createServer(createContainer)
14
12
  })
15
13
 
16
14
  describe('GET /api/v1/healthz', () => {
@@ -21,7 +21,7 @@ export const requestTyped = <
21
21
  Resource extends KeysOfUnion<openapi.paths>,
22
22
  Method extends Exclude<KeysOfUnion<openapi.paths[Resource]>, 'parameters'>,
23
23
  Resp extends OpenApiRouteResponseBody<Openapi[Resource], Method>,
24
- Openapi extends openapi.paths = openapi.paths
24
+ Openapi extends openapi.paths = openapi.paths,
25
25
  >(
26
26
  server: Express,
27
27
  resource: Resource,
@@ -57,10 +57,10 @@ export const requestTyped = <
57
57
  ) {
58
58
  if (headers) {
59
59
  Object.entries(headers).forEach(([key, value]) => {
60
- void req.set(key, value)
60
+ req.set(key, value)
61
61
  })
62
62
  }
63
- void originalSend(body as any)
63
+ originalSend(body as any)
64
64
  return enhanced
65
65
  },
66
66
  responseTyped: () => {
@@ -42,7 +42,7 @@ export function createErrorHandler(
42
42
  return (error, _req, res, _next) => {
43
43
  const statusCode =
44
44
  error instanceof DomainError
45
- ? ERROR_DOMAIN_CODE_TO_HTTP_STATUS[error.code] ?? 500
45
+ ? (ERROR_DOMAIN_CODE_TO_HTTP_STATUS[error.code] ?? 500)
46
46
  : 500
47
47
 
48
48
  ;(res as any).error = errorToDevObject(error)
@@ -1,6 +1,6 @@
1
1
  import express from 'express'
2
2
  import { openApiRouter, registerOpenApiRoutes } from './util/openapi.util.js'
3
- import { healthCheckController } from './controller/health-check.controller.js'
3
+ import { healthCheckController } from './controllers/health-check.controller.js'
4
4
 
5
5
  const apiRouter = openApiRouter(express.Router(), {
6
6
  removePrefix: '/api/v1',
@@ -6,7 +6,7 @@ import { RequestContext } from '../../../context.js'
6
6
  export type OpenApiRouteResponseBodyMethod<
7
7
  T,
8
8
  TMethod extends string = 'get',
9
- TDefault = unknown
9
+ TDefault = unknown,
10
10
  > = T extends {
11
11
  [key in TMethod]: {
12
12
  responses: { 200: { content: { 'application/json': infer U } } }
@@ -17,7 +17,7 @@ export type OpenApiRouteResponseBodyMethod<
17
17
 
18
18
  export type OpenApiRouteResponseBody<
19
19
  T,
20
- TMethod extends string = 'get'
20
+ TMethod extends string = 'get',
21
21
  > = OpenApiRouteResponseBodyMethod<
22
22
  T,
23
23
  TMethod,
@@ -41,18 +41,18 @@ export type OpenApiRoutePathParam<T> = T extends {
41
41
  }
42
42
  ? U
43
43
  : T extends { post: { parameters: { path: infer U } } }
44
- ? U
45
- : T extends { put: { parameters: { path: infer U } } }
46
- ? U
47
- : unknown
44
+ ? U
45
+ : T extends { put: { parameters: { path: infer U } } }
46
+ ? U
47
+ : unknown
48
48
 
49
49
  export type OpenApiRouteQueryParam<T> = T extends {
50
50
  get: { parameters: { query: infer U } }
51
51
  }
52
52
  ? U
53
53
  : T extends { post: { parameters: { query: infer U } } }
54
- ? U
55
- : unknown
54
+ ? U
55
+ : unknown
56
56
 
57
57
  export type OpenApiRouteHeaderParam<T> = T extends {
58
58
  get: {
@@ -63,14 +63,14 @@ export type OpenApiRouteHeaderParam<T> = T extends {
63
63
  }
64
64
  ? LowercaseKeys<U>
65
65
  : T extends {
66
- post: {
67
- parameters: {
68
- header: infer U extends Record<string | number | symbol, any>
66
+ post: {
67
+ parameters: {
68
+ header: infer U extends Record<string | number | symbol, any>
69
+ }
69
70
  }
70
71
  }
71
- }
72
- ? LowercaseKeys<U>
73
- : unknown
72
+ ? LowercaseKeys<U>
73
+ : unknown
74
74
 
75
75
  export type OpenApiRouteParam<T> = OpenApiRoutePathParam<T> &
76
76
  OpenApiRouteQueryParam<T> &
@@ -79,7 +79,7 @@ export type OpenApiRouteParam<T> = OpenApiRoutePathParam<T> &
79
79
  export type OpenApiRouteRequestBodyMethod<
80
80
  T,
81
81
  TMethod extends string = 'post',
82
- TDefault = unknown
82
+ TDefault = unknown,
83
83
  > = T extends {
84
84
  [key in TMethod]: {
85
85
  requestBody: {
@@ -94,7 +94,7 @@ export type OpenApiRouteRequestBodyMethod<
94
94
 
95
95
  export type OpenApiRouteRequestBody<
96
96
  T,
97
- TMethod extends string = 'post'
97
+ TMethod extends string = 'post',
98
98
  > = OpenApiRouteRequestBodyMethod<
99
99
  T,
100
100
  TMethod,
@@ -150,7 +150,7 @@ type MimeContent<MimeType extends string> = {
150
150
 
151
151
  type MimeContentValue<
152
152
  MimeType extends ApiMimeTypes,
153
- T extends MimeContent<MimeType>
153
+ T extends MimeContent<MimeType>,
154
154
  > = {
155
155
  [K in keyof T['content']]: T['content'][K]
156
156
  }[keyof T['content']]
@@ -194,7 +194,7 @@ type RouteHandlers<SubsetOperationIds extends OperationIds> = {
194
194
  }
195
195
 
196
196
  export type RestApiController<
197
- SubsetOperationIds extends OperationIds = OperationIds
197
+ SubsetOperationIds extends OperationIds = OperationIds,
198
198
  > = {
199
199
  [Key in SubsetOperationIds]: OpenApiHandler<Key>
200
200
  }
@@ -225,7 +225,7 @@ export const openApiRouter = (
225
225
  route: <
226
226
  Method extends keyof paths[Path],
227
227
  Path extends keyof paths,
228
- OperationId extends OperationIds
228
+ OperationId extends OperationIds,
229
229
  >(
230
230
  method: Method,
231
231
  path: Path,
@@ -1,18 +1,20 @@
1
1
  import express from 'express'
2
2
  import { createRequestLogger } from './rest/middleware/request-logger.js'
3
- import { Container } from '../container.js'
4
3
  import { config } from '../config.js'
5
4
  import { createErrorHandler } from './rest/middleware/error-handler.js'
6
5
  import { routes } from './rest/routes.js'
7
6
  import { createContextMiddleware } from './rest/middleware/context-middleware.js'
7
+ import { ContainerFactory } from '../container.js'
8
8
 
9
- export const createServer = (appContainer: Container) => {
10
- const { logger } = appContainer
9
+ export const createServer = async (containerFactory: ContainerFactory) => {
10
+ const container = await containerFactory()
11
+ const { logger } = container
11
12
 
12
13
  const server = express()
14
+
13
15
  const errorHandler = createErrorHandler(config.server)
14
16
  const requestLogger = createRequestLogger(logger)
15
- const contextMiddleware = createContextMiddleware(appContainer)
17
+ const contextMiddleware = createContextMiddleware(container)
16
18
 
17
19
  server.disable('x-powered-by')
18
20
 
@@ -0,0 +1,5 @@
1
+ {
2
+ // DATABASE
3
+ /// Application PostgreSQL database connection props
4
+ "DB_CONNECTION_STRING": "postgres://{{PROJECT_NAME}}_docker:{{PROJECT_NAME}}_docker@localhost:5432/postgres"
5
+ }