@things-factory/shell 8.0.0-beta.9 → 8.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 (67) hide show
  1. package/client/themes/calendar-theme.css +1 -3
  2. package/client/themes/index.css +1 -2
  3. package/dist-server/server-dev.js +4 -2
  4. package/dist-server/server-dev.js.map +1 -1
  5. package/dist-server/server.js +4 -2
  6. package/dist-server/server.js.map +1 -1
  7. package/dist-server/tsconfig.tsbuildinfo +1 -1
  8. package/dist-server/typeorm/json5-transform.js +2 -2
  9. package/dist-server/typeorm/json5-transform.js.map +1 -1
  10. package/package.json +14 -14
  11. package/server/graphql-local-client.ts +59 -0
  12. package/server/index.ts +13 -0
  13. package/server/initializers/database.ts +96 -0
  14. package/server/initializers/naming-strategy.ts +14 -0
  15. package/server/middlewares/domain-middleware.ts +60 -0
  16. package/server/middlewares/index.ts +43 -0
  17. package/server/migrations/1000000000000-SeedDomain.ts +37 -0
  18. package/server/migrations/index.ts +9 -0
  19. package/server/pubsub-log-transport.ts +59 -0
  20. package/server/pubsub.ts +84 -0
  21. package/server/routers/domain-router.ts +13 -0
  22. package/server/routers/global-router.ts +76 -0
  23. package/server/routers/graphql-router.ts +3 -0
  24. package/server/routers/index.ts +3 -0
  25. package/server/schema.ts +163 -0
  26. package/server/server-dev.ts +305 -0
  27. package/server/server.ts +296 -0
  28. package/server/service/attribute-set/attribute-set-item-type.ts +65 -0
  29. package/server/service/attribute-set/attribute-set-mutation.ts +125 -0
  30. package/server/service/attribute-set/attribute-set-query.ts +36 -0
  31. package/server/service/attribute-set/attribute-set-type.ts +46 -0
  32. package/server/service/attribute-set/attribute-set.ts +35 -0
  33. package/server/service/attribute-set/index.ts +6 -0
  34. package/server/service/common-types/index.ts +6 -0
  35. package/server/service/common-types/list-param.ts +61 -0
  36. package/server/service/common-types/log.ts +17 -0
  37. package/server/service/common-types/object-ref.ts +13 -0
  38. package/server/service/common-types/scalar-any.ts +44 -0
  39. package/server/service/common-types/scalar-date.ts +22 -0
  40. package/server/service/common-types/scalar-object.ts +15 -0
  41. package/server/service/directive-transaction/index.ts +1 -0
  42. package/server/service/directive-transaction/transaction.ts +40 -0
  43. package/server/service/domain/domain-mutation.ts +120 -0
  44. package/server/service/domain/domain-query.ts +48 -0
  45. package/server/service/domain/domain-types.ts +63 -0
  46. package/server/service/domain/domain.ts +147 -0
  47. package/server/service/domain/index.ts +6 -0
  48. package/server/service/index.ts +32 -0
  49. package/server/service/subscription-data/data-resolver.ts +37 -0
  50. package/server/service/subscription-data/data-types.ts +16 -0
  51. package/server/service/subscription-data/index.ts +4 -0
  52. package/server/typeorm/encrypt-transform.ts +70 -0
  53. package/server/typeorm/get-data-encryption-key.ts +13 -0
  54. package/server/typeorm/json5-transform.ts +26 -0
  55. package/server/typeorm/round-transform.ts +20 -0
  56. package/server/utils/condition-builder.ts +145 -0
  57. package/server/utils/get-domain.ts +226 -0
  58. package/server/utils/get-query-builder-from-list-params.ts +469 -0
  59. package/server/utils/get-times-for-period.ts +60 -0
  60. package/server/utils/index.ts +8 -0
  61. package/server/utils/list-param-adjuster.ts +21 -0
  62. package/server/utils/list-params-converter.ts +200 -0
  63. package/server/utils/list-query-builder.ts +120 -0
  64. package/server/utils/publish-progress.ts +23 -0
  65. package/dist-server/process-cleaner.d.ts +0 -1
  66. package/dist-server/process-cleaner.js +0 -92
  67. package/dist-server/process-cleaner.js.map +0 -1
