lupine.api 1.1.49 → 1.1.51

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.
@@ -218,7 +218,7 @@ export const AdminReleasePage = () => {
218
218
  releaseUpdateBtn.disabled = false;
219
219
  if (!dataResponse || dataResponse.status !== 'ok') {
220
220
  NotificationMessage.sendMessage(
221
- dataResponse.message || 'Failed to update release (timeout, possibly backend is runing, please wait!)',
221
+ dataResponse.message || 'Failed to update release (timeout, possibly backend is runing, please wait and click Check!)',
222
222
  NotificationColor.Error
223
223
  );
224
224
  return;
@@ -259,6 +259,9 @@ export const AdminReleasePage = () => {
259
259
 
260
260
  domUpdate.value = <ReleaseList result={result} onUpdate={onUpdate} onLogClick={onLogClick} />;
261
261
  domLog.value = <pre>{JSON.stringify(result, null, 2)}</pre>;
262
+ if (result.releaseProgress) {
263
+ NotificationMessage.sendMessage('Release progress: ' + result.releaseProgress, NotificationColor.Warning);
264
+ }
262
265
  };
263
266
 
264
267
  const onRefreshCacheLocal = async () => {
@@ -30,11 +30,10 @@ const ResourcesList = (props: {
30
30
  const listFolder = (results: any, parentFolder = '') => {
31
31
  return results.map((result: any) => (
32
32
  <div>
33
- <div
34
- class={`row-box mt-m` + (typeof result.size === 'undefined' ? ' f-folder' : '')}
35
- onClick={() => props.onListFolder(parentFolder + result.name)}
36
- >
37
- <label class='label mr-m f-name'>{result.name}</label>
33
+ <div class={`row-box mt-m`}>
34
+ <label class='label mr-m f-name' onClick={() => props.onListFolder(parentFolder + result.name)}>
35
+ {result.name}
36
+ </label>
38
37
  <label class='label mr-m f-time'>{result.time}</label>
39
38
  <label class='label mr-m f-size'>{typeof result.size !== 'undefined' && formatBytes(result.size)}</label>
40
39
  {typeof result.size !== 'undefined' && (
@@ -97,12 +96,10 @@ const ResourcesList = (props: {
97
96
  ));
98
97
  };
99
98
  const css: CssProps = {
100
- '.f-folder': {
101
- textDecoration: 'underline',
102
- cursor: 'pointer',
103
- },
104
99
  '.f-name': {
100
+ textDecoration: 'underline',
105
101
  width: '200px',
102
+ cursor: 'pointer',
106
103
  },
107
104
  '.f-size span, .f-time span': {
108
105
  color: 'red',
@@ -154,18 +151,18 @@ export const AdminResourcesPage = () => {
154
151
  fDom.click();
155
152
  };
156
153
  const onUploadLocal = (fPath: string) => {
157
- MessageBox.show({
158
- title: 'Override remote file',
159
- buttonType: MessageBoxButtonProps.YesNo,
160
- contentMinWidth: '300px',
161
- handleClicked: (index: number, close) => {
162
- if (index === 0) {
163
- uploadFile(fPath);
164
- }
165
- close();
166
- },
167
- children: <div>Do you upload local files that may overwrite remote file [{fPath}]?</div>,
168
- });
154
+ // MessageBox.show({
155
+ // title: 'Override remote file',
156
+ // buttonType: MessageBoxButtonProps.YesNo,
157
+ // contentMinWidth: '300px',
158
+ // handleClicked: (index: number, close) => {
159
+ // if (index === 0) {
160
+ uploadFile(fPath);
161
+ // }
162
+ // close();
163
+ // },
164
+ // children: <div>Do you upload local files that may overwrite remote file [{fPath}]?</div>,
165
+ // });
169
166
  };
170
167
  const onNewFolder = async (fPath: string) => {
171
168
  let newName = '';
@@ -37,7 +37,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
37
37
  // if isMobile=true, then the last number is 1, or if isMobile=false, the last is 2
38
38
  const chgTime = Math.trunc(f1.mtime.getTime() / 10) * 10 + (isMobile ? 1 : 2);
39
39
 
40
- // when it's isMobile, need to update env and configs
40
+ // when it's isMobile, need to update env and configs => no configs as mobile app fetches it from api
41
41
  if (!f2 || f2.mtime.getTime() !== chgTime || isMobile) {
42
42
  const inHtml = await fs.readFile(htmlFile, 'utf-8');
43
43
  let outStr = inHtml.replace(/{hash}/gi, new Date().getTime().toString(36));
@@ -48,7 +48,7 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
48
48
  const metaIndexStart = inHtml.indexOf(metaTextStart);
49
49
  const metaIndexEnd = inHtml.indexOf(metaTextEnd);
50
50
 
51
- const webConfig = await readWebConfig(outdirData);
51
+ // const webConfig = await readWebConfig(outdirData);
52
52
  const webEnvData = webEnv.getWebEnv(appName);
53
53
 
54
54
  outStr =
@@ -56,9 +56,9 @@ exports.cpIndexHtml = async (htmlFile, outputFile, appName, isMobile, defaultThe
56
56
  '<script id="web-env" type="application/json">' +
57
57
  JSON.stringify(webEnvData) +
58
58
  '</script>\r\n' +
59
- '<script id="web-setting" type="application/json">' +
60
- JSON.stringify(webConfig) +
61
- '</script>' +
59
+ // '<script id="web-setting" type="application/json">' +
60
+ // JSON.stringify(webConfig) +
61
+ // '</script>' +
62
62
  outStr.substring(metaIndexEnd + metaTextEnd.length);
63
63
  // outStr = webEnv.replaceWebEnv(inHtml, appName, true);
64
64
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.49",
3
+ "version": "1.1.51",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -11,6 +11,7 @@ import { AdminConfig } from './admin-config';
11
11
  import { Logger } from '../lib';
12
12
  import { IApiBase, ServerRequest } from '../models';
13
13
  import { ApiRouter } from '../api';
14
+ import { readWebConfig } from './web-config-api';
14
15
 
15
16
  const logger = new Logger('admin-api');
16
17
 
@@ -27,6 +28,8 @@ export class AdminApi implements IApiBase {
27
28
  }
28
29
 
29
30
  protected mountDashboard() {
31
+ this.router.use('/web-config', readWebConfig);
32
+
30
33
  const adminDb = new AdminDb();
31
34
  this.router.use('/db', needDevAdminSession, adminDb.getRouter());
32
35
 
@@ -10,6 +10,11 @@ export const needDevAdminSession = async (req: ServerRequest, res: ServerRespons
10
10
  const devAdminSession = await adminApiHelper.getDevAdminFromCookie(req, res, true);
11
11
  if (!devAdminSession) {
12
12
  // return true to skip the rest of the middleware
13
+ const response = {
14
+ status: 'error',
15
+ message: langHelper.getLang('shared:permission_denied'),
16
+ };
17
+ ApiHelper.sendJson(req, res, response);
13
18
  return true;
14
19
  }
15
20
  return false;
@@ -54,6 +54,8 @@ export const loadCSV = async (db: Db, lines: string[]) => {
54
54
  let table = '';
55
55
  let insSql = '';
56
56
  const result: any = {};
57
+ let toFields: string[] = [];
58
+ let toFieldsIndex: number[] = [];
57
59
  for (const i in lines) {
58
60
  if (!lines[i] || lines[i].startsWith('#')) {
59
61
  continue;
@@ -61,14 +63,25 @@ export const loadCSV = async (db: Db, lines: string[]) => {
61
63
  if (lines[i].startsWith('@TABLE,')) {
62
64
  table = lines[i].substring(7);
63
65
  result[table] = { succeeded: 0, failed: 0, errorMessage: [] };
66
+
67
+ const toTableInfo = await db.getTableInfo(table);
68
+ toFields = toTableInfo.map((item: any) => item.name);
64
69
  } else if (lines[i].startsWith('@FIELD,')) {
65
- const fields = lines[i].substring(7); //.split(',');
66
- const values = Array(fields.split(',').length).fill('?').join(',');
67
- insSql = `INSERT INTO ${table} (${fields} ) VALUES (${values})`;
70
+ const fromFields = lines[i]
71
+ .substring(7)
72
+ .split(',')
73
+ .filter((item: string) => toFields.includes(item));
74
+ toFieldsIndex = toFields.map((item: string) => fromFields.indexOf(item));
75
+ const values = Array(fromFields.length).fill('?').join(',');
76
+ insSql = `INSERT INTO ${table} (${fromFields.join(',')} ) VALUES (${values})`;
68
77
  } else {
78
+ if (toFields.length === 0 || !insSql) {
79
+ throw new Error('Invalid CSV format (no @TABLE or @FIELD)');
80
+ }
69
81
  try {
70
82
  const row = JSON.parse(lines[i]);
71
- const r = await db.execute(insSql, row);
83
+ const values = toFieldsIndex.map((index: number) => row[index]);
84
+ await db.execute(insSql, values);
72
85
  result[table].succeeded++;
73
86
  } catch (error: any) {
74
87
  result[table].failed++;
@@ -10,11 +10,13 @@ import {
10
10
  FsUtils,
11
11
  adminApiHelper,
12
12
  processRefreshCache,
13
+ apiStorage,
13
14
  } from 'lupine.api';
14
15
  import path from 'path';
15
16
  import { needDevAdminSession } from './admin-auth';
16
17
  import { adminTokenHelper } from './admin-token-helper';
17
18
 
19
+ const releaseProgress = 'admin-release-progress';
18
20
  export class AdminRelease implements IApiBase {
19
21
  private logger = new Logger('release-api');
20
22
  protected router = new ApiRouter();
@@ -30,7 +32,7 @@ export class AdminRelease implements IApiBase {
30
32
  protected mountDashboard() {
31
33
  // called by FE
32
34
  this.router.use('/check', needDevAdminSession, this.check.bind(this));
33
- this.router.use('/update', needDevAdminSession, this.update.bind(this));
35
+ this.router.use('/update', needDevAdminSession, this.callUpdate.bind(this));
34
36
  this.router.use('/view-log', needDevAdminSession, this.viewLog.bind(this));
35
37
  // called online or by clients
36
38
  this.router.use('/refresh-cache', needDevAdminSession, this.refreshCache.bind(this));
@@ -197,6 +199,7 @@ export class AdminRelease implements IApiBase {
197
199
  // const webSub = webSubFolders.filter(i => i.isDirectory()).map(i => path.join(i.parentPath.substring(appData.webPath.length + 1), i.name).replace(/\\/g, '/')).sort();
198
200
 
199
201
  const response = {
202
+ releaseProgress: await apiStorage.get(releaseProgress),
200
203
  status: 'ok',
201
204
  message: 'check.',
202
205
  appsFrom: apps,
@@ -261,6 +264,24 @@ export class AdminRelease implements IApiBase {
261
264
  return true;
262
265
  }
263
266
 
267
+ async callUpdate(req: ServerRequest, res: ServerResponse) {
268
+ // when remote server is slow, then local update call may be timeout.
269
+ // so we set a flag to prevent multiple update calls
270
+ apiStorage.set(releaseProgress, 'update started: ' + new Date().toLocaleString());
271
+ let result = true;
272
+ try {
273
+ result = await this.update(req, res);
274
+ } catch (e: any) {
275
+ const response = {
276
+ status: 'error',
277
+ message: e.message,
278
+ };
279
+ ApiHelper.sendJson(req, res, response);
280
+ }
281
+ apiStorage.set(releaseProgress, undefined);
282
+ return result;
283
+ }
284
+
264
285
  async update(req: ServerRequest, res: ServerResponse) {
265
286
  const jsonData = req.locals.json();
266
287
  const data = await this.chkData(jsonData, req, res, false);
@@ -391,6 +412,7 @@ export class AdminRelease implements IApiBase {
391
412
  this.logger.error(`updateSendFile, not found: ${sendFile}`);
392
413
  return { status: 'error', message: 'Client file not found: ' + sendFile };
393
414
  }
415
+ apiStorage.set(releaseProgress, 'updateSendFile: ' + sendFile);
394
416
  const fileContent = (await FsUtils.readFile(sendFile))!;
395
417
  // const compressedContent = await new Promise<Buffer>((resolve, reject) => {
396
418
  // zlib.gzip(fileContent, (err, buffer) => {
@@ -417,6 +439,12 @@ export class AdminRelease implements IApiBase {
417
439
  fileContent.length
418
440
  }), f: ${sendFile}`
419
441
  );
442
+ apiStorage.set(
443
+ releaseProgress,
444
+ `updateSendFile, index: ${cnt}, sending: ${chunk.length} (${i + chunk.length} / ${
445
+ fileContent.length
446
+ }), f: ${sendFile}`
447
+ );
420
448
  i > 0 && (await new Promise((resolve) => setTimeout(resolve, 1000)));
421
449
  const remoteData = await fetch(targetUrl + '/api/admin/release/byClientUpdate', postData);
422
450
  const resultText = await remoteData.text();
@@ -0,0 +1,19 @@
1
+ import { ServerResponse } from 'http';
2
+ import { Logger } from '../lib';
3
+ import { ServerRequest } from '../models';
4
+ import { langHelper } from '../lang';
5
+ import { ApiHelper, apiStorage } from '../api';
6
+
7
+ // only used by mobile app. For web, it's injected in the html by server-side render
8
+ const logger = new Logger('web-cfg-api');
9
+ export const readWebConfig = async (req: ServerRequest, res: ServerResponse) => {
10
+ logger.info('readWebConfig');
11
+
12
+ const response = {
13
+ status: 'ok',
14
+ result: await apiStorage.getWebAll(),
15
+ message: langHelper.getLang('shared:operation_success'),
16
+ };
17
+ ApiHelper.sendJson(req, res, response);
18
+ return true;
19
+ };
@@ -1,11 +1,11 @@
1
1
  /**
2
- * A persistent storage for the Api
2
+ * A persistent storage for the Api (stored in primary process)
3
3
  */
4
4
  import { IAppSharedStorage } from '../models';
5
5
  import { SimpleStorageDataProps } from '../models';
6
6
  import { apiCache } from './api-cache';
7
7
 
8
- // ApiSharedStorage is used in api module to store variables inside an api scope
8
+ // ApiSharedStorage is used in api module to store variables under appName scope, but is stored in one place - primary process
9
9
  export class ApiSharedStorage {
10
10
  private storage: IAppSharedStorage | undefined;
11
11
 
@@ -9,7 +9,6 @@ import { IToClientDelivery } from '../models/to-client-delivery-props';
9
9
  import { JsonObject } from '../models/json-object';
10
10
  import { getTemplateCache } from './api-cache';
11
11
  import { apiStorage } from './api-shared-storage';
12
- import { SimpleStorageDataProps } from '../models';
13
12
  import { RuntimeRequire } from '../lib/runtime-require';
14
13
 
15
14
  const logger = new Logger('StaticServer');
@@ -159,13 +158,13 @@ export const serverSideRenderPage = async (
159
158
  const _lupineJs = cachedHtml[nearRoot]._lupineJs;
160
159
  const currentCache = cachedHtml[nearRoot] as CachedHtmlProps;
161
160
  const webSetting = await apiStorage.getWebAll();
162
- const webSettingShortKey: SimpleStorageDataProps = {};
163
- for (let item of Object.keys(webSetting)) {
164
- const newItem = item.substring(4);
165
- webSettingShortKey[newItem] = webSetting[item];
166
- }
161
+ // const webSettingShortKey: SimpleStorageDataProps = {};
162
+ // for (let item of Object.keys(webSetting)) {
163
+ // const newItem = item.substring(4);
164
+ // webSettingShortKey[newItem] = webSetting[item];
165
+ // }
167
166
  // const webSetting = AppConfig.get(AppConfig.WEB_SETTINGS_KEY) || {};
168
- const clientDelivery = new ToClientDelivery(currentCache.webEnv, webSettingShortKey, req.locals.cookies());
167
+ const clientDelivery = new ToClientDelivery(currentCache.webEnv, webSetting, req.locals.cookies());
169
168
  const page = await _lupineJs.generatePage(props, clientDelivery);
170
169
  // console.log(`=========load lupin: `, content);
171
170
 
@@ -199,7 +198,7 @@ export const serverSideRenderPage = async (
199
198
  res.write(page.metaData);
200
199
  res.write(page.globalCss);
201
200
  res.write('<script id="web-env" type="application/json">' + JSON.stringify(currentCache.webEnv) + '</script>');
202
- res.write('<script id="web-setting" type="application/json">' + JSON.stringify(webSettingShortKey) + '</script>');
201
+ res.write('<script id="web-setting" type="application/json">' + JSON.stringify(webSetting) + '</script>');
203
202
  res.write(
204
203
  currentCache.content.substring(
205
204
  currentCache.metaIndexEnd + metaTextEnd.length,
@@ -1,13 +1,33 @@
1
1
  import cluster from 'cluster';
2
2
  import { Logger, LogWriter, LogWriterMessageId } from '../lib';
3
3
  import { processDebugMessage } from './process-dev-requests';
4
- import { appStorage } from './app-shared-storage';
5
- import { AppSharedStorageMessageId } from '../models';
6
4
  import { cleanupAndExit } from './cleanup-exit';
7
5
 
6
+ export type AppMessageProps = {
7
+ id: string;
8
+ message: string;
9
+ appName?: string;
10
+ [id: string]: any;
11
+ };
12
+ const _savedMessageHandler: {
13
+ fromPrimary: { [messageId: string]: (msgObject: AppMessageProps) => void };
14
+ fromWorker: { [messageId: string]: (msgObject: AppMessageProps) => void };
15
+ } = {
16
+ fromPrimary: {},
17
+ fromWorker: {},
18
+ };
19
+
20
+ export const registerMessageHandlerFromPrimary = (messageId: string, handler: (msgObject: AppMessageProps) => void) => {
21
+ _savedMessageHandler.fromPrimary[messageId] = handler;
22
+ };
23
+
24
+ export const registerMessageHandlerFromWorker = (messageId: string, handler: (msgObject: AppMessageProps) => void) => {
25
+ _savedMessageHandler.fromWorker[messageId] = handler;
26
+ };
27
+
8
28
  const logger = new Logger('app-message');
9
29
  // send msg to all clients
10
- const broadcast = (msgObject: any) => {
30
+ const broadcast = (msgObject: AppMessageProps) => {
11
31
  for (let i in cluster.workers) {
12
32
  if (cluster.workers[i]) cluster.workers[i].send(msgObject);
13
33
  }
@@ -15,33 +35,45 @@ const broadcast = (msgObject: any) => {
15
35
 
16
36
  // this is a worker and msg is from Primary
17
37
  // when debug is on, it's in primary, but it shouldn't receive those msgs
18
- export const processMessageFromPrimary = (msgObject: any) => {
38
+ export const processMessageFromPrimary = (msgObject: AppMessageProps) => {
19
39
  if (!msgObject || !msgObject.id) {
20
40
  logger.warn(`Unknown message from master in work: ${cluster.worker?.id}`);
21
41
  return;
22
42
  }
23
43
 
44
+ const primaryFn = _savedMessageHandler.fromPrimary[msgObject.id];
45
+ if (primaryFn) {
46
+ primaryFn(msgObject);
47
+ return;
48
+ }
49
+
24
50
  if (msgObject.id == 'debug') {
25
51
  processDebugMessage(msgObject);
26
- } else if (msgObject.id == AppSharedStorageMessageId) {
27
- appStorage.messageFromPrimaryProcess(msgObject);
52
+ // } else if (msgObject.id == AppSharedStorageMessageId) {
53
+ // appStorage.messageFromPrimaryProcess(msgObject);
28
54
  } else {
29
55
  logger.warn(`Unknown message: ${msgObject.id}`);
30
56
  }
31
57
  };
32
58
 
33
59
  // this is primary, msg is from a client
34
- export const processMessageFromWorker = (msgObject: any) => {
60
+ export const processMessageFromWorker = (msgObject: AppMessageProps) => {
35
61
  if (!msgObject || !msgObject.id) {
36
62
  if (msgObject['watch:require']) return;
37
63
  logger.warn(`Unknown message from work: ${cluster.worker?.id}`);
38
64
  return;
39
65
  }
40
66
 
67
+ const workerFn = _savedMessageHandler.fromWorker[msgObject.id];
68
+ if (workerFn) {
69
+ workerFn(msgObject);
70
+ return;
71
+ }
72
+
41
73
  if (msgObject.id == LogWriterMessageId) {
42
- LogWriter.messageFromSubProcess(msgObject);
43
- } else if (msgObject.id == AppSharedStorageMessageId) {
44
- appStorage.messageFromSubProcess(msgObject);
74
+ LogWriter.messageFromSubProcess(msgObject as any);
75
+ // } else if (msgObject.id == AppSharedStorageMessageId) {
76
+ // appStorage.messageFromSubProcess(msgObject);
45
77
  } else if (msgObject.id == 'debug') {
46
78
  logger.debug(
47
79
  `Message from worker ${cluster.worker?.id}, message: ${msgObject.message}, appName: ${msgObject.appName}`
@@ -1,5 +1,7 @@
1
1
  /**
2
- * A persistent storage for the Api
2
+ * A persistent storage to store data in primary process and share to all workers
3
+ *
4
+ * You should use apiStorage in api module
3
5
  */
4
6
  import * as fs from 'fs/promises';
5
7
  import * as path from 'path';
@@ -14,6 +16,7 @@ import {
14
16
  IAppSharedStorage,
15
17
  StorageMessageFromSubProcess,
16
18
  } from '../models';
19
+ import { registerMessageHandlerFromPrimary, registerMessageHandlerFromWorker } from './app-message';
17
20
 
18
21
  // in Api scope, use ApiSharedStorage instead of this
19
22
  // storage cross clusters, loaded when start and saved before exist
@@ -27,6 +30,15 @@ export class AppSharedStorage implements IAppSharedStorage {
27
30
  public static getInstance(): IAppSharedStorage {
28
31
  if (!AppSharedStorage.instance) {
29
32
  AppSharedStorage.instance = new AppSharedStorage();
33
+ // register app message handlers
34
+ registerMessageHandlerFromPrimary(
35
+ AppSharedStorageMessageId,
36
+ AppSharedStorage.instance.messageFromPrimaryProcess.bind(AppSharedStorage.instance)
37
+ );
38
+ registerMessageHandlerFromWorker(
39
+ AppSharedStorageMessageId,
40
+ AppSharedStorage.instance.messageFromSubProcess.bind(AppSharedStorage.instance)
41
+ );
30
42
  }
31
43
  return AppSharedStorage.instance;
32
44
  }
@@ -129,9 +141,9 @@ export class AppSharedStorage implements IAppSharedStorage {
129
141
  }
130
142
 
131
143
  // called from primary before exit, or from api to save changes
132
- async save(appName?: string) {
144
+ async save(appName?: string, exit?: boolean) {
133
145
  if (!cluster.isPrimary) {
134
- AppSharedStorageWorker.save(appName);
146
+ await AppSharedStorageWorker.save(appName);
135
147
  return;
136
148
  }
137
149
 
@@ -171,8 +183,16 @@ export class AppSharedStorage implements IAppSharedStorage {
171
183
  getApi(appName: string, key: string): Promise<string> {
172
184
  return this.get(appName, AppSharedStorageApiPrefix + key);
173
185
  }
174
- getWebAll(appName: string): Promise<SimpleStorageDataProps> {
175
- return this.getWithPrefix(appName, AppSharedStorageWebPrefix);
186
+ async getWebAll(appName: string): Promise<SimpleStorageDataProps> {
187
+ const webAll = await this.getWithPrefix(appName, AppSharedStorageWebPrefix);
188
+
189
+ const webSettingShortKey: SimpleStorageDataProps = {};
190
+ for (let item of Object.keys(webAll)) {
191
+ const newItem = item.substring(AppSharedStorageWebPrefix.length);
192
+ webSettingShortKey[newItem] = webAll[item];
193
+ }
194
+
195
+ return webSettingShortKey;
176
196
  }
177
197
  getWithPrefix(appName: string, prefixKey: string): Promise<SimpleStorageDataProps> {
178
198
  return new Promise((resolve, reject) => {
@@ -255,7 +275,7 @@ class AppSharedStorageWorker {
255
275
  workerId: cluster.worker?.id || 0,
256
276
  action: 'save',
257
277
  appName: appName || '',
258
- key: '',
278
+ key: 'save',
259
279
  };
260
280
  process.send!(obj);
261
281
  }
@@ -278,7 +298,12 @@ class AppSharedStorageWorker {
278
298
  process.send!(obj);
279
299
  }
280
300
 
281
- static getWithPrefix(appName: string, prefixKey: string, resolve: (value: any) => void, reject: (reason: any) => void) {
301
+ static getWithPrefix(
302
+ appName: string,
303
+ prefixKey: string,
304
+ resolve: (value: any) => void,
305
+ reject: (reason: any) => void
306
+ ) {
282
307
  if (cluster.isPrimary) {
283
308
  throw new Error('AppSharedStorageWorker should be only called from workers');
284
309
  }
@@ -46,7 +46,7 @@ class AppStart {
46
46
  }
47
47
  }
48
48
 
49
- if (props.debug || !cluster.isPrimary) {
49
+ if (!cluster.isPrimary) {
50
50
  console.log(`Worker id ${this.getWorkerId()}`);
51
51
 
52
52
  process.on('message', processMessageFromPrimary);
@@ -55,7 +55,7 @@ class AppStart {
55
55
  appLoader.loadApi(props.apiConfig);
56
56
  this.initServer(props.serverConfig);
57
57
  } else if (cluster.isPrimary) {
58
- const numCPUs = require('os').cpus().length;
58
+ const numCPUs = props.debug ? 1 : require('os').cpus().length;
59
59
  console.log(`Master Process is trying to fork ${numCPUs} processes`);
60
60
 
61
61
  for (let i = 0; i < numCPUs; i++) {
@@ -73,7 +73,7 @@ class AppStart {
73
73
  bindProcess() {
74
74
  if (cluster.isPrimary) {
75
75
  // it looks like the child processes are hung up here
76
- process.stdin.resume(); // so the program will not close instantly
76
+ process.stdin.resume(); // so the program will not close instantly, keep isPrimary process running
77
77
  }
78
78
  // Emitted whenever a no-error-handler Promise is rejected
79
79
  process.on('unhandledRejection', (reason: string, promise) => {
@@ -6,7 +6,7 @@ export const cleanupAndExit = async () => {
6
6
  // save shared storage first
7
7
  if (cluster.isPrimary) {
8
8
  // save only happens once
9
- await appStorage.save();
9
+ await appStorage.save('', true);
10
10
  }
11
11
  process.exit(0);
12
12
  };
@@ -39,7 +39,47 @@ export const setServerName = (serverName: string) => {
39
39
  SERVER_NAME = serverName;
40
40
  };
41
41
 
42
- export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
42
+ export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => Promise<void>;
43
+
44
+ /*
45
+ IP limit
46
+
47
+ let IP_LIMIT_ENABLED = false;
48
+ export const setIpLimitEnabled = (enabled: boolean) => {
49
+ IP_LIMIT_ENABLED = enabled;
50
+ };
51
+
52
+ let IP_LIMIT_RATE = 60;
53
+ let IP_LIMIT_DURATION = 1000 * 30;
54
+ export const setIpLimitRateAndDuration = (rate: number, durationSeconds: number) => {
55
+ IP_LIMIT_RATE = rate;
56
+ IP_LIMIT_DURATION = durationSeconds * 1000;
57
+ };
58
+
59
+
60
+ class TTLMap<K, V> extends Map<K, V> {
61
+ set(key: K, value: V): this;
62
+ set(key: K, value: V, ttl: number): this;
63
+ set(key: K, value: V, ttl?: number): this {
64
+ const isNew = !this.has(key);
65
+ super.set(key, value);
66
+ if (isNew && ttl) {
67
+ setTimeout(() => this.delete(key), ttl).unref?.();
68
+ }
69
+ return this;
70
+ }
71
+ }
72
+ const counts = new TTLMap<string, number>();
73
+
74
+ function checkFixedWindow(ip: string, limit = IP_LIMIT_RATE) {
75
+ const now = Date.now();
76
+ const key = `${ip}:${Math.floor(now / IP_LIMIT_DURATION)}`;
77
+ const count = (counts.get(key) || 0) + 1;
78
+ counts.set(key, count, IP_LIMIT_DURATION * 2); // key自动过期
79
+ return count > limit;
80
+ }
81
+
82
+ */
43
83
 
44
84
  let lastRequestTime = new Date().getTime();
45
85
 
@@ -57,16 +97,14 @@ export class WebListener {
57
97
  addRawMiddlewareChain(middleware: RawMiddleware) {
58
98
  this.rawMiddlewares.push(middleware);
59
99
  }
60
- async handleRawMiddlewares(req: IncomingMessage, res: ServerResponse) {
61
- const runChain = (list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) => {
62
- const dispatch = async (i: number) => {
63
- const fn = list[i];
64
- if (!fn) return;
65
- await fn(context.req, context.res, () => dispatch(i + 1));
66
- };
67
- return dispatch(0);
100
+
101
+ runMiddlewareChain(list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) {
102
+ const dispatch = async (i: number) => {
103
+ const fn = list[i];
104
+ if (!fn) return;
105
+ await fn(context.req, context.res, () => dispatch(i + 1));
68
106
  };
69
- await runChain(this.rawMiddlewares, { req, res });
107
+ return dispatch(0);
70
108
  }
71
109
 
72
110
  async listener(reqOrigin: IncomingMessage, res: ServerResponse) {
@@ -87,7 +125,7 @@ export class WebListener {
87
125
  return;
88
126
  }
89
127
 
90
- await this.handleRawMiddlewares(reqOrigin, res);
128
+ await this.runMiddlewareChain(this.rawMiddlewares, { req: reqOrigin, res });
91
129
  if (res.writableEnded || res.headersSent) {
92
130
  return;
93
131
  }
@@ -95,7 +95,7 @@ export class DbSqlite extends Db {
95
95
 
96
96
  public async getTableInfo(table: string): Promise<any> {
97
97
  const query = `PRAGMA table_info(${table});`;
98
- const result = await this.execute(query);
98
+ const result = await this.select(query);
99
99
  return result;
100
100
  }
101
101
  }
package/src/lib/db/db.ts CHANGED
@@ -2,7 +2,12 @@ import { Logger } from '../logger';
2
2
  import { DbConfig } from '../../models/db-config';
3
3
 
4
4
  // Instead, Boolean values are stored as integers 0 (false) and 1 (true).
5
+ export type DbFieldExprssionProps = { exprssion: string; params?: (string | number)[] };
5
6
  export type DbFieldValue = { [key: string]: string | number };
7
+ export type DbFieldExpression = { [key: string]: string | number | DbFieldExprssionProps };
8
+ const isDbFieldExprssion = (value: any): value is DbFieldExprssionProps => {
9
+ return value && typeof value === 'object' && 'exprssion' in value;
10
+ };
6
11
 
7
12
  const logger = new Logger('db');
8
13
  export class Db {
@@ -252,12 +257,28 @@ export class Db {
252
257
  return await this.execute(sql, params);
253
258
  }
254
259
 
255
- public async updateObject(table: string, updateFieldValues: DbFieldValue, whereFieldValues: DbFieldValue) {
260
+ public async updateObject(table: string, updateFieldValues: DbFieldExpression, whereFieldValues: DbFieldValue) {
256
261
  table = this.replacePrefix(table);
257
262
  const fields = Object.keys(updateFieldValues);
258
- let sql = 'UPDATE ' + table + ' SET ' + fields.map((item) => `${item}=?`).join(',');
259
- const params = Object.values(updateFieldValues);
263
+ const setClauseParts: string[] = [];
264
+ const params: (string | number)[] = [];
265
+ // let sql = 'UPDATE ' + table + ' SET ' + fields.map((item) => `${item}=?`).join(',');
266
+ // const params = Object.values(updateFieldValues);
267
+ for (const field of fields) {
268
+ const value = updateFieldValues[field];
269
+
270
+ // expression
271
+ if (isDbFieldExprssion(value)) {
272
+ setClauseParts.push(`${field} = ${value.exprssion}`);
273
+ if (value.params) params.push(...value.params);
274
+ } else {
275
+ // static value
276
+ setClauseParts.push(`${field} = ?`);
277
+ params.push(value);
278
+ }
279
+ }
260
280
 
281
+ let sql = `UPDATE ${table} SET ${setClauseParts.join(', ')}`;
261
282
  if (whereFieldValues && Object.keys(whereFieldValues).length > 0) {
262
283
  sql +=
263
284
  ' WHERE ' +
package/src/lib/logger.ts CHANGED
@@ -25,10 +25,17 @@ export const LogWriterMessageId = 'LogWriter';
25
25
  logger.info('log test. numer: %d, string: %s, json: %j', 11, 'p2', {key: 'value', n: 22});
26
26
  */
27
27
  export class LogWriter {
28
- static instance = new LogWriter();
28
+ static instance?: LogWriter;
29
29
 
30
30
  private constructor() {}
31
31
 
32
+ public static getInstance(): LogWriter {
33
+ if (!LogWriter.instance) {
34
+ LogWriter.instance = new LogWriter();
35
+ }
36
+ return LogWriter.instance;
37
+ }
38
+
32
39
  private _config: LogConfig | undefined;
33
40
  getConfig = (): LogConfig => {
34
41
  return this._config || getDefaultLogConfig();
@@ -138,7 +145,7 @@ export class LogWriter {
138
145
  console.error('Logger got wrong message: ', msgObject);
139
146
  return;
140
147
  }
141
- LogWriter.instance._log(
148
+ LogWriter.getInstance()._log(
142
149
  msgObject.level,
143
150
  msgObject.namespace,
144
151
  msgObject.color,
@@ -234,41 +241,41 @@ export class Logger {
234
241
  }
235
242
 
236
243
  isDebug(): boolean {
237
- return LogWriter.instance.level <= 4;
244
+ return LogWriter.getInstance().level <= 4;
238
245
  }
239
246
 
240
247
  debug(...message: (string | object | number)[]) {
241
- if (LogWriter.instance.level < 4) {
248
+ if (LogWriter.getInstance().level < 4) {
242
249
  return;
243
250
  }
244
- LogWriter.instance._log('DEBUG', this.namespace, this.color, message);
251
+ LogWriter.getInstance()._log('DEBUG', this.namespace, this.color, message);
245
252
  }
246
253
 
247
254
  info(...message: (string | object | number)[]) {
248
- if (LogWriter.instance.level < 3) {
255
+ if (LogWriter.getInstance().level < 3) {
249
256
  return;
250
257
  }
251
- LogWriter.instance._log('INFO', this.namespace, this.color, message);
258
+ LogWriter.getInstance()._log('INFO', this.namespace, this.color, message);
252
259
  }
253
260
 
254
261
  warn(...message: (string | object | number)[]) {
255
- if (LogWriter.instance.level < 2) {
262
+ if (LogWriter.getInstance().level < 2) {
256
263
  return;
257
264
  }
258
- LogWriter.instance._log('WARN', this.namespace, this.color, message);
265
+ LogWriter.getInstance()._log('WARN', this.namespace, this.color, message);
259
266
  }
260
267
 
261
268
  error(...message: (string | object | number)[]) {
262
- if (LogWriter.instance.level < 1) {
269
+ if (LogWriter.getInstance().level < 1) {
263
270
  return;
264
271
  }
265
- LogWriter.instance._log('ERROR', this.namespace, this.color, message);
272
+ LogWriter.getInstance()._log('ERROR', this.namespace, this.color, message);
266
273
  }
267
274
 
268
275
  fatal(...message: (string | object | number)[]) {
269
- if (LogWriter.instance.level < 0) {
276
+ if (LogWriter.getInstance().level < 0) {
270
277
  return;
271
278
  }
272
- LogWriter.instance._log('FATAL', this.namespace, this.color, message);
279
+ LogWriter.getInstance()._log('FATAL', this.namespace, this.color, message);
273
280
  }
274
281
  }
@@ -25,7 +25,7 @@ export interface IAppSharedStorage {
25
25
  // this is primary, msg is from a client
26
26
  messageFromSubProcess(msgObject: any): void;
27
27
  load(appName: string, rootPath: string): Promise<void>;
28
- save(appName?: string): Promise<void>;
28
+ save(appName?: string, exit?: boolean): Promise<void>;
29
29
  get(appName: string, key: string): Promise<string>;
30
30
  getWeb(appName: string, key: string): Promise<string>;
31
31
  getApi(appName: string, key: string): Promise<string>;
@@ -1,274 +0,0 @@
1
- import { IncomingMessage, ServerResponse } from 'http';
2
- import { Logger } from '../lib/logger';
3
- import crypto from 'crypto';
4
- import { parseCookies } from '../lib/utils/cookie-util';
5
- import { WebProcessor } from './web-processor';
6
- import { handler403, handler404, handler500, handler503, SimpleStorage } from '../api';
7
- import { JsonObject, AsyncStorageProps, ServerRequest, SetCookieProps } from '../models';
8
- import { HostToPath } from './host-to-path';
9
- import { serializeCookie } from '../lib/utils/cookie-util';
10
- const logger = new Logger('listener');
11
-
12
- let MAX_REQUEST_SIZE = 1024 * 1024 * 5;
13
- export const setMaxRequestSize = (size: number) => {
14
- MAX_REQUEST_SIZE = size;
15
- };
16
-
17
- // The maximum number of requests being processed. If there are no requests for 10 minutes, this number will be reset to 0.
18
- let MAX_REQUEST_COUNT = 100;
19
- let REQUEST_COUNT = 0;
20
- export const setMaxRequestCount = (count: number) => {
21
- MAX_REQUEST_COUNT = count;
22
- };
23
- export const getRequestCount = () => {
24
- return REQUEST_COUNT;
25
- };
26
-
27
- let REQUEST_TIMEOUT = 1000 * 30;
28
- export const setRequestTimeout = (timeout: number) => {
29
- REQUEST_TIMEOUT = timeout;
30
- };
31
-
32
- let accessControlAllowHosts: string[] = [];
33
- export const setAccessControlAllowHost = (allowHosts: string[]) => {
34
- accessControlAllowHosts = allowHosts;
35
- };
36
-
37
- let SERVER_NAME: string = 'nginx/1.19.2';
38
- export const setServerName = (serverName: string) => {
39
- SERVER_NAME = serverName;
40
- };
41
-
42
- let IP_LIMIT_RATE_PER_SECOND = 3;
43
- export const setIpLimitRatePerSecond = (rate: number) => {
44
- IP_LIMIT_RATE_PER_SECOND = rate;
45
- };
46
- let IP_LIMIT_RATE_PER_MINUTE = 60;
47
- export const setIpLimitRatePerMinute = (rate: number) => {
48
- IP_LIMIT_RATE_PER_MINUTE = rate;
49
- };
50
-
51
- let IP_BLOCK_TIME = 60_000;
52
- export const setIpBlockTime = (time: number) => {
53
- IP_BLOCK_TIME = time;
54
- };
55
-
56
- export const rawMiddlewareIpLimit = (req: IncomingMessage, res: ServerResponse, next: () => void) => {
57
- const clientIp = req.socket.remoteAddress || 'unknown';
58
- };
59
-
60
- export type RawMiddleware = (req: IncomingMessage, res: ServerResponse, next: () => void) => void;
61
-
62
- let lastRequestTime = new Date().getTime();
63
-
64
- // type ProcessRequest = (req: ServerRequest, res: ServerResponse) => void;
65
- export class WebListener {
66
- // process requests before business logic, for example IP filter, rate limit, etc.
67
- rawMiddlewares: RawMiddleware[];
68
- processor: WebProcessor;
69
-
70
- constructor(processRequest: WebProcessor) {
71
- this.rawMiddlewares = [];
72
- this.processor = processRequest;
73
- }
74
-
75
- addRawMiddlewareChain(middleware: RawMiddleware) {
76
- this.rawMiddlewares.push(middleware);
77
- }
78
- async handleRawMiddlewares(req: IncomingMessage, res: ServerResponse) {
79
- const runChain = (list: RawMiddleware[], context: { req: IncomingMessage; res: ServerResponse }) => {
80
- const dispatch = async (i: number) => {
81
- const fn = list[i];
82
- if (!fn) return;
83
- await fn(context.req, context.res, () => dispatch(i + 1));
84
- };
85
- return dispatch(0);
86
- };
87
- await runChain(this.rawMiddlewares, { req, res });
88
- }
89
-
90
- async listener(reqOrigin: IncomingMessage, res: ServerResponse) {
91
- // If there is no request in the last 10 minutes, reset the request count.
92
- if (new Date().getTime() - lastRequestTime > 1000 * 60 * 10) {
93
- if (REQUEST_COUNT != 0) {
94
- // in case any errors skipped (--REQUEST_COUNT)
95
- logger.warn(`!!!!!!!!!! ========== REQUEST_COUNT is not counted properly: ${REQUEST_COUNT}`);
96
- }
97
- REQUEST_COUNT = 0;
98
- lastRequestTime = new Date().getTime();
99
- }
100
-
101
- // back-pressure
102
- if (REQUEST_COUNT > MAX_REQUEST_COUNT) {
103
- logger.warn(`Too many requests, count: ${REQUEST_COUNT} > ${MAX_REQUEST_COUNT}`);
104
- handler503(res, 'Server is busy, please retry later.');
105
- return;
106
- }
107
-
108
- await this.handleRawMiddlewares(reqOrigin, res);
109
- if (res.writableEnded || res.headersSent) {
110
- return;
111
- }
112
-
113
- // const requestStart = process.hrtime.bigint();
114
- const uuid = crypto.randomUUID();
115
- const url = reqOrigin.url || '';
116
- const requestInfo = `uuid: ${uuid}, Access url: ${url}`;
117
- const req = reqOrigin as ServerRequest;
118
-
119
- const host = (req.headers.host || '').split(':')[0]; // req.headers.host contains port
120
- const hostPath = HostToPath.findHostPath(host);
121
- if (!hostPath || !hostPath.webPath || !hostPath.appName) {
122
- const msg = `Web root is not defined properly for host: ${host}.`;
123
- logger.error(msg);
124
- handler404(res, msg);
125
- return;
126
- }
127
-
128
- REQUEST_COUNT++;
129
- logger.debug(
130
- `Request started. Count: ${REQUEST_COUNT}, Log uuid: ${uuid}, access: ${
131
- req.headers.host
132
- }, url: ${url}, time: ${new Date().toISOString()}, from: ${req.socket.remoteAddress}`
133
- );
134
-
135
- const urlSplit = url.split('?');
136
- req.setTimeout(REQUEST_TIMEOUT);
137
- req.on('timeout', () => {
138
- REQUEST_COUNT--;
139
- logger.warn('timeout');
140
- req.destroy(new Error('timeout handling'));
141
- });
142
-
143
- const jsonFn = (): JsonObject | undefined => {
144
- if (!req.locals._json && req.locals.body) {
145
- const sBody = req.locals.body.toString();
146
- if (!sBody) {
147
- req.locals._json = undefined;
148
- } else {
149
- try {
150
- req.locals._json = JSON.parse(sBody);
151
- } catch (err: any) {
152
- logger.warn(`JSON.parse error: ${err.message}`);
153
- }
154
- }
155
- }
156
- return req.locals._json;
157
- };
158
- const cookiesFn = (): SimpleStorage => {
159
- if (!req.locals._cookies) {
160
- req.locals._cookies = new SimpleStorage(req.headers ? parseCookies(req.headers.cookie) : {});
161
- }
162
- return req.locals._cookies;
163
- };
164
- const setCookieFn = (name: string, value: string, options: SetCookieProps): void => {
165
- const cookies: string[] = [];
166
- const cookiesOld = res.getHeader('Set-Cookie');
167
- if (cookiesOld) {
168
- if (!Array.isArray(cookiesOld)) {
169
- cookies.push(cookiesOld as any);
170
- } else {
171
- cookies.push(...cookiesOld);
172
- }
173
- }
174
-
175
- const cookiePair = serializeCookie(name, value, options);
176
- cookies.push(cookiePair);
177
- res.setHeader('Set-Cookie', cookies);
178
-
179
- const localCookies = req.locals.cookies();
180
- localCookies.set(name, value);
181
- };
182
-
183
- req.locals = {
184
- uuid,
185
- host,
186
- url,
187
- hostPath,
188
- // urlSections: urlSplit[0].split('/').filter((i) => !!i),
189
- query: new URLSearchParams(urlSplit[1] || ''),
190
- urlWithoutQuery: urlSplit[0],
191
- urlParameters: new SimpleStorage({}),
192
- body: undefined,
193
- json: jsonFn,
194
- cookies: cookiesFn,
195
- setCookie: setCookieFn,
196
- clearCookie: (name: string) => {
197
- res.setHeader('Set-Cookie', `${name}=; max-age=0`);
198
- },
199
- };
200
-
201
- let bigRequest = false;
202
- let totalLength = 0;
203
- const bodyData: any[] = [];
204
- req.on('error', (err: any) => {
205
- REQUEST_COUNT--;
206
- logger.error(`${requestInfo}, count: ${REQUEST_COUNT}, Request Error: `, err);
207
- handler500(res, `listener error: ${err && err.message}`);
208
- });
209
-
210
- req.on('data', (chunk: any) => {
211
- totalLength += chunk.length;
212
- logger.debug(`${requestInfo}, Request data length: ${chunk.length}, total: ${totalLength}`);
213
- // Limit Request Size
214
- if (!bigRequest && totalLength < MAX_REQUEST_SIZE) {
215
- bodyData.push(chunk);
216
- } else {
217
- if (!bigRequest) {
218
- bigRequest = true;
219
- logger.warn(`Warn, request data is too big: ${totalLength} > ${MAX_REQUEST_SIZE}`);
220
- }
221
- req.socket.destroy();
222
- }
223
- });
224
-
225
- req.on('end', async () => {
226
- try {
227
- if (bigRequest) {
228
- logger.warn(`Request data is too big to process, url: ${req.locals.url}`);
229
- handler403(res, `Request data is too big to process, url: ${req.locals.url}`);
230
- return;
231
- }
232
-
233
- const body = Buffer.concat(bodyData);
234
- const contentType = req.headers['content-type'];
235
- logger.debug(`url: ${url}, Request body length: ${body.length}, contentType: ${contentType}`);
236
- req.locals.body = body;
237
-
238
- res.setHeader('Server', SERVER_NAME);
239
- if (accessControlAllowHosts.includes(host)) {
240
- const allowOrigin = req.headers.origin && req.headers.origin !== 'null' ? req.headers.origin : '*';
241
- res.setHeader('Access-Control-Allow-Origin', allowOrigin);
242
- res.setHeader('Access-Control-Allow-Credentials', 'true');
243
- }
244
-
245
- const store: AsyncStorageProps = {
246
- uuid: uuid,
247
- hostPath: hostPath,
248
- appName: hostPath.appName,
249
- locals: req.locals,
250
- lang: req.locals.cookies().get('lang', 'en') || 'en',
251
- };
252
- await this.processor.processRequest(store, req, res);
253
- } finally {
254
- REQUEST_COUNT--;
255
- }
256
- // await new Promise(resolve => setTimeout(resolve, 3000));
257
-
258
- // asyncLocalStorage.run(store, async () => {
259
- // try {
260
- // await onEnd();
261
- // } catch (error: any) {
262
- // logger.error(`url: ${url}, Request end error: `, error.message);
263
- // }
264
-
265
- // lastRequestTime = new Date().getTime();
266
- // const requestEnd = process.hrtime.bigint();
267
- // REQUEST_COUNT--;
268
- // logger.debug(
269
- // `Request finished. Count: ${REQUEST_COUNT}, url: ${url}, time: ${new Date().toISOString()}, duration: ${Number(requestEnd - requestStart) / 1000000} ms`
270
- // );
271
- // });
272
- });
273
- }
274
- }