@drax/crud-back 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/controllers/AbstractFastifyController.js +113 -44
  2. package/dist/exports/AbstractExport.js +41 -0
  3. package/dist/exports/ExportCsv.js +86 -0
  4. package/dist/exports/ExportJson.js +51 -0
  5. package/dist/repository/AbstractMongoRepository.js +11 -0
  6. package/dist/services/AbstractService.js +44 -0
  7. package/dist/workers/ExportCsvWorker.js +12 -0
  8. package/package.json +8 -6
  9. package/src/controllers/AbstractFastifyController.ts +130 -46
  10. package/src/exports/AbstractExport.ts +74 -0
  11. package/src/exports/ExportCsv.ts +107 -0
  12. package/src/exports/ExportJson.ts +64 -0
  13. package/src/index.ts +0 -3
  14. package/src/repository/AbstractMongoRepository.ts +25 -5
  15. package/src/repository/AbstractSqliteRepository.ts +1 -1
  16. package/src/services/AbstractService.ts +81 -10
  17. package/src/workers/ExportCsvWorker.js +12 -0
  18. package/src/workers/ExportCsvWorker.ts +12 -0
  19. package/test/_mocks/MockRepository.ts +58 -0
  20. package/test/exports/ExportCsv.test.ts +137 -0
  21. package/test/services/AbstractService.test.ts +40 -0
  22. package/test/workers/ExportCsvWorker.test.ts +92 -0
  23. package/tsconfig.json +1 -1
  24. package/tsconfig.tsbuildinfo +1 -1
  25. package/types/controllers/AbstractFastifyController.d.ts +19 -7
  26. package/types/controllers/AbstractFastifyController.d.ts.map +1 -1
  27. package/types/exports/AbstractExport.d.ts +23 -0
  28. package/types/exports/AbstractExport.d.ts.map +1 -0
  29. package/types/exports/ExportCsv.d.ts +13 -0
  30. package/types/exports/ExportCsv.d.ts.map +1 -0
  31. package/types/exports/ExportFsCsv.d.ts +17 -0
  32. package/types/exports/ExportFsCsv.d.ts.map +1 -0
  33. package/types/exports/ExportJson.d.ts +8 -0
  34. package/types/exports/ExportJson.d.ts.map +1 -0
  35. package/types/index.d.ts +0 -2
  36. package/types/index.d.ts.map +1 -1
  37. package/types/interfaces/ICrudRepository.d.ts +2 -1
  38. package/types/interfaces/ICrudRepository.d.ts.map +1 -1
  39. package/types/repository/AbstractMongoRepository.d.ts +3 -3
  40. package/types/repository/AbstractMongoRepository.d.ts.map +1 -1
  41. package/types/repository/AbstractSqliteRepository.d.ts +1 -1
  42. package/types/repository/AbstractSqliteRepository.d.ts.map +1 -1
  43. package/types/services/AbstractService.d.ts +8 -5
  44. package/types/services/AbstractService.d.ts.map +1 -1
  45. package/types/workers/ExportCsvWorker.d.ts +2 -0
  46. package/types/workers/ExportCsvWorker.d.ts.map +1 -0
  47. package/src/interfaces/ICrudRepository.ts +0 -16
  48. package/src/interfaces/IEntityPermission.ts +0 -10
@@ -1,20 +1,65 @@
1
- import { ValidationError } from "@drax/common-back";
1
+ import { CommonConfig, DraxConfig, ValidationError } from "@drax/common-back";
2
2
  import { UnauthorizedError } from "@drax/identity-back";
