@canmingir/link-express 1.6.11 → 1.7.1

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/.eslintrc.ts +29 -0
  2. package/bin/event-listener.ts +10 -0
  3. package/index.ts +2 -0
  4. package/package.json +66 -12
  5. package/{prepare.js → prepare.ts} +1 -1
  6. package/src/{authorization.js → authorization.ts} +31 -9
  7. package/src/config.ts +112 -0
  8. package/src/dynamodb.ts +24 -0
  9. package/src/{error.js → error.ts} +30 -11
  10. package/src/event/client/adapters/KafkaAdapter.ts +140 -0
  11. package/src/event/client/adapters/SocketAdapter.ts +59 -0
  12. package/src/event/client/adapters/TxEventQAdapter.ts +232 -0
  13. package/src/event/client/eventManager.ts +244 -0
  14. package/src/event/client/index.ts +50 -0
  15. package/src/event/client/metrics.ts +171 -0
  16. package/src/event/client/types/types.ts +43 -0
  17. package/src/event/index.ts +3 -0
  18. package/src/event/server/server.ts +55 -0
  19. package/src/event/src/Event.ts +201 -0
  20. package/src/express.ts +66 -0
  21. package/src/lib/{settings.js → settings.ts} +16 -4
  22. package/src/lib/test.ts +68 -0
  23. package/src/{logger.js → logger.ts} +11 -9
  24. package/src/metrics/{dbMetrics.js → dbMetrics.ts} +37 -14
  25. package/src/models/Organization.model.ts +27 -0
  26. package/src/models/Permission.model.ts +48 -0
  27. package/src/models/Project.model.ts +50 -0
  28. package/src/models/Settings.model.ts +31 -0
  29. package/src/models/{index.js → index.ts} +8 -8
  30. package/src/platform.ts +55 -0
  31. package/src/postgres.ts +309 -0
  32. package/src/routes/index.ts +8 -0
  33. package/src/routes/metrics.ts +13 -0
  34. package/src/routes/oauth.ts +267 -0
  35. package/src/routes/{organizations.js → organizations.ts} +10 -8
  36. package/src/routes/{permissions.js → permissions.ts} +8 -6
  37. package/src/routes/{projects.js → projects.ts} +22 -16
  38. package/src/routes/settings.ts +31 -0
  39. package/src/schemas/{Organization.js → Organization.ts} +2 -2
  40. package/src/schemas/{Permission.js → Permission.ts} +2 -2
  41. package/src/schemas/{Project.js → Project.ts} +2 -2
  42. package/src/schemas/index.ts +5 -0
  43. package/src/sequelize.ts +13 -0
  44. package/src/{test.js → test.ts} +11 -13
  45. package/src/types/Organization.ts +9 -0
  46. package/src/types/Permission.ts +13 -0
  47. package/src/types/Project.ts +14 -0
  48. package/src/types/index.ts +5 -0
  49. package/tsconfig.json +32 -0
  50. package/.eslintrc.js +0 -20
  51. package/index.js +0 -1
  52. package/src/config.js +0 -21
  53. package/src/dynamodb.js +0 -18
  54. package/src/express.js +0 -58
  55. package/src/lib/test.js +0 -69
  56. package/src/models/Organization.js +0 -17
  57. package/src/models/Permission.js +0 -33
  58. package/src/models/Project.js +0 -37
  59. package/src/models/Settings.js +0 -21
  60. package/src/openapi.js +0 -40
  61. package/src/platform.js +0 -56
  62. package/src/postgres.js +0 -308
  63. package/src/routes/index.js +0 -15
  64. package/src/routes/metrics.js +0 -12
  65. package/src/routes/oauth.js +0 -213
  66. package/src/routes/settings.js +0 -25
  67. package/src/schemas/index.js +0 -5
