This commit is contained in:
parent
f665fcbb54
commit
148182c680
42
.dockerignore
Normal file
42
.dockerignore
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
# Git
|
||||||
|
**/.git/
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
**/*.bak
|
||||||
|
|
||||||
|
# NodeJS
|
||||||
|
**/node_modules/
|
||||||
|
|
||||||
|
# coverage test output
|
||||||
|
.nyc_output
|
||||||
|
coverage/
|
||||||
|
|
||||||
|
# log files
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Yarn2 (PnP)
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/releases/
|
||||||
|
|
||||||
|
# tsc buildinfo
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# VScode
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Test output
|
||||||
|
**/.nyc_output
|
||||||
|
|
||||||
|
# Volumes (for Docker deployment)
|
||||||
|
volumes/*
|
||||||
|
|
||||||
|
# App specific
|
||||||
|
.env
|
||||||
|
dist/
|
||||||
|
scripts/
|
||||||
5
.editorconfig
Normal file
5
.editorconfig
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
[*]
|
||||||
|
tab_width = 2
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
4
.eslintignore
Normal file
4
.eslintignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
42
.eslintrc.js
Normal file
42
.eslintrc.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
es2021: true,
|
||||||
|
},
|
||||||
|
parser: '@typescript-eslint/parser',
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:@typescript-eslint/recommended',
|
||||||
|
'plugin:import/recommended',
|
||||||
|
'plugin:import/typescript',
|
||||||
|
'prettier',
|
||||||
|
],
|
||||||
|
settings: {
|
||||||
|
'import/resolver': { typescript: {} },
|
||||||
|
'import/external-module-folders': ['node_modules', '.yarn'],
|
||||||
|
},
|
||||||
|
parserOptions: {
|
||||||
|
project: './tsconfig.json',
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'module',
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-underscore-dangle': 'off',
|
||||||
|
'require-await': 'warn',
|
||||||
|
eqeqeq: 'warn',
|
||||||
|
'import/extensions': 'off',
|
||||||
|
'import/order': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
named: true,
|
||||||
|
alphabetize: { order: 'asc' },
|
||||||
|
warnOnUnassignedImports: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
'import/no-cycle': ['error', { ignoreExternal: true }],
|
||||||
|
'import/no-self-import': 'error',
|
||||||
|
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
},
|
||||||
|
};
|
||||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
/.yarn/** linguist-vendored
|
||||||
|
/.yarn/releases/* binary
|
||||||
|
/.yarn/plugins/**/* binary
|
||||||
|
/.pnp.* binary linguist-generated
|
||||||
34
.gitea/workflows/release.yaml
Normal file
34
.gitea/workflows/release.yaml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
name: Release Docker Image
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- v*
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Gitea
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars._CI_REGISTRY }}
|
||||||
|
username: ${{ vars._CI_REGISTRY_USER }}
|
||||||
|
password: ${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
# Remarks: Setting up QEMU and Buildx are useful for cross-platform build but it will slow the CI pipeline.
|
||||||
|
|
||||||
|
# - name: Set up QEMU
|
||||||
|
# uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
# - name: Set up Docker Buildx
|
||||||
|
# uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
secrets: JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN=${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||||
|
push: true
|
||||||
|
tags: ${{ vars._CI_REGISTRY }}/${{ gitea.repository }}:${{ gitea.ref_name }},${{ vars._CI_REGISTRY }}/${{ gitea.repository }}:latest
|
||||||
27
.gitea/workflows/test.yaml
Normal file
27
.gitea/workflows/test.yaml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
name: Test
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- .gitea/workflows/test.yaml
|
||||||
|
- src/**/*
|
||||||
|
- test/**/*
|
||||||
|
- .mocharc.js
|
||||||
|
- tsconfig.json
|
||||||
|
- package.json
|
||||||
|
- .yarnrc.yml
|
||||||
|
- yarn.lock
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
image: node:lts
|
||||||
|
env:
|
||||||
|
JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN: ${{ secrets._CI_REGISTRY_ACCESS_TOKEN }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Install packages
|
||||||
|
run: corepack enable && yarn
|
||||||
|
- name: Test code
|
||||||
|
run: yarn test:coverage
|
||||||
46
.gitignore
vendored
Normal file
46
.gitignore
vendored
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
node_modules
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# npm settings
|
||||||
|
.npmrc
|
||||||
|
|
||||||
|
# coverage test output
|
||||||
|
.nyc_output
|
||||||
|
/coverage/
|
||||||
|
|
||||||
|
# log files
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Backup
|
||||||
|
*.bak
|
||||||
|
|
||||||
|
# yarn files (sdks are excluded as it depends on vscode version)
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# tsc buildinfo
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# tsc build output
|
||||||
|
/dist/
|
||||||
|
|
||||||
|
# Volumes (for Docker deployment)
|
||||||
|
/volumes/*
|
||||||
|
|
||||||
|
# env
|
||||||
|
/.env
|
||||||
|
|
||||||
|
# GitKeep
|
||||||
|
!.gitkeep
|
||||||
15
.mocharc.js
Normal file
15
.mocharc.js
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
const [_major, _minor] = process.version.slice(1).split('.', 2);
|
||||||
|
|
||||||
|
const major = parseInt(_major, 10);
|
||||||
|
const minor = parseInt(_minor, 10);
|
||||||
|
|
||||||
|
// Disable experimental-strip-types to handle enum transform with ts-node
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
require: 'ts-node/register',
|
||||||
|
spec: ['test/**/*.spec.ts'],
|
||||||
|
'node-option':
|
||||||
|
major > 22 || (major === 22 && minor >= 6)
|
||||||
|
? ['no-experimental-strip-types']
|
||||||
|
: [],
|
||||||
|
};
|
||||||
5
.prettierignore
Normal file
5
.prettierignore
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
dist/
|
||||||
|
node_modules/
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
.vscode/*
|
||||||
15
.prettierrc
Normal file
15
.prettierrc
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"useTabs": false,
|
||||||
|
"arrowParens": "always",
|
||||||
|
"singleQuote": true,
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": ".yarnrc.yml",
|
||||||
|
"options": {
|
||||||
|
"singleQuote": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
7
.vscode/extensions.json
vendored
Normal file
7
.vscode/extensions.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"arcanis.vscode-zipfs",
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode"
|
||||||
|
]
|
||||||
|
}
|
||||||
10
.vscode/settings.json
vendored
Normal file
10
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"search.exclude": {
|
||||||
|
"**/.yarn": true,
|
||||||
|
"**/.pnp.*": true
|
||||||
|
},
|
||||||
|
"eslint.nodePath": ".yarn/sdks",
|
||||||
|
"prettier.prettierPath": ".yarn/sdks/prettier/index.cjs",
|
||||||
|
"typescript.tsdk": ".yarn/sdks/typescript/lib",
|
||||||
|
"typescript.enablePromptUseWorkspaceTsdk": true
|
||||||
|
}
|
||||||
BIN
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
BIN
.yarn/releases/yarn-4.12.0.cjs
vendored
Executable file
Binary file not shown.
32
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
32
.yarn/sdks/eslint/bin/eslint.js
vendored
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint/bin/eslint.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real eslint/bin/eslint.js your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`eslint/bin/eslint.js`));
|
||||||
32
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
32
.yarn/sdks/eslint/lib/api.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real eslint your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`eslint`));
|
||||||
32
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
32
.yarn/sdks/eslint/lib/unsupported-api.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require eslint/use-at-your-own-risk
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real eslint/use-at-your-own-risk your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`eslint/use-at-your-own-risk`));
|
||||||
14
.yarn/sdks/eslint/package.json
vendored
Normal file
14
.yarn/sdks/eslint/package.json
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"name": "eslint",
|
||||||
|
"version": "8.57.1-sdk",
|
||||||
|
"main": "./lib/api.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": {
|
||||||
|
"eslint": "./bin/eslint.js"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
"./package.json": "./package.json",
|
||||||
|
".": "./lib/api.js",
|
||||||
|
"./use-at-your-own-risk": "./lib/unsupported-api.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
5
.yarn/sdks/integrations.yml
vendored
Normal file
5
.yarn/sdks/integrations.yml
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# This file is automatically generated by @yarnpkg/sdks.
|
||||||
|
# Manual changes might be lost!
|
||||||
|
|
||||||
|
integrations:
|
||||||
|
- vscode
|
||||||
32
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
32
.yarn/sdks/prettier/bin/prettier.cjs
vendored
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require prettier/bin/prettier.cjs
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real prettier/bin/prettier.cjs your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`prettier/bin/prettier.cjs`));
|
||||||
32
.yarn/sdks/prettier/index.cjs
vendored
Normal file
32
.yarn/sdks/prettier/index.cjs
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require prettier
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real prettier your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`prettier`));
|
||||||
7
.yarn/sdks/prettier/package.json
vendored
Normal file
7
.yarn/sdks/prettier/package.json
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"name": "prettier",
|
||||||
|
"version": "3.7.4-sdk",
|
||||||
|
"main": "./index.cjs",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": "./bin/prettier.cjs"
|
||||||
|
}
|
||||||
32
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
32
.yarn/sdks/typescript/bin/tsc
vendored
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsc
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsc your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsc`));
|
||||||
32
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
32
.yarn/sdks/typescript/bin/tsserver
vendored
Executable file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/bin/tsserver
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real typescript/bin/tsserver your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`typescript/bin/tsserver`));
|
||||||
32
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
32
.yarn/sdks/typescript/lib/tsc.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsc.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsc.js your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`typescript/lib/tsc.js`));
|
||||||
248
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
248
.yarn/sdks/typescript/lib/tsserver.js
vendored
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserver.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
const moduleWrapper = exports => {
|
||||||
|
return wrapWithUserWrapper(moduleWrapperFn(exports));
|
||||||
|
};
|
||||||
|
|
||||||
|
const moduleWrapperFn = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
||||||
|
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
||||||
|
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
||||||
|
if (major > 5 || (major === 5 && minor >= 5)) {
|
||||||
|
moduleWrapper(absRequire(`typescript`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserver.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserver.js`));
|
||||||
248
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
248
.yarn/sdks/typescript/lib/tsserverlibrary.js
vendored
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript/lib/tsserverlibrary.js
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
const moduleWrapper = exports => {
|
||||||
|
return wrapWithUserWrapper(moduleWrapperFn(exports));
|
||||||
|
};
|
||||||
|
|
||||||
|
const moduleWrapperFn = tsserver => {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
return tsserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {isAbsolute} = require(`path`);
|
||||||
|
const pnpApi = require(`pnpapi`);
|
||||||
|
|
||||||
|
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
|
||||||
|
const isPortal = str => str.startsWith("portal:/");
|
||||||
|
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
|
||||||
|
|
||||||
|
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
|
||||||
|
return `${locator.name}@${locator.reference}`;
|
||||||
|
}));
|
||||||
|
|
||||||
|
// VSCode sends the zip paths to TS using the "zip://" prefix, that TS
|
||||||
|
// doesn't understand. This layer makes sure to remove the protocol
|
||||||
|
// before forwarding it to TS, and to add it back on all returned paths.
|
||||||
|
|
||||||
|
function toEditorPath(str) {
|
||||||
|
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
|
||||||
|
if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
|
||||||
|
// We also take the opportunity to turn virtual paths into physical ones;
|
||||||
|
// this makes it much easier to work with workspaces that list peer
|
||||||
|
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
|
||||||
|
// file instances instead of the real ones.
|
||||||
|
//
|
||||||
|
// We only do this to modules owned by the the dependency tree roots.
|
||||||
|
// This avoids breaking the resolution when jumping inside a vendor
|
||||||
|
// with peer dep (otherwise jumping into react-dom would show resolution
|
||||||
|
// errors on react).
|
||||||
|
//
|
||||||
|
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
|
||||||
|
if (resolved) {
|
||||||
|
const locator = pnpApi.findPackageLocator(resolved);
|
||||||
|
if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
|
||||||
|
str = resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
str = normalize(str);
|
||||||
|
|
||||||
|
if (str.match(/\.zip\//)) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
// Absolute VSCode `Uri.fsPath`s need to start with a slash.
|
||||||
|
// VSCode only adds it automatically for supported schemes,
|
||||||
|
// so we have to do it manually for the `zip` scheme.
|
||||||
|
// The path needs to start with a caret otherwise VSCode doesn't handle the protocol
|
||||||
|
//
|
||||||
|
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
|
||||||
|
//
|
||||||
|
// 2021-10-08: VSCode changed the format in 1.61.
|
||||||
|
// Before | ^zip:/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-04-06: VSCode changed the format in 1.66.
|
||||||
|
// Before | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
// 2022-05-06: VSCode changed the format in 1.68
|
||||||
|
// Before | ^/zip/c:/foo/bar.zip/package.json
|
||||||
|
// After | ^/zip//c:/foo/bar.zip/package.json
|
||||||
|
//
|
||||||
|
case `vscode <1.61`: {
|
||||||
|
str = `^zip:${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.66`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode <1.68`: {
|
||||||
|
str = `^/zip${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`: {
|
||||||
|
str = `^/zip/${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// To make "go to definition" work,
|
||||||
|
// We have to resolve the actual file system path from virtual path
|
||||||
|
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = resolve(`zipfile:${str}`);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
// Support neovim native LSP and [typescript-language-server](https://github.com/theia-ide/typescript-language-server)
|
||||||
|
// We have to resolve the actual file system path from virtual path,
|
||||||
|
// everything else is up to neovim
|
||||||
|
case `neovim`: {
|
||||||
|
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
|
||||||
|
str = `zipfile://${str}`;
|
||||||
|
} break;
|
||||||
|
|
||||||
|
default: {
|
||||||
|
str = `zip:${str}`;
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str = str.replace(/^\/?/, process.platform === `win32` ? `` : `/`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fromEditorPath(str) {
|
||||||
|
switch (hostInfo) {
|
||||||
|
case `coc-nvim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for coc-nvim is in format of /<pwd>/zipfile:/<pwd>/.yarn/...
|
||||||
|
// So in order to convert it back, we use .* to match all the thing
|
||||||
|
// before `zipfile:`
|
||||||
|
return process.platform === `win32`
|
||||||
|
? str.replace(/^.*zipfile:\//, ``)
|
||||||
|
: str.replace(/^.*zipfile:/, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `neovim`: {
|
||||||
|
str = str.replace(/\.zip::/, `.zip/`);
|
||||||
|
// The path for neovim is in format of zipfile:///<pwd>/.yarn/...
|
||||||
|
return str.replace(/^zipfile:\/\//, ``);
|
||||||
|
} break;
|
||||||
|
|
||||||
|
case `vscode`:
|
||||||
|
default: {
|
||||||
|
return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force enable 'allowLocalPluginLoads'
|
||||||
|
// TypeScript tries to resolve plugins using a path relative to itself
|
||||||
|
// which doesn't work when using the global cache
|
||||||
|
// https://github.com/microsoft/TypeScript/blob/1b57a0395e0bff191581c9606aab92832001de62/src/server/project.ts#L2238
|
||||||
|
// VSCode doesn't want to enable 'allowLocalPluginLoads' due to security concerns but
|
||||||
|
// TypeScript already does local loads and if this code is running the user trusts the workspace
|
||||||
|
// https://github.com/microsoft/vscode/issues/45856
|
||||||
|
const ConfiguredProject = tsserver.server.ConfiguredProject;
|
||||||
|
const {enablePluginsWithOptions: originalEnablePluginsWithOptions} = ConfiguredProject.prototype;
|
||||||
|
ConfiguredProject.prototype.enablePluginsWithOptions = function() {
|
||||||
|
this.projectService.allowLocalPluginLoads = true;
|
||||||
|
return originalEnablePluginsWithOptions.apply(this, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// And here is the point where we hijack the VSCode <-> TS communications
|
||||||
|
// by adding ourselves in the middle. We locate everything that looks
|
||||||
|
// like an absolute path of ours and normalize it.
|
||||||
|
|
||||||
|
const Session = tsserver.server.Session;
|
||||||
|
const {onMessage: originalOnMessage, send: originalSend} = Session.prototype;
|
||||||
|
let hostInfo = `unknown`;
|
||||||
|
|
||||||
|
Object.assign(Session.prototype, {
|
||||||
|
onMessage(/** @type {string | object} */ message) {
|
||||||
|
const isStringMessage = typeof message === 'string';
|
||||||
|
const parsedMessage = isStringMessage ? JSON.parse(message) : message;
|
||||||
|
|
||||||
|
if (
|
||||||
|
parsedMessage != null &&
|
||||||
|
typeof parsedMessage === `object` &&
|
||||||
|
parsedMessage.arguments &&
|
||||||
|
typeof parsedMessage.arguments.hostInfo === `string`
|
||||||
|
) {
|
||||||
|
hostInfo = parsedMessage.arguments.hostInfo;
|
||||||
|
if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
|
||||||
|
const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
|
||||||
|
// The RegExp from https://semver.org/ but without the caret at the start
|
||||||
|
/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
|
||||||
|
) ?? []).map(Number)
|
||||||
|
|
||||||
|
if (major === 1) {
|
||||||
|
if (minor < 61) {
|
||||||
|
hostInfo += ` <1.61`;
|
||||||
|
} else if (minor < 66) {
|
||||||
|
hostInfo += ` <1.66`;
|
||||||
|
} else if (minor < 68) {
|
||||||
|
hostInfo += ` <1.68`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
|
||||||
|
return typeof value === 'string' ? fromEditorPath(value) : value;
|
||||||
|
});
|
||||||
|
|
||||||
|
return originalOnMessage.call(
|
||||||
|
this,
|
||||||
|
isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
send(/** @type {any} */ msg) {
|
||||||
|
return originalSend.call(this, JSON.parse(JSON.stringify(msg, (key, value) => {
|
||||||
|
return typeof value === `string` ? toEditorPath(value) : value;
|
||||||
|
})));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return tsserver;
|
||||||
|
};
|
||||||
|
|
||||||
|
const [major, minor] = absRequire(`typescript/package.json`).version.split(`.`, 2).map(value => parseInt(value, 10));
|
||||||
|
// In TypeScript@>=5.5 the tsserver uses the public TypeScript API so that needs to be patched as well.
|
||||||
|
// Ref https://github.com/microsoft/TypeScript/pull/55326
|
||||||
|
if (major > 5 || (major === 5 && minor >= 5)) {
|
||||||
|
moduleWrapper(absRequire(`typescript`));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defer to the real typescript/lib/tsserverlibrary.js your application uses
|
||||||
|
module.exports = moduleWrapper(absRequire(`typescript/lib/tsserverlibrary.js`));
|
||||||
32
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
32
.yarn/sdks/typescript/lib/typescript.js
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const {existsSync} = require(`fs`);
|
||||||
|
const {createRequire, register} = require(`module`);
|
||||||
|
const {resolve} = require(`path`);
|
||||||
|
const {pathToFileURL} = require(`url`);
|
||||||
|
|
||||||
|
const relPnpApiPath = "../../../../.pnp.cjs";
|
||||||
|
|
||||||
|
const absPnpApiPath = resolve(__dirname, relPnpApiPath);
|
||||||
|
const absUserWrapperPath = resolve(__dirname, `./sdk.user.cjs`);
|
||||||
|
const absRequire = createRequire(absPnpApiPath);
|
||||||
|
|
||||||
|
const absPnpLoaderPath = resolve(absPnpApiPath, `../.pnp.loader.mjs`);
|
||||||
|
const isPnpLoaderEnabled = existsSync(absPnpLoaderPath);
|
||||||
|
|
||||||
|
if (existsSync(absPnpApiPath)) {
|
||||||
|
if (!process.versions.pnp) {
|
||||||
|
// Setup the environment to be able to require typescript
|
||||||
|
require(absPnpApiPath).setup();
|
||||||
|
if (isPnpLoaderEnabled && register) {
|
||||||
|
register(pathToFileURL(absPnpLoaderPath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wrapWithUserWrapper = existsSync(absUserWrapperPath)
|
||||||
|
? exports => absRequire(absUserWrapperPath)(exports)
|
||||||
|
: exports => exports;
|
||||||
|
|
||||||
|
// Defer to the real typescript your application uses
|
||||||
|
module.exports = wrapWithUserWrapper(absRequire(`typescript`));
|
||||||
10
.yarn/sdks/typescript/package.json
vendored
Normal file
10
.yarn/sdks/typescript/package.json
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "typescript",
|
||||||
|
"version": "5.9.3-sdk",
|
||||||
|
"main": "./lib/typescript.js",
|
||||||
|
"type": "commonjs",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "./bin/tsc",
|
||||||
|
"tsserver": "./bin/tsserver"
|
||||||
|
}
|
||||||
|
}
|
||||||
12
.yarnrc.yml
Normal file
12
.yarnrc.yml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
enableGlobalCache: false
|
||||||
|
|
||||||
|
nodeLinker: pnp
|
||||||
|
|
||||||
|
npmScopes:
|
||||||
|
jig-software:
|
||||||
|
npmAlwaysAuth: false
|
||||||
|
npmAuthToken: "${JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN:-NONE}"
|
||||||
|
npmPublishRegistry: "https://gitea.jig.com.hk/api/packages/jig-software/npm/"
|
||||||
|
npmRegistryServer: "https://gitea.jig.com.hk/api/packages/jig-software/npm/"
|
||||||
|
|
||||||
|
yarnPath: .yarn/releases/yarn-4.12.0.cjs
|
||||||
24
Dockerfile
Normal file
24
Dockerfile
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY .yarnrc.yml package.json yarn.lock ./
|
||||||
|
COPY .yarn/releases ./.yarn/releases
|
||||||
|
RUN --mount=type=secret,id=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN,env=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN yarn
|
||||||
|
COPY tsconfig.json tsconfig.build.json ./
|
||||||
|
COPY src ./src
|
||||||
|
RUN yarn build
|
||||||
|
RUN yarn workspaces focus --all --production
|
||||||
|
|
||||||
|
FROM node:22-alpine
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
COPY .yarnrc.yml package.json yarn.lock ./
|
||||||
|
COPY --from=build /app/dist ./dist
|
||||||
|
COPY --from=build /app/.yarn ./.yarn
|
||||||
|
COPY --from=build /app/.pnp.* ./
|
||||||
|
|
||||||
|
CMD ["yarn", "start"]
|
||||||
|
|
||||||
|
# Uncomment if healthcheck is needed
|
||||||
|
# HEALTHCHECK --interval=1m --timeout=10s --start-period=10s --retries=3 \
|
||||||
|
# CMD [ "yarn", "health-check"]
|
||||||
BIN
asset/fast_logo.jpg
Normal file
BIN
asset/fast_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
BIN
asset/jec_logo.jpg
Normal file
BIN
asset/jec_logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
10
config.example.json
Normal file
10
config.example.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"port": 3000,
|
||||||
|
"apiPrefix": "/api",
|
||||||
|
"databaseURL": "mongodb://mongo:27017/?replicaSet=rs0",
|
||||||
|
"databaseName": "example",
|
||||||
|
"logDirectory": "volumes/logs",
|
||||||
|
"printLogLevel": "verbose",
|
||||||
|
"persistLogLevel": "info",
|
||||||
|
"defaultSettings": {}
|
||||||
|
}
|
||||||
BIN
event_report.xlsx
Normal file
BIN
event_report.xlsx
Normal file
Binary file not shown.
83
init.sh
Executable file
83
init.sh
Executable file
@ -0,0 +1,83 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Workaround to nullify yarn 2+ auto git init with master branch
|
||||||
|
git init --initial-branch=main
|
||||||
|
yarn init --yes
|
||||||
|
|
||||||
|
yarn set version berry --yarn-path
|
||||||
|
|
||||||
|
yarn add \
|
||||||
|
@jig-software/sdk \
|
||||||
|
@jig-software/trest-core \
|
||||||
|
@jig-software/trest-client \
|
||||||
|
@jig-software/zod-bson \
|
||||||
|
@jig-software/logger \
|
||||||
|
@jig-software/logger-winston \
|
||||||
|
debug \
|
||||||
|
bson \
|
||||||
|
koa \
|
||||||
|
koa-body \
|
||||||
|
@koa/router \
|
||||||
|
zod \
|
||||||
|
mongodb \
|
||||||
|
eventemitter3 \
|
||||||
|
reflect-metadata
|
||||||
|
|
||||||
|
yarn add --dev \
|
||||||
|
@types/koa \
|
||||||
|
@types/koa__router \
|
||||||
|
@types/debug \
|
||||||
|
@types/node \
|
||||||
|
commitizen cz-emoji \
|
||||||
|
ts-node-dev \
|
||||||
|
@typescript-eslint/eslint-plugin \
|
||||||
|
@typescript-eslint/parser \
|
||||||
|
eslint@8 \
|
||||||
|
eslint-import-resolver-typescript \
|
||||||
|
eslint-plugin-import \
|
||||||
|
eslint-config-prettier \
|
||||||
|
typescript \
|
||||||
|
prettier \
|
||||||
|
mocha \
|
||||||
|
@types/mocha \
|
||||||
|
chai@^4.3.10 \
|
||||||
|
@types/chai@^4.3.11 \
|
||||||
|
nyc \
|
||||||
|
ts-node \
|
||||||
|
@types/node \
|
||||||
|
rimraf
|
||||||
|
|
||||||
|
yarn dlx @yarnpkg/sdks vscode
|
||||||
|
|
||||||
|
# Format by prettier
|
||||||
|
yarn prettier . --write
|
||||||
|
|
||||||
|
# Set npm environment to skip confirmation prompt
|
||||||
|
orig_npm_config_yes=$npm_config_yes
|
||||||
|
export npm_config_yes=yes
|
||||||
|
|
||||||
|
# Add lint script and package config to the project
|
||||||
|
./patch-package-json.sh
|
||||||
|
|
||||||
|
# Enable .gitea/workflows
|
||||||
|
mv .gitea/workflows.template .gitea/workflows
|
||||||
|
|
||||||
|
|
||||||
|
# Restore the original env
|
||||||
|
export npm_config_yes=$orig_npm_config_yes
|
||||||
|
|
||||||
|
for (( ; ; )); do
|
||||||
|
read -p "The initialization process is done. Do you want to remove this script file now? (Y/n): " remove
|
||||||
|
remove=${remove:-y}
|
||||||
|
echo $remove
|
||||||
|
if [ "$remove" = "y" ] || [ "$remove" = "Y" ]; then
|
||||||
|
rm ./package.patch.json
|
||||||
|
rm ./patch-package-json.sh
|
||||||
|
rm -- "$0"
|
||||||
|
break
|
||||||
|
else
|
||||||
|
if [ "$remove" = "n" ] || [ "$remove" = "N" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
68
package.json
Normal file
68
package.json
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
{
|
||||||
|
"name": "report-generation",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn clean && tsc -b tsconfig.build.json",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"commit": "cz",
|
||||||
|
"dev": "ts-node-dev --respawn --project tsconfig.json --watch ./volumes/config.json ./src/bootstrap.ts",
|
||||||
|
"health-check": "node ./dist/healthCheck.js",
|
||||||
|
"lint": "eslint '{src,test}/**/*.ts'",
|
||||||
|
"lint:src": "eslint 'src/**/*.ts'",
|
||||||
|
"lint:test": "eslint 'test/**/*.ts'",
|
||||||
|
"start": "node ./dist/bootstrap.js",
|
||||||
|
"test": "nyc mocha",
|
||||||
|
"test:coverage": "nyc --reporter cobertura mocha"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "cz-emoji"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@jig-software/logger": "^1.1.0",
|
||||||
|
"@jig-software/logger-winston": "^1.0.0",
|
||||||
|
"@jig-software/sdk": "^7.0.0",
|
||||||
|
"@jig-software/trest-client": "^3.0.0",
|
||||||
|
"@jig-software/trest-core": "^3.0.0",
|
||||||
|
"@jig-software/zod-bson": "^2.0.0",
|
||||||
|
"@koa/router": "^15.2.0",
|
||||||
|
"bson": "^7.0.0",
|
||||||
|
"debug": "^4.4.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"exceljs": "^4.4.0",
|
||||||
|
"koa": "^3.1.1",
|
||||||
|
"koa-body": "^7.0.1",
|
||||||
|
"mongodb": "^7.0.0",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"zod": "^4.3.5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/chai": "^4.3.11",
|
||||||
|
"@types/debug": "^4.1.12",
|
||||||
|
"@types/koa": "^3.0.1",
|
||||||
|
"@types/koa__router": "^12.0.5",
|
||||||
|
"@types/mocha": "^10.0.10",
|
||||||
|
"@types/node": "^25.0.3",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.52.0",
|
||||||
|
"@typescript-eslint/parser": "^8.52.0",
|
||||||
|
"chai": "^4.3.10",
|
||||||
|
"commitizen": "^4.3.1",
|
||||||
|
"cz-emoji": "^1.3.2-canary.2",
|
||||||
|
"eslint": "8",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-import-resolver-typescript": "^4.4.4",
|
||||||
|
"eslint-plugin-import": "^2.32.0",
|
||||||
|
"mocha": "^11.7.5",
|
||||||
|
"nyc": "^17.1.0",
|
||||||
|
"prettier": "^3.7.4",
|
||||||
|
"rimraf": "^6.1.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"ts-node-dev": "^2.0.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@4.12.0"
|
||||||
|
}
|
||||||
22
package.patch.json
Normal file
22
package.patch.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"files": ["dist"],
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"config": {
|
||||||
|
"commitizen": {
|
||||||
|
"path": "cz-emoji"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "yarn clean && tsc -b tsconfig.build.json",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"commit": "cz",
|
||||||
|
"dev": "ts-node-dev --respawn --project tsconfig.json --watch ./volumes/config.json ./src/bootstrap.ts",
|
||||||
|
"lint": "eslint '{src,test}/**/*.ts'",
|
||||||
|
"lint:src": "eslint 'src/**/*.ts'",
|
||||||
|
"lint:test": "eslint 'test/**/*.ts'",
|
||||||
|
"start": "node ./dist/bootstrap.js",
|
||||||
|
"test": "nyc mocha",
|
||||||
|
"test:coverage": "nyc --reporter cobertura mocha",
|
||||||
|
"health-check": "node ./dist/healthCheck.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
patch-package-json.sh
Executable file
6
patch-package-json.sh
Executable file
@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Workaround for yarn workspace
|
||||||
|
cat ./package.json ./package.patch.json | npx json --deep-merge | tee ./package.json.temp
|
||||||
|
mv ./package.json.temp ./package.json
|
||||||
|
npx sort-package-json
|
||||||
4
scripts/build_image.sh
Executable file
4
scripts/build_image.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
NAME=$(cat ./package.json | npx json name)
|
||||||
|
echo "Build image: $NAME"
|
||||||
|
docker build . -t $NAME --secret id=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN,env=JIG_SOFTWARE_GITEA_DEPLOYMENT_TOKEN
|
||||||
12
src/Main.ts
Normal file
12
src/Main.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Inject, KOA_APP, KoaApp, LC, Lifecycle } from '@jig-software/sdk';
|
||||||
|
import { ActiveAppSettings } from './providers';
|
||||||
|
|
||||||
|
export class Main extends Lifecycle {
|
||||||
|
@LC.bind
|
||||||
|
@Inject()
|
||||||
|
activeAppSettings!: ActiveAppSettings;
|
||||||
|
|
||||||
|
@LC.bind
|
||||||
|
@Inject(KOA_APP())
|
||||||
|
koaApp!: KoaApp;
|
||||||
|
}
|
||||||
37
src/bootstrap.ts
Normal file
37
src/bootstrap.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { WinstonLogger } from '@jig-software/logger-winston';
|
||||||
|
import {
|
||||||
|
ConfigDI,
|
||||||
|
MongoDI,
|
||||||
|
KoaDI,
|
||||||
|
container,
|
||||||
|
LoggerDI,
|
||||||
|
} from '@jig-software/sdk';
|
||||||
|
import { Main } from './Main';
|
||||||
|
import {
|
||||||
|
AppConfig,
|
||||||
|
KoaAppOptionsFactory,
|
||||||
|
MongoDIOptionsFactory,
|
||||||
|
WinstonLoggerParamsFactory,
|
||||||
|
} from './config';
|
||||||
|
import { HealthCheckController, ReadingReportController } from './controllers';
|
||||||
|
import { ActiveAppSettings } from './providers';
|
||||||
|
import { AppSettingsRepository } from './repositories';
|
||||||
|
async function bootstrap() {
|
||||||
|
container.use(
|
||||||
|
ConfigDI.config(AppConfig, { configPath: './volumes/config.json' }),
|
||||||
|
LoggerDI.logger(WinstonLogger, {
|
||||||
|
paramsFactory: WinstonLoggerParamsFactory,
|
||||||
|
}),
|
||||||
|
MongoDI.root({ optionsFactory: MongoDIOptionsFactory }),
|
||||||
|
KoaDI.root({ optionsFactory: KoaAppOptionsFactory }),
|
||||||
|
MongoDI.repository('appSettings', AppSettingsRepository),
|
||||||
|
ActiveAppSettings,
|
||||||
|
);
|
||||||
|
await container.create(KoaDI.controller(HealthCheckController));
|
||||||
|
await container.create(KoaDI.controller(ReadingReportController));
|
||||||
|
const main = await container.create(Main);
|
||||||
|
await main.start();
|
||||||
|
process.on('unhandledRejection', console.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bootstrap().catch(console.error);
|
||||||
74
src/config.ts
Normal file
74
src/config.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { WinstonLoggerOptions } from '@jig-software/logger-winston';
|
||||||
|
import {
|
||||||
|
Config,
|
||||||
|
Inject,
|
||||||
|
KoaAppOptions,
|
||||||
|
MongoDIOptions,
|
||||||
|
} from '@jig-software/sdk';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { AppSettingsInit } from './shared';
|
||||||
|
|
||||||
|
// Note: AppConfig is for Static props.
|
||||||
|
|
||||||
|
const LogLevel = z.enum([
|
||||||
|
'error',
|
||||||
|
'warn',
|
||||||
|
'info',
|
||||||
|
'http',
|
||||||
|
'verbose',
|
||||||
|
'debug',
|
||||||
|
'silly',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const _AppConfig = z.object({
|
||||||
|
port: z.number().optional(),
|
||||||
|
apiPrefix: z.string().optional(),
|
||||||
|
databaseURL: z.string(),
|
||||||
|
databaseName: z.string(),
|
||||||
|
logDirectory: z.string().optional(),
|
||||||
|
printLogLevel: LogLevel.optional(),
|
||||||
|
persistLogLevel: LogLevel.optional(),
|
||||||
|
defaultSettings: AppSettingsInit,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AppConfig = Config.extend('AppConfig', _AppConfig);
|
||||||
|
export type AppConfig = Config.infer<typeof AppConfig>;
|
||||||
|
|
||||||
|
export class KoaAppOptionsFactory {
|
||||||
|
@Inject()
|
||||||
|
private appConfig!: AppConfig;
|
||||||
|
create(): KoaAppOptions {
|
||||||
|
return {
|
||||||
|
port: this.appConfig.port ?? 3000,
|
||||||
|
prefix: this.appConfig.apiPrefix,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MongoDIOptionsFactory {
|
||||||
|
@Inject()
|
||||||
|
private appConfig!: AppConfig;
|
||||||
|
|
||||||
|
create(): MongoDIOptions {
|
||||||
|
return {
|
||||||
|
uri: this.appConfig.databaseURL,
|
||||||
|
database: this.appConfig.databaseName,
|
||||||
|
clientOptions: { ignoreUndefined: true },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WinstonLoggerParamsFactory {
|
||||||
|
@Inject()
|
||||||
|
private appConfig!: AppConfig;
|
||||||
|
|
||||||
|
create(): [WinstonLoggerOptions] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
logDirname: this.appConfig.logDirectory,
|
||||||
|
printLogLevel: this.appConfig.printLogLevel,
|
||||||
|
persistLogLevel: this.appConfig.persistLogLevel,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
src/controllers/HealthCheckController.ts
Normal file
22
src/controllers/HealthCheckController.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { Logger } from '@jig-software/logger';
|
||||||
|
import { Controller, Inject } from '@jig-software/sdk';
|
||||||
|
import { healthCheckContract } from '../shared/contracts';
|
||||||
|
|
||||||
|
export class HealthCheckController extends Controller {
|
||||||
|
@Inject()
|
||||||
|
protected logger!: Logger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.logger.setTag(this.constructor.name);
|
||||||
|
this.implement(healthCheckContract, (r) =>
|
||||||
|
r.handlers({
|
||||||
|
checkHealth: {
|
||||||
|
handler: () => {
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
184
src/controllers/ReadingReportController.ts
Normal file
184
src/controllers/ReadingReportController.ts
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
import { Logger } from '@jig-software/logger';
|
||||||
|
import { Controller, Inject } from '@jig-software/sdk';
|
||||||
|
import ExcelJS from 'exceljs';
|
||||||
|
import path from 'path';
|
||||||
|
import { readingReportContract } from '../shared';
|
||||||
|
import { createFileOutput } from '@jig-software/trest-core';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
interface ReadingStats {
|
||||||
|
min?: string;
|
||||||
|
max?: string;
|
||||||
|
avg?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DeviceData {
|
||||||
|
building: string;
|
||||||
|
floor: string;
|
||||||
|
device_name: string;
|
||||||
|
min_max_avg: Record<string, ReadingStats>;
|
||||||
|
readings: Record<string, Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReportParams {
|
||||||
|
start_date?: string;
|
||||||
|
end_date?: string;
|
||||||
|
time_step?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ReadingReportController extends Controller {
|
||||||
|
@Inject()
|
||||||
|
protected logger!: Logger;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.logger.setTag(this.constructor.name);
|
||||||
|
this.implement(readingReportContract, (r) =>
|
||||||
|
r.handlers({
|
||||||
|
generateReadingsReport: {
|
||||||
|
handler: async (FullReportSchema) => {
|
||||||
|
const data: DeviceData[] = FullReportSchema.data;
|
||||||
|
const reportParams: ReportParams = FullReportSchema.report_params;
|
||||||
|
const workbook = new ExcelJS.Workbook();
|
||||||
|
const worksheet = workbook.addWorksheet('Event Records');
|
||||||
|
|
||||||
|
// ---------------- Logos ----------------
|
||||||
|
const jecLogoId = workbook.addImage({
|
||||||
|
filename: path.resolve('asset/jec_logo.jpg'),
|
||||||
|
extension: 'jpeg',
|
||||||
|
});
|
||||||
|
|
||||||
|
const fastLogoId = workbook.addImage({
|
||||||
|
filename: path.resolve('asset/fast_logo.jpg'),
|
||||||
|
extension: 'jpeg',
|
||||||
|
});
|
||||||
|
|
||||||
|
worksheet.addImage(jecLogoId, {
|
||||||
|
tl: { col: 0, row: 0 },
|
||||||
|
ext: { width: 120, height: 60 },
|
||||||
|
});
|
||||||
|
worksheet.addImage(fastLogoId, {
|
||||||
|
tl: { col: 7, row: 0 },
|
||||||
|
ext: { width: 120, height: 60 },
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------- Title ----------------
|
||||||
|
worksheet.mergeCells('B1:G2');
|
||||||
|
const titleCell = worksheet.getCell('B1');
|
||||||
|
titleCell.value = 'Event Report';
|
||||||
|
titleCell.alignment = { horizontal: 'center', vertical: 'middle' };
|
||||||
|
titleCell.font = { bold: true, size: 18 };
|
||||||
|
|
||||||
|
// ---------------- Fixed params ----------------
|
||||||
|
const fixedParams = [
|
||||||
|
['Start Date:', reportParams.start_date ?? ''],
|
||||||
|
['End Date:', reportParams.end_date ?? ''],
|
||||||
|
['Time Step:', reportParams.time_step ?? ''],
|
||||||
|
];
|
||||||
|
|
||||||
|
fixedParams.forEach(([label, value], idx) => {
|
||||||
|
worksheet.getCell(4 + idx, 2).value = label;
|
||||||
|
worksheet.getCell(4 + idx, 3).value = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------- Headers ----------------
|
||||||
|
const allDates = Array.from(
|
||||||
|
new Set(
|
||||||
|
data.flatMap((device) =>
|
||||||
|
Object.values(device.readings).flatMap((r) => Object.keys(r)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).sort();
|
||||||
|
|
||||||
|
const headers = [
|
||||||
|
'Building',
|
||||||
|
'Floor',
|
||||||
|
'Device Name',
|
||||||
|
'Reading Type',
|
||||||
|
'Minimum',
|
||||||
|
'Maximum',
|
||||||
|
'Average',
|
||||||
|
...allDates,
|
||||||
|
];
|
||||||
|
|
||||||
|
const headerRowIndex = 7;
|
||||||
|
const headerRow = worksheet.getRow(headerRowIndex);
|
||||||
|
headerRow.values = headers;
|
||||||
|
headerRow.eachCell((cell) => {
|
||||||
|
cell.font = { bold: true };
|
||||||
|
cell.fill = {
|
||||||
|
type: 'pattern',
|
||||||
|
pattern: 'solid',
|
||||||
|
fgColor: { argb: 'FFD9D9D9' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------- Data ----------------
|
||||||
|
let currentRow = headerRowIndex + 1;
|
||||||
|
|
||||||
|
for (const device of data) {
|
||||||
|
const types = Object.keys(device.readings);
|
||||||
|
const startRow = currentRow;
|
||||||
|
const endRow = currentRow + types.length - 1;
|
||||||
|
|
||||||
|
if (types.length > 1) {
|
||||||
|
worksheet.mergeCells(startRow, 1, endRow, 1);
|
||||||
|
worksheet.mergeCells(startRow, 2, endRow, 2);
|
||||||
|
worksheet.mergeCells(startRow, 3, endRow, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
worksheet.getCell(startRow, 1).value = device.building;
|
||||||
|
worksheet.getCell(startRow, 2).value = device.floor;
|
||||||
|
worksheet.getCell(startRow, 3).value = device.device_name;
|
||||||
|
|
||||||
|
for (const type of types) {
|
||||||
|
const row = worksheet.getRow(currentRow);
|
||||||
|
|
||||||
|
row.getCell(4).value = type;
|
||||||
|
|
||||||
|
const stats = device.min_max_avg?.[type] ?? {};
|
||||||
|
row.getCell(5).value = stats.min ?? '-';
|
||||||
|
row.getCell(6).value = stats.max ?? '-';
|
||||||
|
row.getCell(7).value = stats.avg ?? '-';
|
||||||
|
|
||||||
|
allDates.forEach((date, idx) => {
|
||||||
|
row.getCell(8 + idx).value =
|
||||||
|
device.readings[type]?.[date] ?? '-';
|
||||||
|
});
|
||||||
|
|
||||||
|
currentRow++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Processed device '${device.device_name}' rows ${startRow}–${endRow}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------- Auto-width ----------------
|
||||||
|
worksheet.columns.forEach((col) => {
|
||||||
|
let max = 10;
|
||||||
|
col.eachCell!({ includeEmpty: true }, (cell) => {
|
||||||
|
const len = String(cell.value ?? '').length;
|
||||||
|
max = Math.max(max, len);
|
||||||
|
});
|
||||||
|
col.width = max + 2;
|
||||||
|
});
|
||||||
|
|
||||||
|
worksheet.autoFilter = {
|
||||||
|
from: { row: headerRowIndex, column: 1 },
|
||||||
|
to: { row: currentRow - 1, column: headers.length },
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------- Save file ----------------
|
||||||
|
const outputPath = path.resolve('event_report.xlsx');
|
||||||
|
await workbook.xlsx.writeFile(outputPath);
|
||||||
|
console.log(`Excel report generated: ${outputPath}`);
|
||||||
|
const readStream = fs.createReadStream(outputPath);
|
||||||
|
|
||||||
|
return createFileOutput(readStream, 'event_report.xlsx');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
2
src/controllers/index.ts
Normal file
2
src/controllers/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './HealthCheckController';
|
||||||
|
export * from './ReadingReportController';
|
||||||
24
src/healthCheck.ts
Normal file
24
src/healthCheck.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { ConfigDI, container } from '@jig-software/sdk';
|
||||||
|
import { TRestClient } from '@jig-software/trest-client';
|
||||||
|
import { AppConfig } from './config';
|
||||||
|
import { healthCheckContract } from './shared';
|
||||||
|
|
||||||
|
async function healthCheck() {
|
||||||
|
const appConfig = await container.create(
|
||||||
|
ConfigDI.config(AppConfig, {
|
||||||
|
configPath: './volumes/config.json',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const { port, apiPrefix } = appConfig;
|
||||||
|
const baseUrl = apiPrefix
|
||||||
|
? `http://localhost:${port}/${apiPrefix.replace(/^\//, '')}`
|
||||||
|
: `http://localhost:${port}/`;
|
||||||
|
const healthCheckClient = new TRestClient(healthCheckContract, baseUrl);
|
||||||
|
await healthCheckClient.actions.checkHealth().unwrap();
|
||||||
|
console.info('OK');
|
||||||
|
}
|
||||||
|
|
||||||
|
healthCheck().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
3
src/middlewares/index.ts
Normal file
3
src/middlewares/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
// * Export middlewares here
|
||||||
|
// * export * from './someMiddleware'
|
||||||
|
export {};
|
||||||
170
src/providers/ActiveAppSettings.ts
Normal file
170
src/providers/ActiveAppSettings.ts
Normal file
@ -0,0 +1,170 @@
|
|||||||
|
import { Logger } from '@jig-software/logger';
|
||||||
|
import { Inject, Lifecycle, MongoOrmValidationError } from '@jig-software/sdk';
|
||||||
|
import { EventEmitter } from 'eventemitter3';
|
||||||
|
import { ChangeStream } from 'mongodb';
|
||||||
|
import { AppConfig } from '../config';
|
||||||
|
import { AppSettingsRepository } from '../repositories';
|
||||||
|
import { AppSettings, AppSettingsInit } from '../shared';
|
||||||
|
|
||||||
|
export type AppSettingsChangeEmitter = EventEmitter<{
|
||||||
|
change: (appSettings: AppSettingsInit) => void;
|
||||||
|
error: (err: unknown) => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export class ActiveAppSettings extends Lifecycle {
|
||||||
|
@Inject()
|
||||||
|
protected logger!: Logger;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private appConfig!: AppConfig;
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
private appSettingsRepo!: AppSettingsRepository;
|
||||||
|
|
||||||
|
private _changeStream: ChangeStream | null = null;
|
||||||
|
|
||||||
|
private _appSettings: AppSettings | null = null;
|
||||||
|
|
||||||
|
private _changeEmitter: AppSettingsChangeEmitter = new EventEmitter();
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.logger.setTag(this.constructor.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
get changeEmitter() {
|
||||||
|
return this._changeEmitter;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(): Readonly<AppSettingsInit> {
|
||||||
|
return (
|
||||||
|
(this._appSettings as unknown as AppSettingsInit) ??
|
||||||
|
this.appConfig.defaultSettings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Modify appSettings in-place.
|
||||||
|
* Will persist modification. If fails will rollback to existing one.
|
||||||
|
*/
|
||||||
|
async set(modify: (appSettings: AppSettingsInit) => void) {
|
||||||
|
if (!this._appSettings) {
|
||||||
|
throw new Error('AppSettings is not ready.'); // This should not happens.
|
||||||
|
}
|
||||||
|
const _id = this._appSettings._id;
|
||||||
|
modify(this._appSettings as unknown as AppSettingsInit);
|
||||||
|
try {
|
||||||
|
await this.appSettingsRepo.persist(this._appSettings);
|
||||||
|
} catch (err) {
|
||||||
|
this._appSettings = await this.appSettingsRepo.get(_id);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async onStart() {
|
||||||
|
await this.ensureAppSettings();
|
||||||
|
this.setUpChangeStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
onStop() {
|
||||||
|
this._appSettings = null;
|
||||||
|
this.cleanUpChangeStream();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureAppSettings() {
|
||||||
|
const count = await this.appSettingsRepo.count({});
|
||||||
|
if (count === 0) {
|
||||||
|
this.logger.info('AppSettings is not found. Create one with default.');
|
||||||
|
this._appSettings = await this.appSettingsRepo.create(
|
||||||
|
this.appConfig.defaultSettings,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (count > 1)
|
||||||
|
this.logger.warn(
|
||||||
|
`Found ${count} AppSettings. Only the latest will be used.`,
|
||||||
|
);
|
||||||
|
else this.logger.info('Found AppSettings.');
|
||||||
|
this._appSettings = await this.appSettingsRepo.findOne(
|
||||||
|
{},
|
||||||
|
{ sort: { updatedAt: -1 } },
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
this.appSettingsRepo.validatePersisted(this._appSettings);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
'Invalid AppSettings. AppSettings may not be properly migrated!',
|
||||||
|
);
|
||||||
|
if (err instanceof MongoOrmValidationError)
|
||||||
|
this.logger.error(err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._changeEmitter.emit(
|
||||||
|
'change',
|
||||||
|
this._appSettings! as unknown as AppSettingsInit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private setUpChangeStream() {
|
||||||
|
if (this._changeStream) {
|
||||||
|
this.logger.warn(`ChangeStream already setUp. Skip.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._changeStream = this.appSettingsRepo
|
||||||
|
.watch(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
operationType: { $in: ['replace', 'update', 'delete'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
{ fullDocument: 'updateLookup' },
|
||||||
|
)
|
||||||
|
.on('change', (change) => {
|
||||||
|
switch (change.operationType) {
|
||||||
|
case 'update':
|
||||||
|
case 'replace': {
|
||||||
|
const appSettings = change.fullDocument!;
|
||||||
|
this.logger.debug(`Detect ${change.operationType}.`);
|
||||||
|
if (this._appSettings?._id.equals(appSettings._id)) {
|
||||||
|
this.logger.info('Current AppSettings is updated.');
|
||||||
|
this._appSettings = appSettings;
|
||||||
|
this._changeEmitter.emit(
|
||||||
|
'change',
|
||||||
|
this._appSettings! as unknown as AppSettingsInit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const { _id: deletedId } = change.documentKey;
|
||||||
|
this.logger.debug(`Detect delete (${deletedId}).`);
|
||||||
|
if (this._appSettings?._id.equals(deletedId)) {
|
||||||
|
this.logger.info('Current AppSettings is deleted. Reinitialize.');
|
||||||
|
this._appSettings = null;
|
||||||
|
this.ensureAppSettings().catch((err) => {
|
||||||
|
this.logger.error('Fail to reinitialize.');
|
||||||
|
this.logger.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.once('error', (err) => {
|
||||||
|
this.logger.error(`Error in ChangeStream. Restart.`);
|
||||||
|
this.logger.error(err.message);
|
||||||
|
this.launch(() => {
|
||||||
|
throw err;
|
||||||
|
}).catch(() => {});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private cleanUpChangeStream() {
|
||||||
|
this._changeStream?.close().catch((err) => {
|
||||||
|
this.logger.error('Fail to close ChangeStream.');
|
||||||
|
this.logger.error(err);
|
||||||
|
});
|
||||||
|
this._changeStream = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/providers/index.ts
Normal file
1
src/providers/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './ActiveAppSettings';
|
||||||
16
src/repositories/AppSettingsRepository.ts
Normal file
16
src/repositories/AppSettingsRepository.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Repository } from '@jig-software/sdk';
|
||||||
|
import { AppSettings } from '../shared';
|
||||||
|
|
||||||
|
export const AppSettingsRepository = Repository.extend(
|
||||||
|
'AppSettingsRepository',
|
||||||
|
{
|
||||||
|
schema: AppSettings,
|
||||||
|
autoTimestamps: {
|
||||||
|
createTimestampKey: 'createdAt',
|
||||||
|
updateTimestampKey: 'updatedAt',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
export type AppSettingsRepository = Repository.infer<
|
||||||
|
typeof AppSettingsRepository
|
||||||
|
>;
|
||||||
1
src/repositories/index.ts
Normal file
1
src/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './AppSettingsRepository';
|
||||||
5
src/shared/constants/index.ts
Normal file
5
src/shared/constants/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// * Export constants here
|
||||||
|
// * export * from './someConstant'
|
||||||
|
// * export const SOME_OTHER_CONSTANT = 'constant value';
|
||||||
|
export {};
|
||||||
|
export * from './reading';
|
||||||
53
src/shared/constants/reading.ts
Normal file
53
src/shared/constants/reading.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
export const FullReportInput = z.object({
|
||||||
|
data: z.array(
|
||||||
|
z.object({
|
||||||
|
building: z.string(),
|
||||||
|
floor: z.string(),
|
||||||
|
device_name: z.string(),
|
||||||
|
min_max_avg: z.object({
|
||||||
|
Temperature: z.object({
|
||||||
|
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||||
|
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||||
|
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||||
|
}),
|
||||||
|
Humidity: z.object({
|
||||||
|
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||||
|
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||||
|
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||||
|
}),
|
||||||
|
Battery: z.object({
|
||||||
|
min: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid min value'),
|
||||||
|
max: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid max value'),
|
||||||
|
avg: z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid avg value'),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
readings: z.object({
|
||||||
|
Temperature: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||||
|
),
|
||||||
|
Humidity: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||||
|
),
|
||||||
|
Battery: z.record(
|
||||||
|
z.string(),
|
||||||
|
z.string().regex(/^\d+(\.\d+)?[°%]$/, 'Invalid reading value'),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
report_params: z.object({
|
||||||
|
start_date: z.string().refine((val) => !isNaN(Date.parse(val)), {
|
||||||
|
message: 'Invalid start_date',
|
||||||
|
}),
|
||||||
|
end_date: z.string().refine((val) => !isNaN(Date.parse(val)), {
|
||||||
|
message: 'Invalid end_date',
|
||||||
|
}),
|
||||||
|
time_step: z.enum(['daily', 'weekly', 'monthly']),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type FullReportInput = z.infer<typeof FullReportInput>;
|
||||||
10
src/shared/contracts/healthCheckContract.ts
Normal file
10
src/shared/contracts/healthCheckContract.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { buildContract } from '@jig-software/trest-core';
|
||||||
|
|
||||||
|
export const healthCheckContract = buildContract((c) =>
|
||||||
|
c
|
||||||
|
.prefix('/health-check')
|
||||||
|
.parse((p) => p.error.text(400))
|
||||||
|
.endpoints({
|
||||||
|
checkHealth: (e) => e.method('GET').parse((p) => p.success.null(200)),
|
||||||
|
}),
|
||||||
|
);
|
||||||
2
src/shared/contracts/index.ts
Normal file
2
src/shared/contracts/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './healthCheckContract';
|
||||||
|
export * from './readingReportContract';
|
||||||
16
src/shared/contracts/readingReportContract.ts
Normal file
16
src/shared/contracts/readingReportContract.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { A, buildContract } from '@jig-software/trest-core';
|
||||||
|
import { FullReportInput } from '../constants';
|
||||||
|
|
||||||
|
export const readingReportContract = buildContract((c) =>
|
||||||
|
c
|
||||||
|
.prefix('/reports')
|
||||||
|
.parse((p) => p.error.text(400).error.text(401))
|
||||||
|
.endpoints({
|
||||||
|
generateReadingsReport: (e) =>
|
||||||
|
e
|
||||||
|
.method('POST')
|
||||||
|
.path('/reading-report')
|
||||||
|
.prepare((p) => p.body.json(FullReportInput).annotate<[query: A]>())
|
||||||
|
.parse((p) => p.success.file(200)),
|
||||||
|
}),
|
||||||
|
);
|
||||||
3
src/shared/index.ts
Normal file
3
src/shared/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export * from './constants';
|
||||||
|
export * from './contracts';
|
||||||
|
export * from './schemas';
|
||||||
21
src/shared/schemas/AppSettings.ts
Normal file
21
src/shared/schemas/AppSettings.ts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { objectId } from '@jig-software/zod-bson';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Note: AppSettings is for Dynamic props.
|
||||||
|
|
||||||
|
export const AppSettings = z.object({
|
||||||
|
_id: objectId(),
|
||||||
|
// Include other properties here.
|
||||||
|
createdAt: z.date(),
|
||||||
|
updatedAt: z.date(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppSettings = z.infer<typeof AppSettings>;
|
||||||
|
|
||||||
|
export const AppSettingsInit = AppSettings.omit({
|
||||||
|
_id: true,
|
||||||
|
createdAt: true,
|
||||||
|
updatedAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type AppSettingsInit = z.infer<typeof AppSettingsInit>;
|
||||||
2
src/shared/schemas/index.ts
Normal file
2
src/shared/schemas/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './AppSettings';
|
||||||
|
export * from './readingReport';
|
||||||
24
src/shared/schemas/readingReport.ts
Normal file
24
src/shared/schemas/readingReport.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import z from 'zod';
|
||||||
|
import { objectId } from '@jig-software/zod-bson';
|
||||||
|
|
||||||
|
export const ReadingReport = z.object({
|
||||||
|
reportId: objectId(), // ✅ call the function
|
||||||
|
generatedAt: z
|
||||||
|
.string()
|
||||||
|
.refine((date) => !isNaN(Date.parse(date)), {
|
||||||
|
message: 'Invalid date format',
|
||||||
|
}),
|
||||||
|
generatedBy: z.string(),
|
||||||
|
readings: z.array(
|
||||||
|
z.object({
|
||||||
|
sensorId: objectId(), // ✅ call the function here too
|
||||||
|
timestamp: z
|
||||||
|
.string()
|
||||||
|
.refine((date) => !isNaN(Date.parse(date)), {
|
||||||
|
message: 'Invalid date format',
|
||||||
|
}),
|
||||||
|
value: z.number(),
|
||||||
|
unit: z.string(),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
8
test/sample.spec.ts
Normal file
8
test/sample.spec.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { expect } from 'chai';
|
||||||
|
|
||||||
|
describe('sample', () => {
|
||||||
|
it('should test all critical logic of the library', () => {
|
||||||
|
const it = true;
|
||||||
|
expect(it).to.be.eq(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
15
tsconfig.build.json
Normal file
15
tsconfig.build.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"incremental": false,
|
||||||
|
"noEmit": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"removeComments": true,
|
||||||
|
"declaration": false,
|
||||||
|
"module": "CommonJS",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"outDir": "dist"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"exclude": ["node_modules", "**/*.spec.ts", "test"]
|
||||||
|
}
|
||||||
18
tsconfig.json
Normal file
18
tsconfig.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"target": "ES2019",
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noFallthroughCasesInSwitch": false,
|
||||||
|
"baseUrl": "./"
|
||||||
|
},
|
||||||
|
"include": ["src", "test", ".eslintrc.js"]
|
||||||
|
}
|
||||||
0
volumes/.gitkeep
Normal file
0
volumes/.gitkeep
Normal file
Loading…
Reference in New Issue
Block a user