@ackee/create-node-app 1.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 (104) hide show
  1. package/.eslintrc.cjs +10 -0
  2. package/AUTHORS +3 -0
  3. package/LICENSE.txt +22 -0
  4. package/README.md +39 -0
  5. package/bin/create-node-app.js +6 -0
  6. package/lib/Bootstrap.js +79 -0
  7. package/lib/Bootstrap.js.map +1 -0
  8. package/lib/Logger.js +12 -0
  9. package/lib/Logger.js.map +1 -0
  10. package/lib/Npm.js +33 -0
  11. package/lib/Npm.js.map +1 -0
  12. package/lib/PackageJson.js +39 -0
  13. package/lib/PackageJson.js.map +1 -0
  14. package/lib/Starter.js +2 -0
  15. package/lib/Starter.js.map +1 -0
  16. package/lib/Toolbelt.js +102 -0
  17. package/lib/Toolbelt.js.map +1 -0
  18. package/lib/cloudrun/CloudRunStarter.js +126 -0
  19. package/lib/cloudrun/CloudRunStarter.js.map +1 -0
  20. package/lib/cloudrun-graphql/GraphQLStarter.js +118 -0
  21. package/lib/cloudrun-graphql/GraphQLStarter.js.map +1 -0
  22. package/lib/types.js +2 -0
  23. package/lib/types.js.map +1 -0
  24. package/logo.png +0 -0
  25. package/package.json +42 -0
  26. package/prettier.config.cjs +1 -0
  27. package/src/Bootstrap.ts +99 -0
  28. package/src/Logger.ts +11 -0
  29. package/src/Npm.ts +38 -0
  30. package/src/PackageJson.ts +47 -0
  31. package/src/Starter.ts +7 -0
  32. package/src/Toolbelt.ts +132 -0
  33. package/src/cloudrun/CloudRunStarter.ts +181 -0
  34. package/src/cloudrun-graphql/GraphQLStarter.ts +182 -0
  35. package/src/types.ts +1 -0
  36. package/starter/cloudrun/.env.jsonc +10 -0
  37. package/starter/cloudrun/.eslint.tsconfig.json +4 -0
  38. package/starter/cloudrun/.eslintrc.cjs +8 -0
  39. package/starter/cloudrun/README.md +69 -0
  40. package/starter/cloudrun/src/adapters/pino.logger.ts +44 -0
  41. package/starter/cloudrun/src/config.ts +22 -0
  42. package/starter/cloudrun/src/container.ts +18 -0
  43. package/starter/cloudrun/src/context.ts +39 -0
  44. package/starter/cloudrun/src/domain/errors/codes.ts +9 -0
  45. package/starter/cloudrun/src/domain/errors/errors.ts +25 -0
  46. package/starter/cloudrun/src/domain/health-check.service.ts +15 -0
  47. package/starter/cloudrun/src/domain/ports/logger.d.ts +21 -0
  48. package/starter/cloudrun/src/index.ts +17 -0
  49. package/starter/cloudrun/src/test/health-check.test.ts +25 -0
  50. package/starter/cloudrun/src/test/util/openapi-test.util.ts +71 -0
  51. package/starter/cloudrun/src/view/cli/README.md +17 -0
  52. package/starter/cloudrun/src/view/cli/cli.ts +94 -0
  53. package/starter/cloudrun/src/view/cli/openapi/generate.ts +64 -0
  54. package/starter/cloudrun/src/view/rest/controller/health-check.controller.ts +33 -0
  55. package/starter/cloudrun/src/view/rest/middleware/context-middleware.ts +28 -0
  56. package/starter/cloudrun/src/view/rest/middleware/error-handler.ts +60 -0
  57. package/starter/cloudrun/src/view/rest/middleware/request-logger.ts +37 -0
  58. package/starter/cloudrun/src/view/rest/request.d.ts +9 -0
  59. package/starter/cloudrun/src/view/rest/routes.ts +15 -0
  60. package/starter/cloudrun/src/view/rest/spec/openapi.yml +65 -0
  61. package/starter/cloudrun/src/view/rest/util/openapi.util.ts +310 -0
  62. package/starter/cloudrun/src/view/server.ts +25 -0
  63. package/starter/cloudrun-graphql/.env.jsonc +12 -0
  64. package/starter/cloudrun-graphql/.eslint.tsconfig.json +4 -0
  65. package/starter/cloudrun-graphql/.eslintrc.cjs +43 -0
  66. package/starter/cloudrun-graphql/README.md +53 -0
  67. package/starter/cloudrun-graphql/codegen.yml +11 -0
  68. package/starter/cloudrun-graphql/src/adapters/pino.logger.ts +44 -0
  69. package/starter/cloudrun-graphql/src/config.ts +21 -0
  70. package/starter/cloudrun-graphql/src/container.ts +15 -0
  71. package/starter/cloudrun-graphql/src/context.ts +39 -0
  72. package/starter/cloudrun-graphql/src/domain/errors/codes.ts +9 -0
  73. package/starter/cloudrun-graphql/src/domain/errors/errors.ts +25 -0
  74. package/starter/cloudrun-graphql/src/domain/ports/logger.d.ts +21 -0
  75. package/starter/cloudrun-graphql/src/index.ts +11 -0
  76. package/starter/cloudrun-graphql/src/test/helloWorld.test.ts +23 -0
  77. package/starter/cloudrun-graphql/src/view/controller.ts +42 -0
  78. package/starter/cloudrun-graphql/src/view/graphql/resolvers/greeting.resolver.ts +5 -0
  79. package/starter/cloudrun-graphql/src/view/graphql/resolvers.ts +6 -0
  80. package/starter/cloudrun-graphql/src/view/graphql/schema/schema.graphql +6 -0
  81. package/starter/cloudrun-graphql/src/view/graphql/schema.ts +7 -0
  82. package/starter/cloudrun-graphql/src/view/server.ts +45 -0
  83. package/starter/shared/.dockerignore +19 -0
  84. package/starter/shared/.gitignore_ +5 -0
  85. package/starter/shared/.gitlab-ci.yml +199 -0
  86. package/starter/shared/.mocha-junit-config.json +6 -0
  87. package/starter/shared/.mocharc.json +8 -0
  88. package/starter/shared/.nvmrc +1 -0
  89. package/starter/shared/Dockerfile +40 -0
  90. package/starter/shared/ci-branch-config/common.env +7 -0
  91. package/starter/shared/ci-branch-config/development.env +7 -0
  92. package/starter/shared/ci-branch-config/master.env +7 -0
  93. package/starter/shared/ci-branch-config/stage.env +7 -0
  94. package/starter/shared/docker-compose/docker-compose-entrypoint.sh +7 -0
  95. package/starter/shared/docker-compose/docker-compose.ci.yml +19 -0
  96. package/starter/shared/docker-compose/docker-compose.local.yml +5 -0
  97. package/starter/shared/docker-compose/docker-compose.override.yml +5 -0
  98. package/starter/shared/docker-compose/docker-compose.yml +8 -0
  99. package/starter/shared/jest.config.js +12 -0
  100. package/starter/shared/prettier.config.cjs +1 -0
  101. package/starter/shared/src/test/setup.ts +1 -0
  102. package/starter/shared/tsconfig.json +22 -0
  103. package/tsconfig.json +19 -0
  104. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,310 @@