3
+ import { join } from "path";
4
+ const BASE_FILE_DIR = DraxConfig.getOrLoad(CommonConfig.FileDir) || 'files';
5
+ const BASE_URL = DraxConfig.getOrLoad(CommonConfig.BaseUrl) ? DraxConfig.get(CommonConfig.BaseUrl).replace(/\/$/, '') : '';
3
6
  class AbstractFastifyController {
4
7
  constructor(service, permission) {
5
8
  this.service = service;
6
9
  this.permission = permission;
7
10
  console.log("AbstractFastifyController created. Permissions", this.permission);
8
11
  }
9
- async findById(request, reply) {
12
+ parseFilters(stringFilters) {
10
13
  try {
11
- request.rbac.assertPermission(this.permission.View);
14
+ if (!stringFilters) {
15
+ return {};
16
+ }
17
+ const filterArray = stringFilters.split(";");
18
+ const filters = [];
19
+ filterArray.forEach((filter) => {
20
+ const [field, operator, value] = filter.split(",");
21
+ filters.push({ field, operator, value });
22
+ });
23
+ return filters;
24
+ }
25
+ catch (e) {
26
+ console.error(e);
27
+ throw e;
28
+ }
29
+ }
30
+ async create(request, reply) {
31
+ try {
32
+ request.rbac.assertPermission(this.permission.Create);
33
+ const payload = request.body;
34
+ let item = await this.service.create(payload);
35
+ return item;
36
+ }
37
+ catch (e) {
38
+ console.error(e);
39
+ if (e instanceof ValidationError) {
40
+ reply.statusCode = e.statusCode;
41
+ reply.send({ error: e.message, inputErrors: e.errors });
42
+ }
43
+ else if (e instanceof UnauthorizedError) {
44
+ reply.statusCode = e.statusCode;
45
+ reply.send({ error: e.message });
46
+ }
47
+ else {
48
+ reply.statusCode = 500;
49
+ reply.send({ error: 'INTERNAL_SERVER_ERROR' });
50
+ }
51
+ }
52
+ }
53
+ async update(request, reply) {
54
+ try {
55
+ request.rbac.assertPermission(this.permission.Update);
12
56
  if (!request.params.id) {
13
57
  reply.statusCode = 400;
14
58
  reply.send({ error: 'BAD REQUEST' });
15
59
  }
16
60
  const id = request.params.id;
17
- let item = await this.service.findById(id);
61
+ const payload = request.body;
62
+ let item = await this.service.update(id, payload);
18
63
  return item;
19
64
  }
20
65
  catch (e) {
@@ -33,16 +78,16 @@ class AbstractFastifyController {
33
78
  }
34
79
  }
35
80
  }
36
- async findByIds(request, reply) {
81
+ async delete(request, reply) {
37
82
  try {
38
- request.rbac.assertPermission(this.permission.View);
39
- if (!request.params.ids) {
83
+ request.rbac.assertPermission(this.permission.Delete);
84
+ if (!request.params.id) {
40
85
  reply.statusCode = 400;
41
86
  reply.send({ error: 'BAD REQUEST' });
42
87
  }
43
- const ids = request.params.ids.split(",");
44
- let items = await this.service.findByIds(ids);
45
- return items;
88
+ const id = request.params.id;
89
+ await this.service.delete(id);
90
+ reply.send({ message: 'Item deleted successfully' });
46
91
  }
47
92
  catch (e) {
48
93
  console.error(e);
@@ -60,11 +105,15 @@ class AbstractFastifyController {
60
105
  }
61
106
  }
62
107
  }
63
- async search(request, reply) {
108
+ async findById(request, reply) {
64
109
  try {
65
110
  request.rbac.assertPermission(this.permission.View);
66
- const search = request.query.search;
67
- let item = await this.service.search(search);
111
+ if (!request.params.id) {
112
+ reply.statusCode = 400;
113
+ reply.send({ error: 'BAD REQUEST' });
114
+ }
115
+ const id = request.params.id;
116
+ let item = await this.service.findById(id);
68
117
  return item;
69
118
  }
70
119
  catch (e) {
@@ -83,16 +132,16 @@ class AbstractFastifyController {
83
132
  }
84
133
  }
85
134
  }
86
- async paginate(request, reply) {
135
+ async findByIds(request, reply) {
87
136
  try {
88
137
  request.rbac.assertPermission(this.permission.View);
89
- const page = request.query.page;
90
- const limit = request.query.limit;
91
- const orderBy = request.query.orderBy;
92
- const order = request.query.order;
93
- const search = request.query.search;
94
- let paginateResult = await this.service.paginate({ page, limit, orderBy, order, search });
95
- return paginateResult;
138
+ if (!request.params.ids) {
139
+ reply.statusCode = 400;
140
+ reply.send({ error: 'BAD REQUEST' });
141
+ }
142
+ const ids = request.params.ids.split(",");
143
+ let items = await this.service.findByIds(ids);
144
+ return items;
96
145
  }
97
146
  catch (e) {
98
147
  console.error(e);
@@ -110,11 +159,11 @@ class AbstractFastifyController {
110
159
  }
111
160
  }
112
161
  }
113
- async create(request, reply) {
162
+ async search(request, reply) {
114
163
  try {
115
- request.rbac.assertPermission(this.permission.Create);
116
- const payload = request.body;
117
- let item = await this.service.create(payload);
164
+ request.rbac.assertPermission(this.permission.View);
165
+ const search = request.query.search;
166
+ let item = await this.service.search(search);
118
167
  return item;
119
168
  }
120
169
  catch (e) {
@@ -133,17 +182,17 @@ class AbstractFastifyController {
133
182
  }
134
183
  }
135
184
  }
136
- async update(request, reply) {
185
+ async paginate(request, reply) {
137
186
  try {
138
- request.rbac.assertPermission(this.permission.Update);
139
- if (!request.params.id) {
140
- reply.statusCode = 400;
141
- reply.send({ error: 'BAD REQUEST' });
142
- }
143
- const id = request.params.id;
144
- const payload = request.body;
145
- let item = await this.service.update(id, payload);
146
- return item;
187
+ request.rbac.assertPermission(this.permission.View);
188
+ const page = request.query.page;
189
+ const limit = request.query.limit;
190
+ const orderBy = request.query.orderBy;
191
+ const order = request.query.order;
192
+ const search = request.query.search;
193
+ //const filters = this.parseFilters(request.query.filters)
194
+ let paginateResult = await this.service.paginate({ page, limit, orderBy, order, search });
195
+ return paginateResult;
147
196
  }
148
197
  catch (e) {
149
198
  console.error(e);
@@ -161,16 +210,36 @@ class AbstractFastifyController {
161
210
  }
162
211
  }
163
212
  }
164
- async delete(request, reply) {
213
+ async export(request, reply) {
165
214
  try {
166
- request.rbac.assertPermission(this.permission.Delete);
167
- if (!request.params.id) {
168
- reply.statusCode = 400;
169
- reply.send({ error: 'BAD REQUEST' });
170
- }
171
- const id = request.params.id;
172
- await this.service.delete(id);
173
- reply.send({ message: 'Item deleted successfully' });
215
+ request.rbac.assertPermission(this.permission.View);
216
+ const format = request.query.format || 'JSON';
217
+ const headers = request.query.headers ? request.query.headers.split(",") : [];
218
+ const separator = request.query.separator || ";";
219
+ const limit = request.query.limit;
220
+ const orderBy = request.query.orderBy;
221
+ const order = request.query.order;
222
+ const search = request.query.search;
223
+ const year = (new Date().getFullYear()).toString();
224
+ const month = (new Date().getMonth() + 1).toString().padStart(2, '0');
225
+ const exportPath = 'exports';
226
+ const destinationPath = join(BASE_FILE_DIR, 'exports', year, month);
227
+ let result = await this.service.export({
228
+ separator,
229
+ format,
230
+ headers,
231
+ limit,
232
+ orderBy,
233
+ order,
234
+ search,
235
+ }, destinationPath);
236
+ const url = `${BASE_URL}/api/file/${exportPath}/${year}/${month}/${result.fileName}`;
237
+ return {
238
+ url: url,
239
+ rowCount: result.rowCount,
240
+ time: result.time,
241
+ fileName: result.fileName,
242
+ };
174
243
  }
175
244
  catch (e) {
176
245
  console.error(e);
@@ -0,0 +1,41 @@
1
+ import { createDirIfNotExist } from "@drax/common-back";
2
+ import crypto from "crypto";
3
+ class AbstractExport {
4
+ constructor(options) {
5
+ this.cursor = options.cursor;
6
+ this.destinationPath = options.destinationPath;
7
+ this.headers = Array.isArray(options.headers) ? options.headers : options.headers.split(',');
8
+ }
9
+ createDirIfNotExist() {
10
+ createDirIfNotExist(this.destinationPath);
11
+ }
12
+ generateFileName(extension) {
13
+ if (!this.fileName) {
14
+ const randomUUID = crypto.randomUUID().toString();
15
+ this.fileName = `export_${randomUUID}.${extension}`;
16
+ }
17
+ return this.fileName;
18
+ }
19
+ generateFilePath(extension) {
20
+ this.createDirIfNotExist();
21
+ this.generateFileName(extension);
22
+ this.relativeFilePath = `${this.destinationPath}/${this.fileName}`;
23
+ return this.relativeFilePath;
24
+ }
25
+ isIterableAsync(value) {
26
+ return value != null && typeof value[Symbol.asyncIterator] === 'function';
27
+ }
28
+ isIterableSync(value) {
29
+ return value != null && typeof value[Symbol.iterator] === 'function';
30
+ }
31
+ getNestedProperty(obj, path) {
32
+ return path.split('.').reduce((acc, part) => {
33
+ if (Array.isArray(acc)) {
34
+ return acc.map(item => item[part]).join('|');
35
+ }
36
+ return acc && acc[part];
37
+ }, obj);
38
+ }
39
+ }
40
+ export { AbstractExport, };
41
+ export default AbstractExport;
@@ -0,0 +1,86 @@
1
+ import * as fs from 'fs';
2
+ import AbstractExport from "./AbstractExport.js";
3
+ class ExportCsv extends AbstractExport {
4
+ constructor(options) {
5
+ super(options);
6
+ this.separator = ';';
7
+ this.separator = options.separator ? options.separator : ';';
8
+ }
9
+ // Método principal para procesar los datos y generar el CSV
10
+ process() {
11
+ return new Promise(async (resolve, reject) => {
12
+ try {
13
+ this.generateFilePath('csv');
14
+ let rowCount = 0;
15
+ const start = Date.now();
16
+ const writableStream = fs.createWriteStream(this.relativeFilePath);
17
+ writableStream.on('error', reject);
18
+ writableStream.on('finish', () => resolve({
19
+ status: 'success',
20
+ destinationPath: this.destinationPath,
21
+ fileName: this.fileName,
22
+ filePath: this.destinationPath + '/' + this.fileName,
23
+ relativeFilePath: this.relativeFilePath,
24
+ rowCount: rowCount,
25
+ time: Date.now() - start,
26
+ message: 'Export successful',
27
+ }));
28
+ const csvHeaders = this.headers.join(this.separator);
29
+ writableStream.write(csvHeaders + '\n');
30
+ if (this.isIterableAsync(this.cursor)) {
31
+ for await (const record of this.cursor) {
32
+ const csvRow = this.convertRecordToCSVrow(record);
33
+ console.log("csvRow", csvRow);
34
+ writableStream.write(csvRow + '\n');
35
+ rowCount++;
36
+ }
37
+ }
38
+ else if (this.isIterableSync(this.cursor)) {
39
+ // Si es un cursor de SQLite (better-sqlite3), usamos iterate()
40
+ for (const record of this.cursor) {
41
+ const csvRow = this.convertRecordToCSVrow(record);
42
+ writableStream.write(csvRow + '\n');
43
+ rowCount++;
44
+ }
45
+ }
46
+ writableStream.end();
47
+ }
48
+ catch (e) {
49
+ reject(e);
50
+ }
51
+ });
52
+ }
53
+ // Método que convierte un registro en una o más filas de CSV
54
+ convertRecordToCSVrow(record) {
55
+ let fields = [];
56
+ for (const header of this.headers) {
57
+ let value;
58
+ if (header.includes('.')) {
59
+ value = this.getNestedProperty(record, header);
60
+ }
61
+ else {
62
+ value = record[header];
63
+ }
64
+ if (value === undefined) {
65
+ fields.push('');
66
+ continue;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ if (value.length > 0 && typeof value[0] === 'object') {
70
+ fields.push(JSON.stringify(value));
71
+ }
72
+ else {
73
+ fields.push(value.join(','));
74
+ }
75
+ }
76
+ else if (typeof value === 'object') {
77
+ fields.push(JSON.stringify(value));
78
+ }
79
+ else {
80
+ fields.push(value.toString());
81
+ }
82
+ }
83
+ return fields.join(this.separator);
84
+ }
85
+ }
86
+ export default ExportCsv;
@@ -0,0 +1,51 @@
1
+ import * as fs from 'fs';
2
+ import AbstractExport from "./AbstractExport.js";
3
+ class ExportJson extends AbstractExport {
4
+ constructor(options) {
5
+ super(options);
6
+ }
7
+ // Método principal para procesar los datos y generar el CSV
8
+ process() {
9
+ return new Promise(async (resolve, reject) => {
10
+ try {
11
+ this.generateFilePath('json');
12
+ let rowCount = 0;
13
+ const start = Date.now();
14
+ const writableStream = fs.createWriteStream(this.relativeFilePath);
15
+ writableStream.on('error', reject);
16
+ writableStream.on('finish', () => resolve({
17
+ status: 'success',
18
+ destinationPath: this.destinationPath,
19
+ fileName: this.fileName,
20
+ filePath: this.destinationPath + '/' + this.fileName,
21
+ relativeFilePath: this.relativeFilePath,
22
+ rowCount: rowCount,
23
+ time: Date.now() - start,
24
+ message: 'Export successful',
25
+ }));
26
+ writableStream.write('[');
27
+ if (this.isIterableAsync(this.cursor)) {
28
+ for await (const record of this.cursor) {
29
+ const csvRow = JSON.stringify(record);
30
+ console.log("csvRow", csvRow);
31
+ writableStream.write(csvRow + ',\n');
32
+ rowCount++;
33
+ }
34
+ }
35
+ else if (this.isIterableSync(this.cursor)) {
36
+ for (const record of this.cursor) {
37
+ const csvRow = JSON.stringify(record);
38
+ writableStream.write(csvRow + ',\n');
39
+ rowCount++;
40
+ }
41
+ }
42
+ writableStream.write(']');
43
+ writableStream.end();
44
+ }
45
+ catch (e) {
46
+ reject(e);
47
+ }
48
+ });
49
+ }
50
+ }
51
+ export default ExportJson;
@@ -82,6 +82,17 @@ class AbstractMongoRepository {
82
82
  items: items.docs
83
83
  };
84
84
  }
85
+ async find({ limit = 0, orderBy = '', order = false, search = '', filters = [] }) {
86
+ const query = {};
87
+ if (search) {
88
+ query['$or'] = this._searchFields.map(field => ({ [field]: new RegExp(search, 'i') }));
89
+ }
90
+ MongooseQueryFilter.applyFilters(query, filters);
91
+ const sort = MongooseSort.applySort(orderBy, order);
92
+ const populate = this._populateFields;
93
+ const items = await this._model.find(query).sort(sort).populate(populate);
94
+ return items;
95
+ }
85
96
  }
86
97
  export default AbstractMongoRepository;
87
98
  export { AbstractMongoRepository };
@@ -1,5 +1,7 @@
1
1
  import { ZodErrorToValidationError } from "@drax/common-back";
2
2
  import { ZodError } from "zod";
3
+ import ExportCsv from "../exports/ExportCsv.js";
4
+ import ExportJson from "../exports/ExportJson.js";
3
5
  class AbstractService {
4
6
  constructor(repository, schema) {
5
7
  this._repository = repository;
@@ -117,6 +119,48 @@ class AbstractService {
117
119
  throw e;
118
120
  }
119
121
  }
122
+ async find({ orderBy = '', order = false, search = '', filters = [] }) {
123
+ try {
124
+ const items = await this._repository.find({ orderBy, order, search, filters });
125
+ return items;
126
+ }
127
+ catch (e) {
128
+ console.error("Error paginating", e);
129
+ throw e;
130
+ }
131
+ }
132
+ async export({ format = 'JSON', headers = [], separator = ';', orderBy = '', order = false, search = '', filters = [] }, destinationPath) {
133
+ try {
134
+ console.log("ExportOptions", {
135
+ format,
136
+ headers,
137
+ separator,
138
+ outputPath: destinationPath,
139
+ orderBy,
140
+ order,
141
+ search,
142
+ filters
143
+ });
144
+ let cursor;
145
+ let exporter;
146
+ switch (format) {
147
+ case 'JSON':
148
+ cursor = await this._repository.find({ orderBy, order, search, filters });
149
+ exporter = new ExportJson({ cursor, destinationPath: destinationPath, headers });
150
+ return await exporter.process();
151
+ case 'CSV':
152
+ cursor = await this._repository.find({ orderBy, order, search, filters });
153
+ exporter = new ExportCsv({ cursor, destinationPath: destinationPath, headers, separator });
154
+ return await exporter.process();
155
+ default:
156
+ throw new Error(`Unsupported export format: ${format}`);
157
+ }
158
+ }
159
+ catch (e) {
160
+ console.error("Error exporting", e);
161
+ throw e;
162
+ }
163
+ }
120
164
  }
121
165
  export default AbstractService;
122
166
  export { AbstractService };
@@ -0,0 +1,12 @@
1
+ import { parentPort } from 'worker_threads';
2
+ import ExportCsv from '../exports/ExportCsv'; // Ajusta la ruta según sea necesario
3
+ parentPort.on('message', async (options) => {
4
+ const exporter = new ExportCsv(options);
5
+ try {
6
+ let result = await exporter.process();
7
+ parentPort.postMessage({ result: result });
8
+ }
9
+ catch (error) {
10
+ parentPort.postMessage({ error: error.message });
11
+ }
12
+ });
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.0",
6
+ "version": "0.5.1",
7
7
  "description": "Crud utils across modules",
8
8
  "main": "dist/index.js",
9
9
  "types": "types/index.d.ts",
@@ -15,14 +15,16 @@
15
15
  "copygql": "copyfiles -u 1 ./**/*.graphql dist/",
16
16
  "tsc": "tsc -b tsconfig.json",
17
17
  "test": "node --import tsx --test test/**/*",
18
- "testCache": "node --import tsx --test test/cache/*",
19
- "testMongoose": "node --import tsx --test test/mongoose/*"
18
+ "testService": "node --import tsx --test test/services/*.ts",
19
+ "testExports": "node --import tsx --test test/exports/*.ts",
20
+ "testWorkers": "node --import tsx --test test/workers/*.ts"
20
21
  },
21
22
  "author": "Cristian Incarnato & Drax Team",
22
23
  "license": "ISC",
23
24
  "dependencies": {
24
- "@drax/common-back": "^0.4.0",
25
- "@drax/common-share": "^0.4.0",
25
+ "@drax/common-back": "^0.5.1",
26
+ "@drax/common-share": "^0.5.1",
27
+ "@drax/media-back": "^0.5.1",
26
28
  "@graphql-tools/load-files": "^7.0.0",
27
29
  "@graphql-tools/merge": "^9.0.4",
28
30
  "mongoose": "^8.6.3",
@@ -41,5 +43,5 @@
41
43
  "tsc-alias": "^1.8.10",
42
44
  "typescript": "^5.6.2"
43
45
  },
44
- "gitHead": "481b302fe72f403abf092806ceca540dd2765dfa"
46
+ "gitHead": "6f507c8b52e134f10839fd7698c2466ff65a3d08"
45
47
  }