package/.eslintrc.ts ADDED
@@ -0,0 +1,29 @@
1
+ export default {
2
+ env: {
3
+ es6: true,
4
+ node: true,
5
+ mocha: true,
6
+ },
7
+ parser: "@typescript-eslint/parser",
8
+ parserOptions: {
9
+ ecmaVersion: 2022,
10
+ sourceType: "module",
11
+ project: "./tsconfig.json",
12
+ },
13
+ plugins: ["@typescript-eslint"],
14
+ extends: [
15
+ "eslint:recommended",
16
+ "plugin:@typescript-eslint/recommended",
17
+ "plugin:prettier/recommended",
18
+ ],
19
+ ignorePatterns: ["/node_modules", "/dist"],
20
+ rules: {
21
+ eqeqeq: ["error", "always"],
22
+ "no-console": "off",
23
+ "no-eval": "error",
24
+ "no-var": "error",
25
+ "prefer-arrow-callback": "error",
26
+ "@typescript-eslint/no-explicit-any": "error",
27
+ "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }],
28
+ },
29
+ };
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env node
2
+
3
+ require('ts-node').register({
4
+ transpileOnly: true,
5
+ compilerOptions: {
6
+ module: 'commonjs'
7
+ }
8
+ });
9
+
10
+ require('../src/event/server/server.ts');
package/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ import * as platform from "./src/platform";
2
+ export default platform;
package/package.json CHANGED
@@ -1,22 +1,57 @@
1
1
  {
2
2
  "name": "@canmingir/link-express",
3
- "version": "1.6.11",
3
+ "version": "1.7.1",
4
4
  "description": "",
5
- "main": "index.js",
5
+ "main": "index.ts",
6
+ "types": "index.ts",
7
+ "typesVersions": {
8
+ "*": {
9
+ "models": [
10
+ "src/models/index.ts"
11
+ ],
12
+ "test": [
13
+ "src/test.ts"
14
+ ],
15
+ "authorization": [
16
+ "src/authorization.ts"
17
+ ],
18
+ "error": [
19
+ "src/error.ts"
20
+ ],
21
+ "types": [
22
+ "src/types/index.ts"
23
+ ],
24
+ "sequelize": [
25
+ "src/sequelize.ts"
26
+ ],
27
+ "event": [
28
+ "src/event/index.ts"
29
+ ]
30
+ }
31
+ },
6
32
  "author": "NucTeam",
33
+ "bin": {
34
+ "event-listener": "./bin/event-listener.ts"
35
+ },
7
36
  "exports": {
8
- ".": "./index.js",
9
- "./models": "./src/models/index.js",
10
- "./test": "./src/test.js",
11
- "./authorization": "./src/authorization.js",
12
- "./error": "./src/error.js"
37
+ ".": "./index.ts",
38
+ "./models": "./src/models/index.ts",
39
+ "./test": "./src/test.ts",
40
+ "./authorization": "./src/authorization.ts",
41
+ "./error": "./src/error.ts",
42
+ "./types": "./src/types/index.ts",
43
+ "./sequelize": "./src/sequelize.ts",
44
+ "./event": "./src/event/serc/Event.ts",
45
+ "./event/client": "./src/event/client/index.ts",
46
+ "./event/server": "./src/event/server/server.ts"
13
47
  },
14
48
  "scripts": {
15
- "prepare": "node prepare.js",
49
+ "prepare": "tsx prepare.ts",
50
+ "build": "tsc",
16
51
  "dev": "node server --dev",
17
52
  "start": "node server",
18
53
  "test": "echo 'No tests'",
19
- "lint": "eslint . --ext .js"
54
+ "lint": "eslint . --ext .ts"
20
55
  },
21
56
  "dependencies": {
22
57
  "@aws-sdk/client-dynamodb": "^3.614.0",
@@ -31,18 +66,35 @@
31
66
  "joi": "^17.13.3",
32
67
  "joi-to-swagger": "^6.2.0",
33
68
  "jsonwebtoken": "^9.0.2",
69
+ "kafkajs": "^2.2.4",
34
70
  "lodash": "^4.17.21",
35
71
  "morgan": "^1.10.0",
72
+ "oracledb": "^6.10.0",
36
73
  "pg": "^8.12.0",
37
74
  "pino": "^10.1.0",
38
75
  "pino-elasticsearch": "^8.1.0",
39
76
  "prom-client": "^15.1.3",
40
- "sequelize": "^6.37.3",
77
+ "sequelize": "^6.37.7",
78
+ "sequelize-typescript": "^2.1.6",
79
+ "socket.io": "^4.8.1",
80
+ "socket.io-client": "^4.8.1",
41
81
  "swagger-jsdoc": "^6.2.8",
42
- "swagger-ui-express": "^5.0.1",
82
+ "ts-node": "^10.9.2",
43
83
  "uuid": "^10.0.0"
44
84
  },
45
85
  "devDependencies": {
86
+ "@types/cors": "^2.8.19",
87
+ "@types/express": "^5.0.5",
88
+ "@types/jsonwebtoken": "^9.0.10",
89
+ "@types/lodash": "^4.17.21",
90
+ "@types/morgan": "^1.9.10",
91
+ "@types/node": "^24.10.1",
92
+ "@types/pg": "^8.15.6",
93
+ "@types/swagger-jsdoc": "^6.0.4",
94
+ "@types/swagger-ui-express": "^4.1.8",
95
+ "@types/uuid": "^10.0.0",
96
+ "@typescript-eslint/eslint-plugin": "^8.48.0",
97
+ "@typescript-eslint/parser": "^8.48.0",
46
98
  "axios-mock-adapter": "^1.21.1",
47
99
  "eslint": "^8.57.1",
48
100
  "eslint-config-prettier": "^8.5.0",
@@ -50,6 +102,8 @@
50
102
  "mocha": "10.8.2",
51
103
  "prettier": "^2.6.2",
52
104
  "sqlite3": "^5.1.6",
53
- "supertest": "^7.0.0"
105
+ "supertest": "^7.0.0",
106
+ "tsx": "^4.20.6",
107
+ "typescript": "^5.9.3"
54
108
  }
55
109
  }