1
+ import express from 'express'
2
+ import bodyParser from 'body-parser'
3
+ import { operations, paths, operationPaths } from '../spec/openapi.js'
4
+ import { RequestContext } from '../../../context.js'
5
+
6
+ export type OpenApiRouteResponseBodyMethod<
7
+ T,
8
+ TMethod extends string = 'get',
9
+ TDefault = unknown
10
+ > = T extends {
11
+ [key in TMethod]: {
12
+ responses: { 200: { content: { 'application/json': infer U } } }
13
+ }
14
+ }
15
+ ? U
16
+ : TDefault
17
+
18
+ export type OpenApiRouteResponseBody<
19
+ T,
20
+ TMethod extends string = 'get'
21
+ > = OpenApiRouteResponseBodyMethod<
22
+ T,
23
+ TMethod,
24
+ OpenApiRouteResponseBodyMethod<
25
+ T,
26
+ 'post',
27
+ OpenApiRouteResponseBodyMethod<
28
+ T,
29
+ 'put',
30
+ OpenApiRouteResponseBodyMethod<T, 'delete'>
31
+ >
32
+ >
33
+ >
34
+
35
+ type LowercaseKeys<T extends Record<keyof any, any>> = {
36
+ [key in keyof T as key extends string ? Lowercase<key> : key]: T[key]
37
+ }
38
+
39
+ export type OpenApiRoutePathParam<T> = T extends {
40
+ get: { parameters: { path: infer U } }
41
+ }
42
+ ? U
43
+ : T extends { post: { parameters: { path: infer U } } }
44
+ ? U
45
+ : T extends { put: { parameters: { path: infer U } } }
46
+ ? U
47
+ : unknown
48
+
49
+ export type OpenApiRouteQueryParam<T> = T extends {
50
+ get: { parameters: { query: infer U } }
51
+ }
52
+ ? U
53
+ : T extends { post: { parameters: { query: infer U } } }
54
+ ? U
55
+ : unknown
56
+
57
+ export type OpenApiRouteHeaderParam<T> = T extends {
58
+ get: {
59
+ parameters: {
60
+ header: infer U extends Record<string | number | symbol, any>
61
+ }
62
+ }
63
+ }
64
+ ? LowercaseKeys<U>
65
+ : T extends {
66
+ post: {
67
+ parameters: {
68
+ header: infer U extends Record<string | number | symbol, any>
69
+ }
70
+ }
71
+ }
72
+ ? LowercaseKeys<U>
73
+ : unknown
74
+
75
+ export type OpenApiRouteParam<T> = OpenApiRoutePathParam<T> &
76
+ OpenApiRouteQueryParam<T> &
77
+ OpenApiRouteHeaderParam<T>
78
+
79
+ export type OpenApiRouteRequestBodyMethod<
80
+ T,
81
+ TMethod extends string = 'post',
82
+ TDefault = unknown
83
+ > = T extends {
84
+ [key in TMethod]: {
85
+ requestBody: {
86
+ content:
87
+ | { 'application/json': infer U }
88
+ | { 'multipart/form-data': infer U }
89
+ }
90
+ }
91
+ }
92
+ ? U
93
+ : TDefault
94
+
95
+ export type OpenApiRouteRequestBody<
96
+ T,
97
+ TMethod extends string = 'post'
98
+ > = OpenApiRouteRequestBodyMethod<
99
+ T,
100
+ TMethod,
101
+ OpenApiRouteRequestBodyMethod<
102
+ T,
103
+ 'put',
104
+ OpenApiRouteRequestBodyMethod<
105
+ T,
106
+ 'delete',
107
+ OpenApiRouteRequestBodyMethod<T, 'patch'>
108
+ >
109
+ >
110
+ >
111
+
112
+ /**
113
+ * pipeMiddleware takes multiple middlewares and creates and merges them into
114
+ * one using express Router.
115
+ */
116
+ export const pipeMiddleware = (...middlewares: express.RequestHandler[]) => {
117
+ const router = express.Router({ mergeParams: true })
118
+ middlewares.forEach(m => router.use(m))
119
+ return router
120
+ }
121
+
122
+ export type ApiHandler<TRes> = (
123
+ ctx: Readonly<RequestContext>,
124
+ req: express.Request,
125
+ res: express.Response
126
+ ) => TRes | Promise<TRes>
127
+
128
+ const asyncHandler =
129
+ <TRes>(fn: ApiHandler<TRes>): express.Handler =>
130
+ async (req, res, next) => {
131
+ try {
132
+ const result = await fn(req.context, req, res)
133
+ res.json(result)
134
+ } catch (error: unknown) {
135
+ next(error)
136
+ }
137
+ }
138
+
139
+ export type OperationIds = keyof operations
140
+
141
+ type ApiMimeTypes = string
142
+
143
+ type Content = {
144
+ content: any
145
+ }
146
+
147
+ type MimeContent<MimeType extends string> = {
148
+ content: { [key in MimeType]?: any }
149
+ }
150
+
151
+ type MimeContentValue<
152
+ MimeType extends ApiMimeTypes,
153
+ T extends MimeContent<MimeType>
154
+ > = {
155
+ [K in keyof T['content']]: T['content'][K]
156
+ }[keyof T['content']]
157
+
158
+ type OpenApiContentTypes<OpenApiContent extends Record<number, any>> = {
159
+ [K in keyof OpenApiContent]: OpenApiContent[K] extends Content
160
+ ? MimeContentValue<ApiMimeTypes, OpenApiContent[K]>
161
+ : never
162
+ }[keyof OpenApiContent]
163
+
164
+ export type OperationParams<OperationId extends OperationIds> =
165
+ operations[OperationId]['parameters']['path']
166
+
167
+ export type OperationQuery<OperationId extends OperationIds> =
168
+ operations[OperationId]['parameters']['query']
169
+
170
+ export type OperationResponse<OperationId extends OperationIds> =
171
+ OpenApiContentTypes<operations[OperationId]['responses']>
172
+
173
+ export type OperationBody<OperationId extends OperationIds> =
174
+ operations[OperationId] extends never
175
+ ? never
176
+ : MimeContentValue<ApiMimeTypes, operations[OperationId]['requestBody']>
177
+
178
+ export type OpenApiHandler<OperationId extends OperationIds> =
179
+ express.RequestHandler<
180
+ OperationParams<OperationId>,
181
+ OperationResponse<OperationId>,
182
+ OperationBody<OperationId>,
183
+ OperationQuery<OperationId>
184
+ >
185
+
186
+ export type OperationHandler<OperationId extends keyof operations> = (
187
+ ctx: RequestContext,
188
+ req: Parameters<OpenApiHandler<OperationId>>[0],
189
+ res: Parameters<OpenApiHandler<OperationId>>[1]
190
+ ) => Promise<OperationResponse<OperationId>>
191
+
192
+ type RouteHandlers<SubsetOperationIds extends OperationIds> = {
193
+ [Key in SubsetOperationIds]: OperationHandler<Key>
194
+ }
195
+
196
+ export type RestApiController<
197
+ SubsetOperationIds extends OperationIds = OperationIds
198
+ > = {
199
+ [Key in SubsetOperationIds]: OpenApiHandler<Key>
200
+ }
201
+
202
+ const handleOperationAsync = <OperationId extends OperationIds>(
203
+ fn: OperationHandler<OperationId>
204
+ ): OpenApiHandler<OperationId> => asyncHandler(fn as any) as any
205
+
206
+ const createRestController = <SubsetOperationIds extends OperationIds>(
207
+ def: RouteHandlers<SubsetOperationIds>
208
+ ): RestApiController<SubsetOperationIds> => {
209
+ return Object.entries(def).reduce((ctrl, [operationId, handler]) => {
210
+ if (!handler) {
211
+ return ctrl
212
+ }
213
+ ctrl[operationId as SubsetOperationIds] = handleOperationAsync(
214
+ handler as any
215
+ ) as any
216
+ return ctrl
217
+ }, {} as RestApiController<SubsetOperationIds>)
218
+ }
219
+
220
+ export const openApiRouter = (
221
+ router: express.Router,
222
+ { removePrefix }: { removePrefix: string } = { removePrefix: '' }
223
+ ) => ({
224
+ express: router,
225
+ route: <
226
+ Method extends keyof paths[Path],
227
+ Path extends keyof paths,
228
+ OperationId extends OperationIds
229
+ >(
230
+ method: Method,
231
+ path: Path,
232
+ handler: OpenApiHandler<OperationId>
233
+ ) => {
234
+ const route = path
235
+ .toString()
236
+ .replaceAll('}', '')
237
+ .replaceAll('{', ':')
238
+ // eslint-disable-next-line security/detect-non-literal-regexp
239
+ .replace(new RegExp(`^${removePrefix}`), '')
240
+
241
+ switch (method) {
242
+ case 'get':
243
+ router.get(route, handler)
244
+ break
245
+ case 'post':
246
+ router.post(route, handler)
247
+ break
248
+ case 'patch':
249
+ router.patch(route, handler)
250
+ break
251
+ case 'delete':
252
+ router.delete(route, handler)
253
+ break
254
+ case 'head':
255
+ router.head(route, handler)
256
+ break
257
+ case 'trace':
258
+ router.trace(route, handler)
259
+ break
260
+ case 'options':
261
+ router.options(route, handler)
262
+ break
263
+ default:
264
+ throw new Error(
265
+ `The OpenApi router received invalid HTTP method to be registered: ${method.toString()}`
266
+ )
267
+ }
268
+ },
269
+ })
270
+
271
+ export const registerOpenApiRoutes = <SomeOperationIds extends OperationIds>(
272
+ router: ReturnType<typeof openApiRouter>,
273
+ controller: Partial<RestApiController<SomeOperationIds>>
274
+ ) => {
275
+ const operations = Object.keys(controller) as SomeOperationIds[]
276
+ operations.forEach(operation => {
277
+ if (!controller[operation]) {
278
+ return
279
+ }
280
+
281
+ router.route(
282
+ operationPaths[operation].method,
283
+ operationPaths[operation].path,
284
+ controller[operation]
285
+ )
286
+ })
287
+ }
288
+
289
+ /**
290
+ * ctrl is a scoped object for controller functions
291
+ */
292
+ export const ctrl = {
293
+ json: pipeMiddleware(
294
+ bodyParser.json(),
295
+ // Monkeypatch res.json to assign the body to res.out first in order
296
+ // to log it by pino
297
+ (_req, res: any, next) => {
298
+ const resJson = res.json.bind(res)
299
+ res.json = (body?: any) => {
300
+ res.out = body
301
+ return resJson(body)
302
+ }
303
+ next()
304
+ }
305
+ ),
306
+ asyncHandler,
307
+ createRestController,
308
+ openApiRouter,
309
+ registerOpenApiRoutes,
310
+ }
@@ -0,0 +1,25 @@
1
+ import express from 'express'
2
+ import { createRequestLogger } from './rest/middleware/request-logger.js'
3
+ import { Container } from '../container.js'
4
+ import { config } from '../config.js'
5
+ import { createErrorHandler } from './rest/middleware/error-handler.js'
6
+ import { routes } from './rest/routes.js'
7
+ import { createContextMiddleware } from './rest/middleware/context-middleware.js'
8
+
9
+ export const createServer = (appContainer: Container) => {
10
+ const { logger } = appContainer
11
+
12
+ const server = express()
13
+ const errorHandler = createErrorHandler(config.server)
14
+ const requestLogger = createRequestLogger(logger)
15
+ const contextMiddleware = createContextMiddleware(appContainer)
16
+
17
+ server.disable('x-powered-by')
18
+
19
+ server.use(requestLogger)
20
+ server.use(contextMiddleware)
21
+ server.use('/api/v1/', routes.api)
22
+ server.use(errorHandler)
23
+
24
+ return server
25
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ // Server will run on this port
3
+ "SERVER_PORT": 3000,
4
+ // Logging level, see https://github.com/pinojs/pino/blob/master/docs/api.md#logger-level
5
+ "LOGGER_DEFAULT_LEVEL": "debug",
6
+ // Enable/disable logging object multiline formatted logging https://github.com/pinojs/pino/blob/master/docs/api.md#prettyprint-boolean--object
7
+ "LOGGER_PRETTY": false,
8
+ // Response development errors for debugging
9
+ "SERVER_ALLOW_RESPONSE_ERRORS": false,
10
+ // Enable GraphQL introspection
11
+ "SERVER_ENABLE_INTROSPECTION": true
12
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["src/generated/**/*"]
4
+ }
@@ -0,0 +1,43 @@
1
+ const { omit } = require('lodash')
2
+ const defaultConfig = {
3
+ ...require('@ackee/styleguide-backend-config/eslint'),
4
+ }
5
+
6
+ module.exports = {
7
+ root: true,
8
+ ignorePatterns: ['dist', 'docs', 'src/generated'],
9
+ overrides: [
10
+ {
11
+ ...omit(defaultConfig, ['ignorePatterns']),
12
+ files: ['*.ts', '*.js'],
13
+ parserOptions: {
14
+ project: '.eslint.tsconfig.json',
15
+ },
16
+ rules: {
17
+ ...defaultConfig['rules'],
18
+ '@typescript-eslint/strict-boolean-expressions': 0,
19
+ '@typescript-eslint/no-misused-promises': 'warn',
20
+ },
21
+ },
22
+ {
23
+ files: ['**/*.graphql'],
24
+ extends: 'plugin:@graphql-eslint/schema-recommended',
25
+ parserOptions: {
26
+ graphQLConfig: {
27
+ schema: './src/view/graphql/schema/*.graphql',
28
+ },
29
+ },
30
+ rules: {
31
+ '@graphql-eslint/strict-id-in-types': 0,
32
+ '@graphql-eslint/require-description': [
33
+ 'warn',
34
+ {
35
+ types: true,
36
+ DirectiveDefinition: true,
37
+ },
38
+ ],
39
+ '@graphql-eslint/no-unreachable-types': 'warn',
40
+ },
41
+ },
42
+ ],
43
+ }
@@ -0,0 +1,53 @@
1
+ # GraphQL Cloudrun Starter
2
+
3
+ Node.js project scaffolded with Cloudrun and GraphQL
4
+
5
+ ## 🎉 Initialize project
6
+
7
+ Run `create-node-app cloudrun-graphql` to init your project. By default project is created in `../node-app` folder.
8
+
9
+ You can pass `destination` argument into the command as well.
10
+ Example:
11
+
12
+ - `create-node-app cloudrun-graphql /Users/foo/Documents/bar `
13
+
14
+ ## 👷 Continuous Integration
15
+
16
+ ### Environment variables
17
+
18
+ Make sure you replace all the variable values containing `REPLACEME` in `ci-branch-config` files.
19
+
20
+ The following variables must be set for each branch in `ci-branch-config` directory.
21
+
22
+ - `GCP_PROJECT_ID` - GCP project identifier
23
+ - `ENVIRONMENT` - e.g. `development`
24
+
25
+ Optional variables:
26
+
27
+ - `ALLOCATED_MEMORY` - Memory allocated at Cloudrun, default is 384Mi
28
+ - `CLOUD_RUN_SERVICE_ACCOUNT` - CloudRun service account
29
+ - `ENV_SECRETS` - Secret variables of the deployment, format is: `KEY=[NAME OF SECRET IN SECRET MANAGER:VERSION],...`
30
+ - `ENV_VARS` - Environment variables of the deployment, format is: `KEY=VALUE,...`
31
+ - `GCP_SA_KEY` - We use different service accounts for different environments. So we have to overwrite `GCP_SA_KEY` variable e.g.`GCP_SA_KEY=$SECRET_GCP_SA_KEY_DEVELOPMENT` Variable `SECRET_GCP_SA_KEY_<environment>` should be set in `GitLab CI secret variables`
32
+ - `GCP_SECRET_NAME` - Name of secret in Google Secret Manager
33
+ - `MAX_INSTANCES` - Maximum instance count in Cloudrun, default is 8
34
+ - `MIN_INSTANCES` - Minimum instance count in Cloudrun, default is 0
35
+ - `SECRET_PATH` - CloudRun volume where secrets will be injected e.g. `/config/secrets.json` can't be the same path ass app work dir e.g. `/usr/src/app` deploy will fail cause secret protection. But have to be identical with ENV `CFG_JSON_PATH` in Dockerfile
36
+ - `SKIP_AUDIT` - Skip NPM Audit, defaults to `false`
37
+ - `SKIP_LINT` - Skip lint, defaults to `false`
38
+ - `SKIP_TESTS` - Skip tests, defaults to `false`
39
+ - `SQL_INSTANCE_NAME` - SQL instance name
40
+ - `VPC_CONNECTOR_NAME` - serverless connector name, has to be in the same region
41
+ - `DOCKER_REGISTRY_TYPE` - whenever to push Docker image into Container Registry or Artifacts Registry, default is
42
+ `container`
43
+ - `DOCKER_REGISTRY_URL` - hostname of Docker Registry, defaults to `eu.gcr.io`
44
+ - `ANY_ADDITIONAL_CLOUDRUN_ARGS` - any argument required by Cloud Run, eg `--concurrency=1000 --clear-labels ...`
45
+
46
+ Common variables:
47
+
48
+ - `GCP_TEST_SECRET_PROJECT_ID` - GCP project identifier for test secrets
49
+ - `GCP_SECRET_TEST_NAME` - Name of test secret in Google Secret Manager
50
+
51
+ ## 📄 Additional Resources
52
+
53
+ - [Google Cloudrun docs](https://cloud.google.com/run/docs)
@@ -0,0 +1,11 @@
1
+ schema: './src/view/graphql/schema/schema.graphql'
2
+ overwrite: true
3
+ generates:
4
+ src/generated/graphql.ts:
5
+ config:
6
+ useIndexSignature: true
7
+ scalars:
8
+ mappers:
9
+ plugins:
10
+ - 'typescript'
11
+ - 'typescript-resolvers'
@@ -0,0 +1,44 @@
1
+ import { pino as pinoLogger } from 'pino'
2
+ import { LoggerFactoryPort } from '../domain/ports/logger.d.js'
3
+
4
+ // https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
5
+ const PinoLevelToSeverityLookup: Record<string, string> = {
6
+ trace: 'DEBUG',
7
+ debug: 'DEBUG',
8
+ info: 'INFO',
9
+ warn: 'WARNING',
10
+ error: 'ERROR',
11
+ fatal: 'CRITICAL',
12
+ } as const
13
+
14
+ const defaultPinoConf = (defaultLevel: string) => ({
15
+ messageKey: 'message',
16
+ formatters: {
17
+ messageKey: 'message',
18
+ level: (label: string, num: number) => {
19
+ return {
20
+ severity:
21
+ PinoLevelToSeverityLookup[label] ??
22
+ PinoLevelToSeverityLookup[defaultLevel],
23
+ level: num,
24
+ }
25
+ },
26
+ },
27
+ })
28
+
29
+ export const pinoLoggerFactory: LoggerFactoryPort = {
30
+ create: config => {
31
+ return pinoLogger({
32
+ ...defaultPinoConf(config.defaultLevel),
33
+ transport: config.enablePrettyPrint
34
+ ? {
35
+ target: 'pino-pretty',
36
+ options: {
37
+ colorize: true,
38
+ },
39
+ }
40
+ : undefined,
41
+ level: config.defaultLevel,
42
+ })
43
+ },
44
+ }
@@ -0,0 +1,21 @@
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
+ server: {
10
+ port: loader.number('SERVER_PORT'),
11
+ allowResponseErrors: loader.bool('SERVER_ALLOW_RESPONSE_ERRORS'),
12
+ enableIntrospection: loader.bool('SERVER_ENABLE_INTROSPECTION'),
13
+ },
14
+ logger: {
15
+ defaultLevel: loader.custom(x => x as any as Level)('LOGGER_DEFAULT_LEVEL'),
16
+ pretty: loader.bool('LOGGER_PRETTY'),
17
+ },
18
+ }
19
+
20
+ export const config = values(configSchema)
21
+ export const safeConfig = maskedValues(configSchema)
@@ -0,0 +1,15 @@
1
+ import { config } from './config.js'
2
+ import { pinoLoggerFactory } from './adapters/pino.logger.js'
3
+ import { LoggerPort } from './domain/ports/logger.d.js'
4
+
5
+ export interface Container {
6
+ logger: LoggerPort
7
+ }
8
+
9
+ export const createContainer = (): Container => {
10
+ const logger = pinoLoggerFactory.create(config.logger)
11
+
12
+ return {
13
+ logger,
14
+ }
15
+ }
@@ -0,0 +1,39 @@
1
+ import { Container } from './container.js'
2
+
3
+ interface BaseContext {
4
+ container: Container
5
+ note?: string
6
+ }
7
+
8
+ export interface ApiUserRequestContext extends BaseContext {
9
+ type: 'api-user'
10
+ user: null // Add UserContext if Auth is implemented
11
+ }
12
+
13
+ export interface ServerRequestContext extends BaseContext {
14
+ type: 'server'
15
+ }
16
+
17
+ /**
18
+ * Defines context in which current application runs.
19
+ * Context should be created with every external call (user request, cli input, ...)
20
+ */
21
+ export type RequestContext = ApiUserRequestContext | ServerRequestContext
22
+
23
+ /**
24
+ * Creator of the context for the App. Every API layer is responsible for
25
+ * creating the context based on the Api parameters (http headers, protocol settings etc...)
26
+ */
27
+ export type RequestContextFactory<Params extends any[]> = (
28
+ container: Readonly<Container>,
29
+ ...params: Params
30
+ ) => Promise<RequestContext>
31
+
32
+ /**
33
+ * Creator of the server context that should be used only in executions that are invoked
34
+ * on server, f.e. CLI, server start up etc.
35
+ */
36
+ export type ServerRequestContextFactory<Params extends any[]> = (
37
+ container: Readonly<Container>,
38
+ ...params: Params
39
+ ) => Promise<ServerRequestContext>
@@ -0,0 +1,9 @@
1
+ export enum ErrorCode {
2
+ UNKNOWN = 0,
3
+ VALIDATION = 1000,
4
+ }
5
+
6
+ export const errorMessages = {
7
+ [ErrorCode.UNKNOWN]: 'Unknown error',
8
+ [ErrorCode.VALIDATION]: 'Validation error',
9
+ }
@@ -0,0 +1,25 @@
1
+ import { ErrorCode, errorMessages } from './codes.js'
2
+
3
+ export class DomainError<AdditionalData = any> extends Error {
4
+ constructor(
5
+ public readonly code: ErrorCode,
6
+ public readonly message: string = errorMessages[code],
7
+ public readonly data?: AdditionalData
8
+ ) {
9
+ super(message)
10
+ }
11
+ }
12
+
13
+ export type ValidationErrorData =
14
+ | Array<{ field: string; message: string }>
15
+ | { field: string; message: string }
16
+
17
+ export class ValidationError extends DomainError<ValidationErrorData> {
18
+ constructor(errors: ValidationErrorData) {
19
+ super(
20
+ ErrorCode.VALIDATION,
21
+ errorMessages[ErrorCode.VALIDATION],
22
+ Array.isArray(errors) ? errors : [errors]
23
+ )
24
+ }
25
+ }
@@ -0,0 +1,21 @@
1
+ export interface LoggerConfig {
2
+ defaultLevel: string
3
+ enablePrettyPrint?: boolean
4
+ }
5
+
6
+ type BaseLoggerFn = (object: any, message?: string) => void
7
+
8
+ export interface LoggerPort {
9
+ level: string
10
+ debug: BaseLoggerFn
11
+ info: BaseLoggerFn
12
+ warn: BaseLoggerFn
13
+ error: BaseLoggerFn
14
+ fatal: BaseLoggerFn
15
+ trace: BaseLoggerFn
16
+ silent: BaseLoggerFn
17
+ }
18
+
19
+ export interface LoggerFactoryPort {
20
+ create: (config: LoggerConfig) => LoggerPort
21
+ }
@@ -0,0 +1,11 @@
1
+ import { safeConfig } from './config.js'
2
+ import { createAppServer, startServer } from './view/server.js'
3
+ import { createContainer } from './container.js'
4
+
5
+ const appContainer = createContainer()
6
+ const { logger } = appContainer
7
+
8
+ logger.info({ config: safeConfig }, 'Loaded config')
9
+
10
+ const appServer = createAppServer()
11
+ void startServer({ ...appServer, container: appContainer })
@@ -0,0 +1,23 @@
1
+ import assert from 'node:assert'
2
+ import { describe, it } from 'mocha'
3
+ import { createAppServer } from '../view/server.js'
4
+ import { gql } from 'graphql-tag'
5
+
6
+ describe('Hello world', () => {
7
+ it('should return greeting', async () => {
8
+ const query = gql`
9
+ query Hello {
10
+ greeting
11
+ }
12
+ `
13
+ const { server } = createAppServer()
14
+ const res = await server.executeOperation({ query })
15
+
16
+ assert(res.body.kind === 'single')
17
+ assert.deepStrictEqual(res.body.singleResult.errors, undefined)
18
+ assert.deepStrictEqual(
19
+ res.body.singleResult.data?.greeting,
20
+ 'Hello, world! 🎉'
21
+ )
22
+ })
23
+ })