@@ -10,7 +10,7 @@ exports.json5Transformer = {
10
10
  * @returns {string} - The stringified JSON5 representation of the entityValue.
11
11
  */
12
12
  to(entityValue) {
13
- return entityValue === null || entityValue === undefined ? null : json5_1.default.stringify(entityValue);
13
+ return json5_1.default.stringify(entityValue);
14
14
  },
15
15
  /**
16
16
  * Converts a JSON5 string from the database back into its original type when retrieving it.
@@ -19,7 +19,7 @@ exports.json5Transformer = {
19
19
  */
20
20
  from(databaseValue) {
21
21
  try {
22
- return databaseValue === null ? null : json5_1.default.parse(databaseValue);
22
+ return json5_1.default.parse(databaseValue);
23
23
  }
24
24
  finally {
25
25
  return databaseValue;
@@ -1 +1 @@
1
- {"version":3,"file":"json5-transform.js","sourceRoot":"","sources":["../../server/typeorm/json5-transform.ts"],"names":[],"mappings":";;;;AACA,0DAAyB;AAEZ,QAAA,gBAAgB,GAAqB;IAChD;;;;OAIG;IACH,EAAE,CAAC,WAAgB;QACjB,OAAO,WAAW,KAAK,IAAI,IAAI,WAAW,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IAChG,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,aAAqB;QACxB,IAAI,CAAC;YACH,OAAO,aAAa,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,eAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;QACnE,CAAC;gBAAS,CAAC;YACT,OAAO,aAAa,CAAA;QACtB,CAAC;IACH,CAAC;CACF,CAAA","sourcesContent":["import { ValueTransformer } from 'typeorm'\nimport json5 from 'json5'\n\nexport const json5Transformer: ValueTransformer = {\n /**\n * Converts the entity's value to a JSON5 string before storing it in the database.\n * @param {any} entityValue - The unencrypted entity field value.\n * @returns {string} - The stringified JSON5 representation of the entityValue.\n */\n to(entityValue: any) {\n return entityValue === null || entityValue === undefined ? null : json5.stringify(entityValue)\n },\n\n /**\n * Converts a JSON5 string from the database back into its original type when retrieving it.\n * @param {string} databaseValue - The JSON5 string stored in the database.\n * @returns {any} - The original type of the entityValue, parsed from the JSON5 string.\n */\n from(databaseValue: string) {\n try {\n return databaseValue === null ? null : json5.parse(databaseValue)\n } finally {\n return databaseValue\n }\n }\n}\n"]}
1
+ {"version":3,"file":"json5-transform.js","sourceRoot":"","sources":["../../server/typeorm/json5-transform.ts"],"names":[],"mappings":";;;;AACA,0DAAyB;AAEZ,QAAA,gBAAgB,GAAqB;IAChD;;;;OAIG;IACH,EAAE,CAAC,WAAgB;QACjB,OAAO,eAAK,CAAC,SAAS,CAAC,WAAW,CAAC,CAAA;IACrC,CAAC;IAED;;;;OAIG;IACH,IAAI,CAAC,aAAqB;QACxB,IAAI,CAAC;YACH,OAAO,eAAK,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;QACnC,CAAC;gBAAS,CAAC;YACT,OAAO,aAAa,CAAA;QACtB,CAAC;IACH,CAAC;CACF,CAAA","sourcesContent":["import { ValueTransformer } from 'typeorm'\nimport json5 from 'json5'\n\nexport const json5Transformer: ValueTransformer = {\n /**\n * Converts the entity's value to a JSON5 string before storing it in the database.\n * @param {any} entityValue - The unencrypted entity field value.\n * @returns {string} - The stringified JSON5 representation of the entityValue.\n */\n to(entityValue: any) {\n return json5.stringify(entityValue)\n },\n\n /**\n * Converts a JSON5 string from the database back into its original type when retrieving it.\n * @param {string} databaseValue - The JSON5 string stored in the database.\n * @returns {any} - The original type of the entityValue, parsed from the JSON5 string.\n */\n from(databaseValue: string) {\n try {\n return json5.parse(databaseValue)\n } finally {\n return databaseValue\n }\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/shell",
3
- "version": "8.0.0-beta.9",
3
+ "version": "8.0.0",
4
4
  "description": "Core module for framework",
5
5
  "bin": {
6
6
  "things-factory": "bin/things-factory",
@@ -43,7 +43,7 @@
43
43
  "@graphql-tools/schema": "^8.5.0",
44
44
  "@graphql-tools/utils": "^10.1.2",
45
45
  "@graphql-yoga/redis-event-target": "^3.0.1",
46
- "@hatiolab/things-scene": "^8.0.0-beta",
46
+ "@hatiolab/things-scene": "^3.2.0",
47
47
  "@koa/cors": "^5.0.0",
48
48
  "@material-design-icons/font": "^0.14.9",
49
49
  "@material/mwc-button": "^0.27.0",
@@ -53,19 +53,19 @@
53
53
  "@material/mwc-textfield": "^0.27.0",
54
54
  "@material/web": "^2.0.0",
55
55
  "@open-wc/scoped-elements": "^2.1.3",
56
- "@operato/board": "^8.0.0-beta",
57
- "@operato/graphql": "^8.0.0-beta",
58
- "@operato/help": "^8.0.0-beta",
59
- "@operato/layout": "^8.0.0-beta",
60
- "@operato/shell": "^8.0.0-beta",
61
- "@operato/typeorm-history": "^8.0.0-beta",
62
- "@operato/utils": "^8.0.0-beta",
56
+ "@operato/board": "^8.0.0",
57
+ "@operato/graphql": "^8.0.0",
58
+ "@operato/help": "^8.0.0",
59
+ "@operato/layout": "^8.0.0",
60
+ "@operato/shell": "^8.0.0",
61
+ "@operato/typeorm-history": "^8.0.0",
62
+ "@operato/utils": "^8.0.0",
63
63
  "@reduxjs/toolkit": "^2.2.5",
64
- "@things-factory/ejs-remote": "^8.0.0-beta.4",
65
- "@things-factory/env": "^8.0.0-beta.4",
64
+ "@things-factory/ejs-remote": "^8.0.0",
65
+ "@things-factory/env": "^8.0.0",
66
66
  "@things-factory/operato-license-checker": "^4.0.4",
67
- "@things-factory/styles": "^8.0.0-beta.4",
68
- "@things-factory/utils": "^8.0.0-beta.4",
67
+ "@things-factory/styles": "^8.0.0",
68
+ "@things-factory/utils": "^8.0.0",
69
69
  "@webcomponents/scoped-custom-element-registry": "^0.0.9",
70
70
  "@webcomponents/webcomponentsjs": "^2.6.0",
71
71
  "args": "^5.0.0",
@@ -133,5 +133,5 @@
133
133
  "pg": "^8.7.3",
134
134
  "sqlite3": "^5.0.8"
135
135
  },
136
- "gitHead": "86b1dfa26292926a2d5447fdc23bfa5a9e983245"
136
+ "gitHead": "07ef27d272dd9a067a9648ac7013748510556a18"
137
137
  }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Apollo Client configuration for a local GraphQL schema.
3
+ * This class provides a static method `init` to initialize the client with a schema and an app context.
4
+ */
5
+ import { ApolloClient, InMemoryCache, NormalizedCacheObject } from '@apollo/client'
6
+ import { SchemaLink } from '@apollo/client/link/schema'
7
+
8
+ const defaultOptions: any = {
9
+ watchQuery: {
10
+ fetchPolicy: 'no-cache',
11
+ errorPolicy: 'ignore'
12
+ },
13
+ query: {
14
+ fetchPolicy: 'no-cache',
15
+ errorPolicy: 'all'
16
+ },
17
+ mutate: {
18
+ errorPolicy: 'all'
19
+ }
20
+ }
21
+
22
+ /**
23
+ * GraphqlLocalClient is a utility class for initializing an Apollo Client for a local GraphQL schema.
24
+ * It provides a static `init` method for setting up the client with a schema and an app context.
25
+ */
26
+ export class GraphqlLocalClient {
27
+ /**
28
+ * Apollo Client instance for local GraphQL schema.
29
+ * @static
30
+ * @type {ApolloClient<NormalizedCacheObject>}
31
+ */
32
+ static client: ApolloClient<NormalizedCacheObject>
33
+
34
+ /**
35
+ * Initializes the Apollo Client for a local GraphQL schema.
36
+ * @static
37
+ * @param {object} schema - The GraphQL schema to use.
38
+ * @param {object} app - The application context to be passed in as part of the execution context.
39
+ */
40
+ static init(schema, app) {
41
+ const cache = new InMemoryCache({
42
+ addTypename: false
43
+ })
44
+
45
+ GraphqlLocalClient.client = new ApolloClient({
46
+ defaultOptions,
47
+ cache,
48
+ link: new SchemaLink({
49
+ schema,
50
+ context: (obj, args, context, info) => {
51
+ return {
52
+ ...obj.getContext(),
53
+ app
54
+ }
55
+ }
56
+ })
57
+ })
58
+ }
59
+ }
@@ -0,0 +1,13 @@
1
+ export * from './migrations'
2
+ export * from './initializers/naming-strategy'
3
+ export * from './initializers/database'
4
+ export * from './utils'
5
+ export * from './pubsub'
6
+ export * from './pubsub-log-transport'
7
+ export * from './middlewares'
8
+ export * from './graphql-local-client'
9
+ export * from './service'
10
+
11
+ export * from './typeorm/encrypt-transform'
12
+ export * from './typeorm/json5-transform'
13
+ export * from './typeorm/round-transform'
@@ -0,0 +1,96 @@
1
+ import { createConnection, DataSource, EntityManager, EntityTarget, Repository } from 'typeorm'
2
+
3
+ import { appRootPath, config, logger } from '@things-factory/env'
4
+
5
+ const path = require('path')
6
+
7
+ var ormconfig
8
+ try {
9
+ ormconfig = require(path.resolve(appRootPath, 'ormconfig'))
10
+ } catch (e) {
11
+ ormconfig = require('@things-factory/shell/ormconfig')
12
+ }
13
+
14
+ const dataSources: { [name: string]: DataSource } = {}
15
+
16
+ /**
17
+ * Returns the specified DataSource by name.
18
+ * @param {string} name - The name of the DataSource.
19
+ * @returns {DataSource} - The DataSource with the specified name.
20
+ */
21
+ export function getDataSource(name?: string): DataSource {
22
+ return dataSources[name || 'default']
23
+ }
24
+
25
+ /**
26
+ * Adds a new DataSource with the specified name.
27
+ * @param {string} name - The name of the DataSource to add.
28
+ * @param {DataSource} dataSource - The DataSource to add.
29
+ */
30
+ export function addDataSource(name: string, dataSource: DataSource) {
31
+ dataSources[name] = dataSource
32
+ }
33
+
34
+ /**
35
+ * Removes a DataSource with the specified name.
36
+ * @param {string} name - The name of the DataSource to remove.
37
+ */
38
+ export function removeDataSource(name: string) {
39
+ delete dataSources[name]
40
+ }
41
+
42
+ /**
43
+ * Returns an array of all registered DataSource names.
44
+ * @returns {string[]} - An array of DataSource names.
45
+ */
46
+ export function getDataSourceNames() {
47
+ return Object.keys(dataSources)
48
+ }
49
+
50
+ /**
51
+ * Returns a repository for the specified entity.
52
+ * @param {EntityTarget<X>} target - The target entity for which to get the repository.
53
+ * @returns {Repository<X>} - The repository for the specified entity.
54
+ */
55
+ export function getRepository<X>(target: EntityTarget<X>, tx?: EntityManager): Repository<X> {
56
+ return tx ? tx.getRepository<X>(target) : getDataSource('default').getRepository<X>(target)
57
+ }
58
+
59
+ /**
60
+ * Initializes the database connections and data sources.
61
+ */
62
+ export const databaseInitializer = async () => {
63
+ try {
64
+ const readConnectionConfig = config.get('ormconfig')
65
+
66
+ const dataSource = await createConnection({
67
+ ...ormconfig,
68
+ ...readConnectionConfig
69
+ })
70
+
71
+ addDataSource('default', dataSource)
72
+
73
+ logger.info('Default DataSource established')
74
+
75
+ if (readConnectionConfig.type == 'sqlite' && readConnectionConfig.synchronize == false) {
76
+ await dataSource.query('PRAGMA foreign_keys=OFF')
77
+ await dataSource.synchronize()
78
+ await dataSource.query('PRAGMA foreign_keys=ON')
79
+ }
80
+
81
+ if (config.get('ormconfig4Tx')) {
82
+ const dataSource4Tx = new DataSource({
83
+ ...ormconfig,
84
+ ...config.get('ormconfig4Tx')
85
+ })
86
+ await dataSource4Tx.initialize()
87
+ addDataSource('tx', dataSource4Tx)
88
+
89
+ logger.info('Transaction DataSource established')
90
+ } else {
91
+ addDataSource('tx', dataSource)
92
+ }
93
+ } catch (e) {
94
+ logger.error(e)
95
+ }
96
+ }
@@ -0,0 +1,14 @@
1
+ import { NamingStrategyInterface } from 'typeorm'
2
+ import { DefaultNamingStrategy } from 'typeorm'
3
+ import pluralize from 'pluralize'
4
+ import _ from 'lodash'
5
+
6
+ export class NamingStrategy extends DefaultNamingStrategy implements NamingStrategyInterface {
7
+ tableName(targetName: string, userSpecifiedName: string): string {
8
+ return userSpecifiedName || pluralize(_.snakeCase(targetName))
9
+ }
10
+
11
+ columnName(propertyName: string, customName: string, embeddedPrefixes: string[]): string {
12
+ return _.snakeCase(embeddedPrefixes.concat(customName || propertyName).join('_'))
13
+ }
14
+ }
@@ -0,0 +1,60 @@
1
+ import requestIp from 'request-ip'
2
+ import { getDomainFromURL } from '../utils'
3
+
4
+ export async function domainMiddleware(context: any, next: any) {
5
+ var { domain } = context.state
6
+ if (!domain) {
7
+ /*
8
+ * The domainType should be checked only when signin and checkin.
9
+ * For purposes such as API calls, the target domainType may be different from the system domainType.
10
+ * So, we don't check domainType here.
11
+ */
12
+ domain = await getDomainFromURL(context)
13
+ }
14
+
15
+ if (domain) {
16
+ const ip = requestIp.getClientIp(context.req)
17
+ const { whitelist = [], blacklist = [], protectedlist = [], privileges = [] } = domain.iplist || {}
18
+
19
+ if (Array.isArray(whitelist) && whitelist.length > 0) {
20
+ /* whitelist 우선 */
21
+ const whitelisted =
22
+ Array.isArray(whitelist) &&
23
+ whitelist.some(item => {
24
+ return new RegExp(item).test(ip)
25
+ })
26
+
27
+ if (!whitelisted) {
28
+ context.status = 403
29
+ return
30
+ }
31
+ } else {
32
+ const blacklisted =
33
+ Array.isArray(blacklist) &&
34
+ blacklist.some(item => {
35
+ return new RegExp(item).test(ip)
36
+ })
37
+
38
+ if (blacklisted) {
39
+ context.status = 403
40
+ return
41
+ }
42
+ }
43
+
44
+ if (Array.isArray(protectedlist) && protectedlist.length > 0) {
45
+ const safe = protectedlist.some(item => {
46
+ return new RegExp(item).test(ip)
47
+ })
48
+
49
+ context.state.unsafeIP = !safe
50
+
51
+ if (!safe) {
52
+ context.state.prohibitedPrivileges = privileges
53
+ }
54
+ }
55
+ }
56
+
57
+ context.state.domain = domain
58
+
59
+ return next()
60
+ }
@@ -0,0 +1,43 @@
1
+ import { config, logger } from '@things-factory/env'
2
+
3
+ import { domainMiddleware } from './domain-middleware'
4
+
5
+ export function initMiddlewares(app) {
6
+ app.subdomainOffset = config.get('subdomainOffset', 2)
7
+
8
+ app.on('error', (err, context) => {
9
+ logger.error(err)
10
+ })
11
+
12
+ /*
13
+ * Catching downstream errors
14
+ * - recommend to use context.throw, context.assert
15
+ */
16
+ app.use(async (context, next) => {
17
+ try {
18
+ await next()
19
+ } catch (err) {
20
+ context.status = err?.status || 500
21
+ context.body = err?.message
22
+
23
+ // emitting error to app.on('error', ...)
24
+ context.app.emit('error', err, context)
25
+ }
26
+ })
27
+
28
+ /*
29
+ * post:graphql 에 대해서는 domain을 확인한다.
30
+ * graphql app을 router에 적용하지 못하기 때문임.
31
+ */
32
+ app.use(async (context, next) => {
33
+ const { method, path } = context
34
+
35
+ if (method == 'POST' && path.startsWith('/graphql')) {
36
+ return await domainMiddleware(context, next)
37
+ }
38
+
39
+ await next()
40
+ })
41
+ }
42
+
43
+ export * from './domain-middleware'
@@ -0,0 +1,37 @@
1
+ import { MigrationInterface, QueryRunner } from 'typeorm'
2
+
3
+ import { getRepository } from '../initializers/database'
4
+ import { Domain } from '../service/domain/domain'
5
+
6
+ const SEED_DOMAINS = [
7
+ {
8
+ name: 'SYSTEM',
9
+ subdomain: 'system',
10
+ systemFlag: true
11
+ }
12
+ ]
13
+
14
+ export class SeedDomain1000000000000 implements MigrationInterface {
15
+ public async up(queryRunner: QueryRunner): Promise<any> {
16
+ const repository = getRepository(Domain)
17
+
18
+ return await Promise.all(
19
+ SEED_DOMAINS.map(async domain => {
20
+ await repository.save({
21
+ ...domain
22
+ })
23
+ })
24
+ )
25
+ }
26
+
27
+ public async down(queryRunner: QueryRunner): Promise<any> {
28
+ const repository = getRepository(Domain)
29
+
30
+ return await Promise.all(
31
+ SEED_DOMAINS.reverse().map(async domain => {
32
+ let recode = await repository.findOneBy({ subdomain: domain.subdomain })
33
+ await repository.remove(recode)
34
+ })
35
+ )
36
+ }
37
+ }
@@ -0,0 +1,9 @@
1
+ const glob = require('glob')
2
+ const path = require('path')
3
+
4
+ export var migrations = []
5
+
6
+ glob.sync(path.resolve(__dirname, '.', '**', '*.js')).forEach(function(file) {
7
+ if (file.indexOf('index.js') !== -1) return
8
+ migrations = migrations.concat(Object.values(require(path.resolve(file))) || [])
9
+ })
@@ -0,0 +1,59 @@
1
+ import camelCase from 'lodash/camelCase'
2
+ import Transport from 'winston-transport'
3
+ import { pubsub } from './pubsub'
4
+
5
+ /**
6
+ * PubSubLogTransport is a custom Winston transport that publishes log messages to a GraphQL Pub/Sub topic.
7
+ */
8
+ export class PubSubLogTransport extends Transport {
9
+ /**
10
+ * The source object providing the log messages.
11
+ * @type {object}
12
+ */
13
+ source: object
14
+
15
+ /**
16
+ * The Pub/Sub topic to which log messages will be published.
17
+ * @type {string}
18
+ */
19
+ topic: string
20
+
21
+ /**
22
+ * The name of the field resolver for log messages.
23
+ * @type {string}
24
+ */
25
+ resolver: string /* field resolver name */
26
+
27
+ /**
28
+ * Creates an instance of PubSubLogTransport.
29
+ * @param {object} opts - Options for configuring the transport.
30
+ * @param {string} opts.topic - The Pub/Sub topic to which log messages will be published.
31
+ * @param {object} opts.source - The source object providing the log messages.
32
+ * @param {string} opts.resolver - The name of the field resolver for log messages (default: derived from the topic).
33
+ */
34
+ constructor(opts) {
35
+ super(opts)
36
+
37
+ this.topic = opts.topic
38
+ this.source = opts.source
39
+ this.resolver = opts.resolver || camelCase(this.topic)
40
+ }
41
+
42
+ /**
43
+ * Publishes a log message to the configured Pub/Sub topic.
44
+ * @param {object} info - The log message information.
45
+ * @param {Function} callback - The callback function to be called after publishing the message.
46
+ */
47
+ log(info, callback) {
48
+ setImmediate(() => {
49
+ pubsub.publish(this.topic, {
50
+ [this.resolver]: {
51
+ source: this.source,
52
+ ...info
53
+ }
54
+ })
55
+ })
56
+
57
+ callback()
58
+ }
59
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * @module pubsub
3
+ * @description
4
+ * This module provides a Pub/Sub (Publish/Subscribe) mechanism for handling messages and events.
5
+ * Developers can use various middleware options such as Redis, Redis Cluster
6
+ * to implement Pub/Sub functionality.
7
+ */
8
+
9
+ import { createPubSub } from 'graphql-yoga'
10
+ import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'
11
+ import Redis from 'ioredis'
12
+
13
+ import { config, logger } from '@things-factory/env'
14
+ import { PubSub } from 'type-graphql'
15
+
16
+ const { middleware, host, port, nodes, options } = config.get('pubsub', {})
17
+
18
+ let pubsub: PubSub
19
+
20
+ switch (middleware) {
21
+ case 'redis':
22
+ const redisOption = {
23
+ host,
24
+ port,
25
+ retryStrategy: times => {
26
+ // reconnect after
27
+ return Math.min(times * 50, 2000)
28
+ },
29
+ ...options
30
+ }
31
+ //@ts-ignore
32
+ pubsub = createPubSub({
33
+ eventTarget: createRedisEventTarget({
34
+ publishClient: new Redis(redisOption),
35
+ subscribeClient: new Redis(redisOption)
36
+ })
37
+ })
38
+ break
39
+ case 'redisCluster':
40
+ const cluster = new Redis.Cluster(nodes, options)
41
+ //@ts-ignore
42
+ pubsub = createPubSub({
43
+ eventTarget: createRedisEventTarget({
44
+ publishClient: cluster,
45
+ subscribeClient: cluster
46
+ })
47
+ })
48
+ break
49
+ default:
50
+ pubsub = createPubSub()
51
+ break
52
+ }
53
+
54
+ // kafka pubsub keeps connection and app port with 'ctrl+c' termination.
55
+ const exitHandler = async evt => {
56
+ //@ts-ignore
57
+ if (pubsub.close) {
58
+ try {
59
+ //@ts-ignore
60
+ await pubsub.close()
61
+ } catch (err) {
62
+ logger.error(err)
63
+ }
64
+ }
65
+ }
66
+
67
+ /*
68
+ * exit events hint from https://stackoverflow.com/a/14032965/14539284
69
+ */
70
+
71
+ //do something when app is closing
72
+ process.on('exit', exitHandler.bind(null, { name: 'exit', exit: true }))
73
+
74
+ //catches ctrl+c event
75
+ process.on('SIGINT', exitHandler.bind(null, { name: 'SIGINT', exit: true }))
76
+
77
+ // catches "kill pid" (for example: nodemon restart)
78
+ process.on('SIGUSR1', exitHandler.bind(null, { name: 'SIGUSR1', exit: true }))
79
+ process.on('SIGUSR2', exitHandler.bind(null, { name: 'SIGUSR2', exit: true }))
80
+
81
+ //catches uncaught exceptions
82
+ // process.on('uncaughtException', exitHandler.bind(null, { name: 'uncaughtException', exit: true }))
83
+
84
+ export { pubsub }
@@ -0,0 +1,13 @@
1
+ import Router from 'koa-router'
2
+
3
+ import { domainMiddleware } from '../middlewares/domain-middleware'
4
+
5
+ export const domainPublicRouter = new Router()
6
+ domainPublicRouter.use(async (context, next) => {
7
+ await next()
8
+ }, domainMiddleware)
9
+
10
+ export const domainPrivateRouter = new Router()
11
+ domainPrivateRouter.use(async (context, next) => {
12
+ await next()
13
+ }, domainMiddleware)
@@ -0,0 +1,76 @@
1
+ import Router from 'koa-router'
2
+ import requestIp from 'request-ip'
3
+
4
+ import { getLicenseInfo } from '@things-factory/operato-license-checker'
5
+ import { domainMiddleware } from '../middlewares/domain-middleware'
6
+
7
+ var crawler = require('npm-license-crawler')
8
+
9
+ export const globalPublicRouter = new Router()
10
+ export const globalPrivateRouter = new Router()
11
+
12
+ /* even though global private router, catch domain for information */
13
+ globalPrivateRouter.use(domainMiddleware)
14
+
15
+ if (process.env.NODE_ENV != 'production') {
16
+ globalPublicRouter.get('/graphql', async (context, next) => {
17
+ const initialEndpoint = context.request.href
18
+
19
+ await context.render('graphql', { initialEndpoint })
20
+ })
21
+ }
22
+
23
+ globalPublicRouter.get('/dependencies', async (context, next) => {
24
+ const { dependencyGraph } = require('@things-factory/env')
25
+
26
+ await context.render('dependencies-view-graphviz', { model: dependencyGraph })
27
+ })
28
+
29
+ globalPublicRouter.get('/license-info', (context, next) => {
30
+ context.type = 'application/json'
31
+ context.body = getLicenseInfo()
32
+ })
33
+
34
+ globalPublicRouter.get('/opensource-licenses', (context, next) => {
35
+ return new Promise(function (resolve, reject) {
36
+ var options = {
37
+ start: ['.'],
38
+ exclude: [],
39
+ noColor: true,
40
+ production: true,
41
+ unknown: false
42
+ }
43
+
44
+ crawler.dumpLicenses(options, function (error, res) {
45
+ if (error) {
46
+ console.error('get:/opensource-licenses', error)
47
+ reject(error)
48
+ } else {
49
+ context.type = 'application/json'
50
+ context.body = res
51
+ resolve(res)
52
+ }
53
+ })
54
+ })
55
+ })
56
+
57
+ globalPublicRouter.get('/request-info', context => {
58
+ context.body = `
59
+ Client info. from "requestIp": ${requestIp.getClientIp(context.req)}
60
+
61
+ Client info. from "context.ip": ${context.ip}
62
+ Client info. from "context.protocol": ${context.protocol}
63
+ Client info. from "context.host": ${context.host}
64
+
65
+ Client info. from "x-forwarded-for": ${context.headers['x-forwarded-for']}
66
+ Client info. from "x-forwarded-proto": ${context.headers['x-forwarded-proto']}
67
+ Client info. from "x-forwarded-host": ${context.headers['x-forwarded-host']}
68
+ Client info. from "x-forwarded-port": ${context.headers['x-forwarded-port']}
69
+ `
70
+ })
71
+
72
+ /* Paths starting with /public are assumed to use the koa-view renderer. */
73
+ globalPublicRouter.get('/public/(.[^.]*)', async (context, next) => {
74
+ const { path } = context
75
+ await context.render(path.substr(1))
76
+ })
@@ -0,0 +1,3 @@
1
+ import Router from 'koa-router'
2
+
3
+ export const graphqlRouter = new Router()
@@ -0,0 +1,3 @@
1
+ export * from './global-router'
2
+ export * from './domain-router'
3
+ export * from './graphql-router'