@@ -1,4 +1,4 @@
1
- const fs = require("fs");
1
+ import fs from "fs";
2
2
 
3
3
  const prePush = `#!/usr/bin/env node
4
4
  const { execSync } = require("child_process");
@@ -1,6 +1,22 @@
1
- const jwt = require("jsonwebtoken");
2
- const { AuthorizationError } = require("./error");
3
- function verify(req, res, next) {
1
+ import jwt from "jsonwebtoken";
2
+ import { Request, Response, NextFunction } from "express";
3
+ import { AuthorizationError } from "./error";
4
+
5
+ interface Session {
6
+ projectId: string;
7
+ userId: string;
8
+ roles: string[];
9
+ appId: string;
10
+ organizationId: string;
11
+ }
12
+
13
+ declare module "express-serve-static-core" {
14
+ interface Request {
15
+ session: Session;
16
+ }
17
+ }
18
+
19
+ function verify(req: Request, _res: Response, next: NextFunction): void {
4
20
  if (process.env.PROFILE === "TEST") {
5
21
  switch (process.env.PROJECT_ID) {
6
22
  case "0c756054-2d28-4f87-9b12-8023a79136a5":
@@ -69,8 +85,15 @@ function verify(req, res, next) {
69
85
  try {
70
86
  const { sub, aud, rls, aid, oid } = jwt.verify(
71
87
  token,
72
- process.env.JWT_SECRET
73
- );
88
+ process.env.JWT_SECRET as string
89
+ ) as {
90
+ sub: string;
91
+ aud: string;
92
+ rls: string[];
93
+ aid: string;
94
+ oid: string;
95
+ };
96
+
74
97
  req.session = {
75
98
  projectId: aud,
76
99
  userId: sub,
@@ -85,9 +108,8 @@ function verify(req, res, next) {
85
108
  next();
86
109
  }
87
110
 
88
- // eslint-disable-next-line no-unused-vars
89
- function authorize(role) {
90
- return (req, res, next) => {
111
+ function authorize(_role: string) {
112
+ return (req: Request, _res: Response, next: NextFunction): void => {
91
113
  const { roles } = req.session;
92
114
 
93
115
  // TODO Add expression check for role
@@ -99,4 +121,4 @@ function authorize(role) {
99
121
  };
100
122
  }
101
123
 
102
- module.exports = { verify, authorize };
124
+ export { verify, authorize };
package/src/config.ts ADDED
@@ -0,0 +1,112 @@
1
+ import _ from "lodash";
2
+
3
+ interface LoggerConfig {
4
+ elasticsearch: {
5
+ index: string;
6
+ consistency?: string;
7
+ node: string;
8
+ esVersion?: number;
9
+ flushBytes?: number;
10
+ };
11
+ }
12
+
13
+ interface ProjectConfig {
14
+ name: string;
15
+ version: string;
16
+ oauth?: {
17
+ jwt: {
18
+ identifier: string;
19
+ };
20
+ providers: Record<
21
+ string,
22
+ {
23
+ tokenUrl: string;
24
+ userUrl: string;
25
+ clientId: string;
26
+ redirectUri?: string;
27
+ userIdentifier: string;
28
+ userFields: {
29
+ name: string;
30
+ displayName: string;
31
+ avatarUrl: string;
32
+ email: string;
33
+ };
34
+ }
35
+ >;
36
+ };
37
+ }
38
+
39
+ interface LinkConfig {
40
+ [key: string]: unknown;
41
+ }
42
+
43
+ interface OpenapiConfig {
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ interface PostgresConfig {
48
+ uri: string;
49
+ debug?: boolean;
50
+ sync?: boolean;
51
+ }
52
+
53
+ interface DynamodbConfig {
54
+ region: string;
55
+ }
56
+
57
+ interface PushGatewayConfig {
58
+ url?: string;
59
+ jobName?: string;
60
+ instance?: string;
61
+ interval?: number;
62
+ }
63
+
64
+ interface MetricsConfig {
65
+ enabled: boolean;
66
+ url?: string;
67
+ pushGateway: PushGatewayConfig;
68
+ interval?: number;
69
+ }
70
+
71
+ interface Config {
72
+ link: LinkConfig;
73
+ project: ProjectConfig | null;
74
+ openapi: OpenapiConfig;
75
+ postgres: PostgresConfig | null;
76
+ dynamodb: DynamodbConfig | null;
77
+ pushGateway: PushGatewayConfig;
78
+ title?: string;
79
+ version?: string;
80
+ logger?: LoggerConfig;
81
+ metrics?: MetricsConfig;
82
+ }
83
+
84
+ let _config: Config = {
85
+ link: {},
86
+ project: null,
87
+ openapi: {},
88
+ postgres: null,
89
+ dynamodb: null,
90
+ pushGateway: {},
91
+ };
92
+
93
+ function init(config: Partial<Config> = {}): Config {
94
+ _config = _.merge(
95
+ {
96
+ link: {},
97
+ project: null,
98
+ openapi: {},
99
+ postgres: null,
100
+ dynamodb: null,
101
+ pushGateway: {},
102
+ },
103
+ config
104
+ );
105
+
106
+ return _config;
107
+ }
108
+
109
+ const getConfig = (): Config => _config;
110
+
111
+ export { getConfig, init, Config };
112
+ export default getConfig;
@@ -0,0 +1,24 @@
1
+ import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
2
+ import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
3
+ import { getConfig } from "./config";
4
+ import dotenv from "dotenv";
5
+
6
+ dotenv.config();
7
+
8
+ const { dynamodb } = getConfig();
9
+
10
+ if (!dynamodb) {
11
+ throw new Error("DynamoDB configuration is required");
12
+ }
13
+
14
+ const client = new DynamoDBClient({
15
+ region: dynamodb.region,
16
+ credentials: {
17
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID as string,
18
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY as string,
19
+ },
20
+ });
21
+
22
+ const docClient = DynamoDBDocumentClient.from(client);
23
+
24
+ export { docClient };
@@ -1,7 +1,31 @@
1
- const { ValidationError } = require("joi");
1
+ import { ValidationError } from "joi";
2
+ import { Request, Response, NextFunction } from "express";
2
3
 
3
- // eslint-disable-next-line no-unused-vars
4
- const handle = (err, req, res, next) => {
4
+ interface AxiosError extends Error {
5
+ isAxiosError: boolean;
6
+ response?: {
7
+ status: number;
8
+ };
9
+ }
10
+
11
+ interface ErrorWithError {
12
+ error: unknown;
13
+ }
14
+
15
+ const handle = (
16
+ err:
17
+ | string
18
+ | ValidationError
19
+ | AuthorizationError
20
+ | AuthenticationError
21
+ | NotFoundError
22
+ | AxiosError
23
+ | ErrorWithError
24
+ | Error,
25
+ _req: Request,
26
+ res: Response,
27
+ _next: NextFunction
28
+ ): Response => {
5
29
  if (typeof err === "string") {
6
30
  return res.status(400).json({ error: err });
7
31
  }
@@ -22,11 +46,11 @@ const handle = (err, req, res, next) => {
22
46
  return res.status(404).end();
23
47
  }
24
48
 
25
- if (err.isAxiosError) {
49
+ if ("isAxiosError" in err && err.isAxiosError) {
26
50
  return res.status(err.response?.status || 503).end();
27
51
  }
28
52
 
29
- if (err.error) {
53
+ if ("error" in err && err.error) {
30
54
  return res.status(400).json(err);
31
55
  } else {
32
56
  console.error(err);
@@ -38,9 +62,4 @@ class AuthorizationError extends Error {}
38
62
  class AuthenticationError extends Error {}
39
63
  class NotFoundError extends Error {}
40
64
 
41
- module.exports = {
42
- handle,
43
- AuthorizationError,
44
- NotFoundError,
45
- AuthenticationError,
46
- };
65
+ export { handle, AuthorizationError, NotFoundError, AuthenticationError };
@@ -0,0 +1,140 @@
1
+ import { Consumer, Kafka, Producer } from "kafkajs";
2
+
3
+ import { EventAdapter } from "../types/types";
4
+
5
+ export class KafkaAdapter implements EventAdapter {
6
+ private kafka: Kafka;
7
+ private consumer: Consumer | null = null;
8
+ private producer: Producer | null = null;
9
+ private messageHandler?: (type: string, payload: object) => void;
10
+
11
+ constructor(
12
+ private readonly options: {
13
+ clientId: string;
14
+ brokers: string[];
15
+ groupId: string;
16
+ topics: string[];
17
+ }
18
+ ) {
19
+ this.kafka = new Kafka({
20
+ clientId: options.clientId,
21
+ brokers: options.brokers,
22
+ });
23
+ }
24
+
25
+ async connect(): Promise<void> {
26
+ this.producer = this.kafka.producer();
27
+ await this.producer.connect();
28
+ this.consumer = this.kafka.consumer({ groupId: this.options.groupId });
29
+
30
+ await this.consumer.connect();
31
+ await this.consumer.subscribe({
32
+ topics: [/^(?!__).*$/],
33
+ fromBeginning: false,
34
+ });
35
+ await this.consumer.run({
36
+ partitionsConsumedConcurrently: 160,
37
+ eachMessage: async ({ topic, message }) => {
38
+ if (topic.startsWith("__")) {
39
+ return;
40
+ }
41
+
42
+ if (this.messageHandler) {
43
+ try {
44
+ const payload = JSON.parse(message.value?.toString() || "{}");
45
+ this.messageHandler(topic, payload);
46
+ } catch (error) {
47
+ console.error(
48
+ `Error processing message for topic ${topic}:`,
49
+ error
50
+ );
51
+ }
52
+ }
53
+ },
54
+ });
55
+ console.log(`Kafka consumer connected`);
56
+ }
57
+
58
+ async disconnect(): Promise<void> {
59
+ if (this.consumer) {
60
+ await this.consumer.stop();
61
+ await this.consumer.disconnect();
62
+ this.consumer = null;
63
+ }
64
+ if (this.producer) {
65
+ await this.producer.disconnect();
66
+ this.producer = null;
67
+ }
68
+ }
69
+
70
+ async publish<T = object>(type: string, payload: T): Promise<void> {
71
+ if (!this.producer) {
72
+ throw new Error("Producer not connected");
73
+ }
74
+ this.producer.send({
75
+ topic: type,
76
+ messages: [{ value: JSON.stringify(payload) }],
77
+ }).then(() => {
78
+ console.log(`Message published to topic ${type}`);
79
+ }).catch((error) => {
80
+ console.error(`Error publishing message to topic ${type}:`, error);
81
+ return Promise.reject(error);
82
+ });
83
+ }
84
+
85
+ async subscribe(type: string): Promise<void> {
86
+ // No-op: EventManager handles callback registration in memory
87
+ }
88
+
89
+ async unsubscribe(type: string): Promise<void> {
90
+ // No-op: EventManager handles callback removal in memory
91
+ }
92
+
93
+ onMessage(handler: (type: string, payload: object) => void): void {
94
+ this.messageHandler = handler;
95
+ }
96
+
97
+ async getBacklog(topics: string[]): Promise<Map<string, number>> {
98
+ const backlogMap = new Map<string, number>();
99
+
100
+ if (topics.length === 0) {
101
+ return backlogMap;
102
+ }
103
+
104
+ const admin = this.kafka.admin();
105
+ await admin.connect();
106
+
107
+ try {
108
+ for (const topic of topics) {
109
+ const offsetsResponse = await admin.fetchOffsets({
110
+ groupId: this.options.groupId,
111
+ topics: [topic],
112
+ });
113
+
114
+ const topicOffsets = await admin.fetchTopicOffsets(topic);
115
+ let totalLag = 0;
116
+
117
+ const topicResponse = offsetsResponse.find((r) => r.topic === topic);
118
+ if (topicResponse) {
119
+ topicResponse.partitions.forEach((partitionOffset) => {
120
+ const latestOffset = topicOffsets.find(
121
+ (to) => to.partition === partitionOffset.partition
122
+ );
123
+
124
+ if (latestOffset) {
125
+ const consumerOffset = parseInt(partitionOffset.offset);
126
+ const latestOffsetValue = parseInt(latestOffset.offset);
127
+ totalLag += Math.max(0, latestOffsetValue - consumerOffset);
128
+ }
129
+ });
130
+ }
131
+
132
+ backlogMap.set(topic, totalLag);
133
+ }
134
+ } finally {
135
+ await admin.disconnect();
136
+ }
137
+
138
+ return backlogMap;
139
+ }
140
+ }
@@ -0,0 +1,59 @@
1
+ import { Socket, io } from "socket.io-client";
2
+
3
+ import { EventAdapter } from "../types/types";
4
+
5
+ export class SocketAdapter implements EventAdapter {
6
+ private socket: Socket | null = null;
7
+ private messageHandler?: (type: string, payload: object) => void;
8
+
9
+ constructor(private readonly options: {
10
+ host: string;
11
+ port?: number;
12
+ protocol: string;
13
+ }) {}
14
+
15
+ async connect(): Promise<void> {
16
+ const { host, port, protocol } = this.options;
17
+ const socketPath = port ? `${protocol}://${host}:${port}` : `${protocol}://${host}`;
18
+
19
+ this.socket = io(socketPath);
20
+
21
+ this.socket.on("event", ({ type, payload }: { type: string; payload: object }) => {
22
+ if (this.messageHandler) {
23
+ this.messageHandler(type, payload);
24
+ }
25
+ });
26
+ }
27
+
28
+ async disconnect(): Promise<void> {
29
+ if (this.socket) {
30
+ this.socket.disconnect();
31
+ this.socket = null;
32
+ }
33
+ }
34
+
35
+ async publish(type: string, payload: object): Promise<void> {
36
+ if (!this.socket) {
37
+ throw new Error("Socket not connected");
38
+ }
39
+ this.socket.emit("publish", { type, payload });
40
+ }
41
+
42
+ async subscribe(type: string): Promise<void> {
43
+ if (!this.socket) {
44
+ throw new Error("Socket not connected");
45
+ }
46
+ this.socket.emit("subscribe", type);
47
+ }
48
+
49
+ async unsubscribe(type: string): Promise<void> {
50
+ if (!this.socket) {
51
+ throw new Error("Socket not connected");
52
+ }
53
+ this.socket.emit("unsubscribe", type);
54
+ }
55
+
56
+ onMessage(handler: (type: string, payload: object) => void): void {
57
+ this.messageHandler = handler;
58
+ }
59
+ }