lupine.api 1.1.63 → 1.1.65

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.
package/README.md CHANGED
@@ -1,3 +1,122 @@
1
1
  # lupine.api
2
2
 
3
- lupine.api is a fast, lightweight, and flexible node.js based server, working with [lupine.web](https://github.com/uuware/lupine.web) to provide SSR and modern JavaScript features for web applications and APIs.
3
+ lupine.api is a fast, lightweight, and flexible node.js based server framework. It is designed to work seamlessly with [lupine.web](https://github.com/uuware/lupine.web) to provide Server-Side Rendering (SSR) and modern API capabilities.
4
+
5
+ The project consists of two main parts:
6
+
7
+ 1. **Server (`src/app`)**: A robust HTTP/HTTPS server that manages multiple applications, domains, and processes.
8
+ 2. **API Module (`src/api`)**: A framework for building the backend logic for individual applications.
9
+
10
+ ---
11
+
12
+ ## Server (`src/app`)
13
+
14
+ The server component is responsible for handling incoming network requests, managing SSL certificates, and dispatching requests to the appropriate application based on domain configuration. It supports clustering logic to utilize multi-core CPUs efficiently.
15
+
16
+ ### Key Features
17
+
18
+ - **Multi-App & Multi-Domain**: Host multiple independent applications on a single server instance, routing traffic based on domain names.
19
+ - **Cluster Support**: Automatically forks worker processes to match CPU cores for high performance.
20
+ - **SSL/TLS**: Built-in support for HTTPS with custom certificate paths.
21
+ - **Environment Management**: Loads configuration from `.env` files.
22
+
23
+ ### Usage Example
24
+
25
+ See `apps/server/src/index.ts` for a complete example.
26
+
27
+ ```typescript
28
+ import { appStart, loadEnv, ServerEnvKeys } from 'lupine.api';
29
+ import * as path from 'path';
30
+
31
+ const initAndStartServer = async () => {
32
+ // 1. Load Environment Variables
33
+ await loadEnv('.env');
34
+
35
+ // 2. Configure Applications
36
+ const serverRootPath = path.resolve(process.env[ServerEnvKeys.SERVER_ROOT_PATH]!);
37
+ const webRootMap = [
38
+ {
39
+ appName: 'demo.app',
40
+ hosts: ['localhost', 'example.com'],
41
+ // Standard directory structure expected by lupine.api
42
+ webPath: path.join(serverRootPath, 'demo.app_web'),
43
+ dataPath: path.join(serverRootPath, 'demo.app_data'),
44
+ apiPath: path.join(serverRootPath, 'demo.app_api'),
45
+ dbType: 'sqlite',
46
+ dbConfig: { filename: 'sqlite3.db' },
47
+ },
48
+ ];
49
+
50
+ // 3. Start Server
51
+ await appStart.start({
52
+ debug: process.env.NODE_ENV === 'development',
53
+ apiConfig: {
54
+ serverRoot: serverRootPath,
55
+ webHostMap: webRootMap,
56
+ },
57
+ serverConfig: {
58
+ httpPort: 8080,
59
+ httpsPort: 8443,
60
+ // sslKeyPath: '...',
61
+ // sslCrtPath: '...',
62
+ },
63
+ });
64
+ };
65
+
66
+ initAndStartServer();
67
+ ```
68
+
69
+ ---
70
+
71
+ ## API Module (`src/api`)
72
+
73
+ The API Module provides the framework for writing the business logic of your application. It acts similarly to Express.js but is optimized for the lupine ecosystem.
74
+
75
+ ### Key Features
76
+
77
+ - **ApiRouter**: A flexible router supporting GET, POST, and middleware-like filters.
78
+ - **Context Isolation**: Uses `AsyncStorage` to safely manage request-scoped data (like sessions or database transactions) across async operations.
79
+ - **Database Integration**: Built-in helpers for database connections (e.g., SQLite via `better-sqlite3`).
80
+
81
+ ### Usage Example
82
+
83
+ An application's API entry point (e.g., `apps/demo.app/api/src/index.ts`) typically exports an instance of `ApiModule`.
84
+
85
+ **1. Entry Point (`index.ts`):**
86
+
87
+ ```typescript
88
+ import { ApiModule } from 'lupine.api';
89
+ import { RootApi } from './service/root-api';
90
+
91
+ // Export apiModule so the server can load it dynamically
92
+ export const apiModule = new ApiModule(new RootApi());
93
+ ```
94
+
95
+ **2. Root API Service (`service/root-api.ts`):**
96
+
97
+ ```typescript
98
+ import { ApiRouter, IApiBase, IApiRouter } from 'lupine.api';
99
+
100
+ export class RootApi implements IApiBase {
101
+ getRouter(): IApiRouter {
102
+ const router = new ApiRouter();
103
+
104
+ // Define routes
105
+ router.get('/hello', async (req, res) => {
106
+ res.write(JSON.stringify({ message: 'Hello World' }));
107
+ res.end();
108
+ return true; // Return true to indicate request was handled
109
+ });
110
+
111
+ // Sub-routers can be mounted
112
+ // router.use('/users', new UserApi().getRouter());
113
+
114
+ return router;
115
+ }
116
+ }
117
+ ```
118
+
119
+ ### Dashboard
120
+
121
+ lupine.api includes a built-in extensible dashboard for managing your services.
122
+ Detailed documentation can be found in [README-dashboard.md](README-dashboard.md).
package/dev/index.js CHANGED
@@ -4,6 +4,7 @@ const fileUtils = require('./file-utils.js');
4
4
  const webEnv = require('../src/common-js/web-env.js');
5
5
  const { runCmd } = require('./run-cmd.js');
6
6
  const { sendRequest } = require('./send-request.js');
7
+ const { markdownProcessOnEnd } = require('./markdown-build.js');
7
8
  module.exports = {
8
9
  copyFolder,
9
10
  cpIndexHtml,
@@ -15,4 +16,5 @@ module.exports = {
15
16
  loadEnv: webEnv.loadEnv,
16
17
  getWebConfig: webEnv.getWebConfig,
17
18
  pluginIfelse: require('./plugin-ifelse.js'),
19
+ markdownProcessOnEnd,
18
20
  };
@@ -0,0 +1,326 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { marked } = require('marked');
4
+ const matter = require('gray-matter');
5
+
6
+ const slugify = (text) =>
7
+ text
8
+ .toLowerCase()
9
+ .replace(/[^\w\u4e00-\u9fa5]+/g, '-')
10
+ .replace(/^-+|-+$/g, '');
11
+
12
+ const cleanMarkdown = (text) => text.replace(/[`*?^]/g, '');
13
+
14
+ marked.use({
15
+ renderer: {
16
+ heading({ text, depth, raw }) {
17
+ const id = slugify(raw);
18
+ // text might contain markdown (e.g. links), so we must parse it inline
19
+ const content = marked.parseInline(text);
20
+ return `<h${depth} id="${id}"><a class="header-anchor" href="#${id}">#</a>${content}</h${depth}>`;
21
+ },
22
+ },
23
+ });
24
+
25
+ const builtFiles = new Map(); // path -> { route, importPath, data, headings }
26
+
27
+ function processFile(filePath, indir, outdir, relativePath) {
28
+ if (builtFiles.has(filePath)) return builtFiles.get(filePath);
29
+
30
+ // Mark as processing to handle potential cycles (though minimal risk with hierarchy)
31
+ // We'll update the value later.
32
+ builtFiles.set(filePath, null);
33
+
34
+ if (!fs.existsSync(filePath)) {
35
+ console.warn(`File not found: ${filePath}`);
36
+ return null;
37
+ }
38
+
39
+ const contentRaw = fs.readFileSync(filePath, 'utf8');
40
+ const { data, content: markdown } = matter(contentRaw);
41
+
42
+ const stat = fs.statSync(filePath);
43
+ const outHtmlFile = relativePath.replace('.md', '.html');
44
+ const outPathFile = path.join(outdir, outHtmlFile);
45
+
46
+ // Ensure dir exists
47
+ const outDirName = path.dirname(outPathFile);
48
+ if (!fs.existsSync(outDirName)) {
49
+ fs.mkdirSync(outDirName, { recursive: true });
50
+ }
51
+
52
+ // Build HTML if needed
53
+ if (!fs.existsSync(outPathFile) || stat.mtime > fs.statSync(outPathFile).mtime) {
54
+ let html = marked.parse(markdown);
55
+ // Link transformation
56
+ html = html.replace(/href="([^"]+)"/g, (match, p1) => {
57
+ if (p1.startsWith('http') || p1.startsWith('#') || p1.startsWith('javascript:')) {
58
+ return match;
59
+ }
60
+ let targetUrl = p1;
61
+ if (!targetUrl.startsWith('/')) {
62
+ const currentRouteDir = '/' + path.dirname(relativePath).replace(/\\/g, '/');
63
+ targetUrl = path.posix.join(currentRouteDir, targetUrl);
64
+ }
65
+ return `href="javascript:lpPressLoad('${targetUrl}')"`;
66
+ });
67
+ fs.writeFileSync(outPathFile, html);
68
+ }
69
+
70
+ const tokens = marked.lexer(markdown);
71
+ const headings = [];
72
+ tokens.forEach((token) => {
73
+ if (token.type === 'heading' && (token.depth === 2 || token.depth === 3)) {
74
+ const id = slugify(token.raw);
75
+ headings.push({ level: token.depth, text: cleanMarkdown(token.text), id });
76
+ }
77
+ });
78
+
79
+ const lang = relativePath.split(/[\\/]/)[0] || 'en';
80
+
81
+ const expandSidebar = (items) => {
82
+ const result = [];
83
+ items.forEach((item) => {
84
+ if (typeof item === 'object' && item.submenu) {
85
+ let targetPath = item.submenu;
86
+ if (targetPath.startsWith('/')) targetPath = targetPath.substring(1);
87
+
88
+ // submenu points to a directory, look for index.md
89
+ const subRelative = path.join(targetPath, 'index.md');
90
+ const subAbs = path.join(indir, subRelative);
91
+
92
+ // Recursively build the submenu index
93
+ const subFileInfo = processFile(subAbs, indir, outdir, subRelative);
94
+
95
+ if (subFileInfo && subFileInfo.data && subFileInfo.data.sidebar) {
96
+ if (subFileInfo.data.title) {
97
+ result.push({ text: subFileInfo.data.title, items: subFileInfo.data.sidebar });
98
+ } else {
99
+ result.push(...subFileInfo.data.sidebar);
100
+ }
101
+ } else {
102
+ // Fallback if no sidebar found, maybe just link to it?
103
+ // Or ignoring it as per previous logic "find managed files"
104
+ result.push(item);
105
+ }
106
+ } else if (typeof item === 'string') {
107
+ // Ensure referenced file is built
108
+ let targetPath = item;
109
+ if (targetPath.startsWith('/')) targetPath = targetPath.substring(1);
110
+
111
+ // It could be a file or a dir (implying index.md)
112
+ let subAbs = path.join(indir, targetPath);
113
+ let subRelative = targetPath;
114
+
115
+ if (fs.existsSync(subAbs) && fs.statSync(subAbs).isDirectory()) {
116
+ subRelative = path.join(targetPath, 'index.md');
117
+ subAbs = path.join(indir, subRelative);
118
+ } else if (!subAbs.endsWith('.md')) {
119
+ subAbs += '.md';
120
+ subRelative += '.md';
121
+ }
122
+
123
+ processFile(subAbs, indir, outdir, subRelative);
124
+ result.push(item);
125
+ } else if (typeof item === 'object' && item.items) {
126
+ item.items = expandSidebar(item.items);
127
+ result.push(item);
128
+ } else {
129
+ result.push(item);
130
+ }
131
+ });
132
+ return result;
133
+ };
134
+
135
+ if (data.sidebar) {
136
+ data.sidebar = expandSidebar(data.sidebar);
137
+ }
138
+
139
+ const prefixLinks = (obj) => {
140
+ if (!obj || typeof obj !== 'object') return obj;
141
+ if (Array.isArray(obj)) return obj.map(prefixLinks);
142
+ const result = {};
143
+ for (const key in obj) {
144
+ let val = obj[key];
145
+ if (key === 'link' && typeof val === 'string' && val.startsWith('/') && !val.startsWith(`/${lang}/`)) {
146
+ val = `/${lang}${val}`;
147
+ }
148
+ result[key] = prefixLinks(val);
149
+ }
150
+ return result;
151
+ };
152
+ const prefixedData = prefixLinks(data);
153
+
154
+ const route = '/' + relativePath.replace(/\\/g, '/').replace(/\.md$/, '');
155
+ // const importPath = './' + relativePath.replace(/\\/g, '/').replace(/\.md$/, '.html');
156
+ const importPath = './' + path.relative(outdir, outPathFile).replace(/\\/g, '/');
157
+
158
+ const info = { route, importPath, data: prefixedData, headings };
159
+ builtFiles.set(filePath, info);
160
+ return info;
161
+ }
162
+
163
+ const markdownProcessOnEnd = async (indir, outdir) => {
164
+ try {
165
+ if (!indir || !outdir) return;
166
+
167
+ const langPath = path.join(indir, 'index.md');
168
+ if (!fs.existsSync(langPath)) {
169
+ console.warn(`[dev-markdown] No index.md found in ${indir}`);
170
+ return;
171
+ }
172
+
173
+ const langContent = fs.readFileSync(langPath, 'utf8');
174
+ const { data, content: markdown } = matter(langContent);
175
+ const lang = data.lang;
176
+ if (!lang || lang.length < 1 || !lang[0].id) {
177
+ console.warn(`[dev-markdown] No lang found in ${langPath}`);
178
+ return;
179
+ }
180
+
181
+ // Ensure outDir exists
182
+ if (!fs.existsSync(outdir)) {
183
+ fs.mkdirSync(outdir, { recursive: true });
184
+ }
185
+
186
+ builtFiles.clear();
187
+
188
+ // Process root index.md to provide global config (languages) at '/'
189
+ if (fs.existsSync(langPath)) {
190
+ const rootInfo = processFile(langPath, indir, outdir, 'index.md');
191
+ if (rootInfo) {
192
+ rootInfo.route = '/';
193
+ // Re-key as '/' to ensure it's accessible as markdownConfig['/']
194
+ builtFiles.delete(langPath);
195
+ builtFiles.set('/', rootInfo);
196
+ }
197
+ }
198
+
199
+ lang.forEach((entry) => {
200
+ const fullPath = path.join(indir, entry.id, 'index.md');
201
+ if (fs.existsSync(fullPath)) {
202
+ const relativePath = path.join(entry.id, 'index.md');
203
+ processFile(fullPath, indir, outdir, relativePath);
204
+ }
205
+ });
206
+
207
+ // 1. Build a map of route -> data to look up titles
208
+ const routeMap = new Map();
209
+ for (const info of builtFiles.values()) {
210
+ if (info && info.route) {
211
+ routeMap.set(info.route, info.data);
212
+ }
213
+ }
214
+
215
+ // 2. Flatten and enrich sidebars for top-level index pages
216
+ const flattenSidebar = (items, level) => {
217
+ const result = [];
218
+ if (!items || !Array.isArray(items)) return result;
219
+
220
+ items.forEach((item) => {
221
+ if (typeof item === 'object' && item.items && Array.isArray(item.items)) {
222
+ // Group
223
+ let newLevel = level;
224
+ if (item.text) {
225
+ result.push({
226
+ type: 'group',
227
+ text: item.text,
228
+ level: level,
229
+ });
230
+ newLevel++;
231
+ }
232
+ result.push(...flattenSidebar(item.items, newLevel));
233
+ } else if (typeof item === 'string') {
234
+ // Link
235
+ let link = item;
236
+ // Ensure link format matches route (usually starts with /)
237
+ // Look up title
238
+ const pageData = routeMap.get(link);
239
+ const text = pageData && pageData.title ? pageData.title : link;
240
+
241
+ result.push({
242
+ type: 'link',
243
+ text: text, // Enriched title
244
+ link: link,
245
+ level: level,
246
+ });
247
+ } else if (typeof item === 'object' && item.link) {
248
+ // Link object already
249
+ result.push({
250
+ type: 'link',
251
+ text: item.text,
252
+ link: item.link,
253
+ level: level,
254
+ });
255
+ }
256
+ });
257
+ return result;
258
+ };
259
+
260
+ // Apply only to index pages (or all pages? usually only index has sidebar)
261
+ // Actually we should perform this for any file that has a sidebar defined.
262
+ for (const info of builtFiles.values()) {
263
+ if (info && info.data && info.data.sidebar) {
264
+ info.data.sidebar = flattenSidebar(info.data.sidebar, 0);
265
+ }
266
+
267
+ // 3. Process nav
268
+ const resolveItem = (item) => {
269
+ let link = typeof item === 'string' ? item : item.link;
270
+ let text = typeof item === 'string' ? item : item.text;
271
+
272
+ if (link) {
273
+ // Handle directory links (e.g. /zh/guide/ -> /zh/guide/index)
274
+ // Check if the link exists in routeMap, if not check link + 'index'
275
+ if (!routeMap.has(link) && routeMap.has(link + '/index')) {
276
+ link = link + '/index';
277
+ } else if (!routeMap.has(link) && link.endsWith('/') && routeMap.has(link + 'index')) {
278
+ link = link + 'index';
279
+ }
280
+
281
+ const pageData = routeMap.get(link);
282
+ if (!text && pageData && pageData.title) {
283
+ text = pageData.title;
284
+ }
285
+ }
286
+
287
+ if (typeof item === 'string') {
288
+ return { text: text || link, link: link };
289
+ }
290
+
291
+ return { ...item, text: text || link, link: link };
292
+ };
293
+
294
+ for (const info of builtFiles.values()) {
295
+ if (info && info.data && info.data.nav && Array.isArray(info.data.nav)) {
296
+ info.data.nav = info.data.nav.map((resolveNav) => resolveItem(resolveNav));
297
+ }
298
+ }
299
+ }
300
+
301
+ const files = Array.from(builtFiles.values())
302
+ .filter((x) => x !== null)
303
+ .sort((a, b) => a.route.localeCompare(b.route));
304
+
305
+ // Generate markdown-config.ts
306
+ const configPath = path.join(outdir, 'markdown-config.ts');
307
+ let configContent = files.map((f, i) => `import html${i} from '${f.importPath}';`).join('\n');
308
+ configContent +=
309
+ '\n\nexport const markdownConfig: Record<string, { html: string; data: any; headings: any[] }> = {\n';
310
+ files.forEach((f, i) => {
311
+ configContent += ` '${f.route}': { html: html${i}, data: ${JSON.stringify(f.data)}, headings: ${JSON.stringify(
312
+ f.headings
313
+ )} },\n`;
314
+ });
315
+ configContent += '};\n';
316
+
317
+ fs.writeFileSync(configPath, configContent);
318
+ console.log(`[dev-markdown: ${indir}] Successfully built ${files.length} markdown files.`);
319
+ } catch (err) {
320
+ console.log(`[dev-markdown: ${indir}] Error:`, err);
321
+ }
322
+ };
323
+
324
+ module.exports = {
325
+ markdownProcessOnEnd,
326
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lupine.api",
3
- "version": "1.1.63",
3
+ "version": "1.1.65",
4
4
  "license": "MIT",
5
5
  "author": "uuware.com",
6
6
  "homepage": "https://github.com/uuware/lupine.js",
@@ -49,7 +49,9 @@ export const isServerSideRenderUrl = (urlWithoutQuery: string) => {
49
49
  ".htpasswd" --> ""
50
50
  "name.with.many.dots.myext" --> "myext"
51
51
  */
52
- const ext = urlWithoutQuery.slice(((urlWithoutQuery.lastIndexOf('.') - 1) >>> 0) + 2);
52
+ // get last section from /
53
+ const lastSection = urlWithoutQuery.split('/').pop() || '';
54
+ const ext = lastSection.slice(((lastSection.lastIndexOf('.') - 1) >>> 0) + 2);
53
55
  return ext === '' || ext === 'html';
54
56
  };
55
57
 
@@ -112,12 +112,12 @@ export class StaticServer {
112
112
  } catch (err: any) {
113
113
  // file doesn't exist
114
114
  if (err.code === 'ENOENT') {
115
- if (isServerSideRenderUrl(urlSplit[0])) {
116
- serverSideRenderPage(hostPath.appName, hostPath.webPath, urlSplit[0], urlSplit[1], req, res);
117
- } else {
118
- this.logger.error(`File not found: ${urlSplit[0]}`);
119
- handler404(res, `File not found: ${urlSplit[0]}`);
120
- }
115
+ // if (isServerSideRenderUrl(urlSplit[0])) {
116
+ serverSideRenderPage(hostPath.appName, hostPath.webPath, urlSplit[0], urlSplit[1], req, res);
117
+ // } else {
118
+ // this.logger.error(`File not found: ${urlSplit[0]}`);
119
+ // handler404(res, `File not found: ${urlSplit[0]}`);
120
+ // }
121
121
  } else {
122
122
  this.logger.error(`Error for: ${urlSplit[0]}`, err);
123
123
  handler500(res, `processRequest error: ${err.message}`);
@@ -1,89 +1,89 @@
1
- import { ServerResponse } from 'http';
2
- import { Db } from '../db';
3
-
4
- export const exportCSVData = async (db: Db, tableName: string, res: ServerResponse) => {
5
- res.write(`@TABLE,${tableName}\r\n`);
6
-
7
- const result = await db.selectObject(tableName);
8
- if (result && result.length > 0) {
9
- const fields = Object.keys(result[0]).join(',').toLowerCase();
10
- res.write(`@FIELD,${fields}\r\n`);
11
- result.forEach((row: any) => {
12
- res.write(`${JSON.stringify(Object.values(row))}\r\n`);
13
- });
14
- } else {
15
- res.write('#no data\r\n');
16
- }
17
- };
18
-
19
- export const exportCSVHead = (fileName: string, res: ServerResponse) => {
20
- res.writeHead(200, {
21
- 'Content-Type': 'text/csv',
22
- // 'Expires': '0',
23
- // 'Cache-Control': 'no-cache, no-store, must-revalidate',
24
- 'Content-Disposition': 'attachment; filename=' + fileName + '.ljcsv',
25
- // 'Content-Description': 'File Transfer',
26
- });
27
- };
28
-
29
- export const exportCSV = async (db: Db, tableName: string, res: ServerResponse) => {
30
- if (tableName) {
31
- exportCSVHead(tableName + '-' + new Date().toJSON().replace(/:/g, '-'), res);
32
- await exportCSVData(db, tableName, res);
33
- } else {
34
- res.writeHead(200, { 'Content-Type': 'text/html' });
35
- res.write('Need a table name.');
36
- }
37
- res.end();
38
- return true;
39
- };
40
-
41
- export const loadCSV = async (db: Db, lines: string[]) => {
42
- let table = '';
43
- let insSql = '';
44
- const result: any = {};
45
- let toFields: string[] = [];
46
- let toFieldsIndex: number[] = [];
47
- for (const i in lines) {
48
- if (!lines[i] || lines[i].startsWith('#')) {
49
- continue;
50
- }
51
- if (lines[i].startsWith('@TABLE,')) {
52
- table = lines[i].substring(7);
53
- result[table] = { succeeded: 0, failed: 0, errorMessage: [] };
54
-
55
- const toTableInfo = await db.getTableInfo(table);
56
- toFields = toTableInfo.map((item: any) => item.name);
57
- } else if (lines[i].startsWith('@FIELD,')) {
58
- const allCsvFields = lines[i].substring(7).split(',');
59
- const validFields: string[] = [];
60
- const validIndices: number[] = [];
61
-
62
- allCsvFields.forEach((field, index) => {
63
- if (toFields.includes(field)) {
64
- validFields.push(field);
65
- validIndices.push(index);
66
- }
67
- });
68
-
69
- toFieldsIndex = validIndices;
70
- const values = Array(validFields.length).fill('?').join(',');
71
- insSql = `INSERT INTO ${table} (${validFields.join(',')}) VALUES (${values})`;
72
- } else {
73
- if (toFields.length === 0 || !insSql) {
74
- throw new Error('Invalid CSV format (no @TABLE or @FIELD)');
75
- }
76
- try {
77
- const row = JSON.parse(lines[i]);
78
- const values = toFieldsIndex.map((index: number) => row[index]);
79
- await db.execute(insSql, values);
80
- result[table].succeeded++;
81
- } catch (error: any) {
82
- result[table].failed++;
83
- result[table].errorMessage.push(error.message);
84
- }
85
- }
86
- }
87
-
88
- return result;
89
- };
1
+ import { ServerResponse } from 'http';
2
+ import { Db } from '../db';
3
+
4
+ export const exportCSVData = async (db: Db, tableName: string, res: ServerResponse) => {
5
+ res.write(`@TABLE,${tableName}\r\n`);
6
+
7
+ const result = await db.selectObject(tableName);
8
+ if (result && result.length > 0) {
9
+ const fields = Object.keys(result[0]).join(',').toLowerCase();
10
+ res.write(`@FIELD,${fields}\r\n`);
11
+ result.forEach((row: any) => {
12
+ res.write(`${JSON.stringify(Object.values(row))}\r\n`);
13
+ });
14
+ } else {
15
+ res.write('#no data\r\n');
16
+ }
17
+ };
18
+
19
+ export const exportCSVHead = (fileName: string, res: ServerResponse) => {
20
+ res.writeHead(200, {
21
+ 'Content-Type': 'text/csv',
22
+ // 'Expires': '0',
23
+ // 'Cache-Control': 'no-cache, no-store, must-revalidate',
24
+ 'Content-Disposition': 'attachment; filename=' + fileName + '.ljcsv',
25
+ // 'Content-Description': 'File Transfer',
26
+ });
27
+ };
28
+
29
+ export const exportCSV = async (db: Db, tableName: string, res: ServerResponse) => {
30
+ if (tableName) {
31
+ exportCSVHead(tableName + '-' + new Date().toJSON().replace(/:/g, '-'), res);
32
+ await exportCSVData(db, tableName, res);
33
+ } else {
34
+ res.writeHead(200, { 'Content-Type': 'text/html' });
35
+ res.write('Need a table name.');
36
+ }
37
+ res.end();
38
+ return true;
39
+ };
40
+
41
+ export const loadCSV = async (db: Db, lines: string[]) => {
42
+ let table = '';
43
+ let insSql = '';
44
+ const result: any = {};
45
+ let toFields: string[] = [];
46
+ let toFieldsIndex: number[] = [];
47
+ for (const i in lines) {
48
+ if (!lines[i] || lines[i].startsWith('#')) {
49
+ continue;
50
+ }
51
+ if (lines[i].startsWith('@TABLE,')) {
52
+ table = lines[i].substring(7);
53
+ result[table] = { succeeded: 0, failed: 0, errorMessage: [] };
54
+
55
+ const toTableInfo = await db.getTableInfo(table);
56
+ toFields = toTableInfo.map((item: any) => item.name);
57
+ } else if (lines[i].startsWith('@FIELD,')) {
58
+ const allCsvFields = lines[i].substring(7).split(',');
59
+ const validFields: string[] = [];
60
+ const validIndices: number[] = [];
61
+
62
+ allCsvFields.forEach((field, index) => {
63
+ if (toFields.includes(field)) {
64
+ validFields.push(field);
65
+ validIndices.push(index);
66
+ }
67
+ });
68
+
69
+ toFieldsIndex = validIndices;
70
+ const values = Array(validFields.length).fill('?').join(',');
71
+ insSql = `INSERT INTO ${table} (${validFields.join(',')}) VALUES (${values})`;
72
+ } else {
73
+ if (toFields.length === 0 || !insSql) {
74
+ throw new Error('Invalid CSV format (no @TABLE or @FIELD)');
75
+ }
76
+ try {
77
+ const row = JSON.parse(lines[i]);
78
+ const values = toFieldsIndex.map((index: number) => row[index]);
79
+ await db.execute(insSql, values);
80
+ result[table].succeeded++;
81
+ } catch (error: any) {
82
+ result[table].failed++;
83
+ result[table].errorMessage.push(error.message);
84
+ }
85
+ }
86
+ }
87
+
88
+ return result;
89
+ };