@appweaver/data 0.0.0-stage → 2.0.0
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/LICENSE +21 -0
- package/README.md +61 -2
- package/config.d.ts +6 -0
- package/config.js +22 -0
- package/database/create-client.d.ts +11 -0
- package/database/create-client.js +145 -0
- package/database/database-config.d.ts +14 -0
- package/database/database-config.js +40 -0
- package/database/database-error.d.ts +49 -0
- package/database/database-error.js +216 -0
- package/database/index.d.ts +3 -0
- package/database/index.js +19 -0
- package/database/prisma-database.d.ts +33 -0
- package/database/prisma-database.js +178 -0
- package/database/run-transaction.d.ts +21 -0
- package/database/run-transaction.js +15 -0
- package/export/export-config.d.ts +7 -0
- package/export/export-config.js +17 -0
- package/export/export-service.d.ts +22 -0
- package/export/export-service.js +188 -0
- package/export/index.d.ts +1 -0
- package/export/index.js +17 -0
- package/factory/create-model.d.ts +11 -0
- package/factory/create-model.js +479 -0
- package/factory/create-policy.d.ts +12 -0
- package/factory/create-policy.js +20 -0
- package/factory/create-service.d.ts +14 -0
- package/factory/create-service.js +116 -0
- package/index.d.ts +22 -0
- package/index.js +38 -0
- package/internal.d.ts +2 -0
- package/internal.js +6 -0
- package/module.d.ts +3 -0
- package/module.js +81 -0
- package/package.json +67 -3
- package/prisma/client/browser.d.ts +39 -0
- package/prisma/client/browser.js +56 -0
- package/prisma/client/client.d.ts +58 -0
- package/prisma/client/client.js +70 -0
- package/prisma/client/commonInputTypes.d.ts +345 -0
- package/prisma/client/commonInputTypes.js +11 -0
- package/prisma/client/enums.d.ts +1 -0
- package/prisma/client/enums.js +11 -0
- package/prisma/client/internal/class.d.ts +198 -0
- package/prisma/client/internal/class.js +85 -0
- package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
- package/prisma/client/internal/prismaNamespace.js +204 -0
- package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
- package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
- package/prisma/client/models/ApiKey.d.ts +1167 -0
- package/prisma/client/models/ApiKey.js +2 -0
- package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
- package/prisma/client/models/ConnectedAccount.js +2 -0
- package/prisma/client/models/File.d.ts +1291 -0
- package/prisma/client/models/File.js +2 -0
- package/prisma/client/models/OneTimeToken.d.ts +1055 -0
- package/prisma/client/models/OneTimeToken.js +2 -0
- package/prisma/client/models/Permission.d.ts +1209 -0
- package/prisma/client/models/Permission.js +2 -0
- package/prisma/client/models/Role.d.ts +1209 -0
- package/prisma/client/models/Role.js +2 -0
- package/prisma/client/models/Seeder.d.ts +1023 -0
- package/prisma/client/models/Seeder.js +2 -0
- package/prisma/client/models.d.ts +8 -0
- package/prisma/client/models.js +2 -0
- package/resource/load-data-resources.d.ts +13 -0
- package/resource/load-data-resources.js +37 -0
- package/resource/model-schema.d.ts +34 -0
- package/resource/model-schema.js +54 -0
- package/resource/resource-config.d.ts +16 -0
- package/resource/resource-config.js +62 -0
- package/resource/resource-loader.d.ts +25 -0
- package/resource/resource-loader.js +138 -0
- package/resource/resource-service.d.ts +462 -0
- package/resource/resource-service.js +1042 -0
- package/resource/schemas/index.d.ts +3 -0
- package/resource/schemas/index.js +19 -0
- package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
- package/resource/schemas/resource-aggregate-schema.js +130 -0
- package/resource/schemas/resource-filter-schema.d.ts +39 -0
- package/resource/schemas/resource-filter-schema.js +153 -0
- package/resource/schemas/resource-sort-schema.d.ts +38 -0
- package/resource/schemas/resource-sort-schema.js +113 -0
- package/resource/utils/aggregate-util.d.ts +113 -0
- package/resource/utils/aggregate-util.js +322 -0
- package/resource/utils/cursor-util.d.ts +60 -0
- package/resource/utils/cursor-util.js +116 -0
- package/resource/utils/date-util.d.ts +22 -0
- package/resource/utils/date-util.js +112 -0
- package/resource/utils/delete-util.d.ts +131 -0
- package/resource/utils/delete-util.js +385 -0
- package/resource/utils/filter-util.d.ts +24 -0
- package/resource/utils/filter-util.js +289 -0
- package/resource/utils/prune-util.d.ts +11 -0
- package/resource/utils/prune-util.js +56 -0
- package/resource/utils/relation-util.d.ts +75 -0
- package/resource/utils/relation-util.js +400 -0
- package/resource/utils/sort-util.d.ts +31 -0
- package/resource/utils/sort-util.js +235 -0
- package/resource/utils/text-search-util.d.ts +14 -0
- package/resource/utils/text-search-util.js +39 -0
- package/resources.d.ts +10 -0
- package/resources.js +26 -0
- package/security/auth-config.d.ts +23 -0
- package/security/auth-config.js +43 -0
- package/security/create-auth-resources.d.ts +28 -0
- package/security/create-auth-resources.js +163 -0
- package/security/helper.d.ts +109 -0
- package/security/helper.js +298 -0
- package/security/resources/api-key/model.d.ts +2 -0
- package/security/resources/api-key/model.js +67 -0
- package/security/resources/api-key/policy.d.ts +2 -0
- package/security/resources/api-key/policy.js +27 -0
- package/security/resources/api-key/service.d.ts +3 -0
- package/security/resources/api-key/service.js +34 -0
- package/security/resources/connected-account/model.d.ts +2 -0
- package/security/resources/connected-account/model.js +57 -0
- package/security/resources/connected-account/service.d.ts +2 -0
- package/security/resources/connected-account/service.js +7 -0
- package/security/resources/one-time-token/model.d.ts +2 -0
- package/security/resources/one-time-token/model.js +36 -0
- package/security/resources/permission/model.d.ts +2 -0
- package/security/resources/permission/model.js +16 -0
- package/security/resources/role/model.d.ts +2 -0
- package/security/resources/role/model.js +22 -0
- package/security/store/database-security-store.d.ts +5 -0
- package/security/store/database-security-store.js +54 -0
- package/seeder/create-seeder.d.ts +29 -0
- package/seeder/create-seeder.js +28 -0
- package/seeder/index.d.ts +2 -0
- package/seeder/index.js +18 -0
- package/seeder/resources/seeder/model.d.ts +2 -0
- package/seeder/resources/seeder/model.js +38 -0
- package/seeder/seeder.d.ts +29 -0
- package/seeder/seeder.js +197 -0
- package/storage/file-service.d.ts +133 -0
- package/storage/file-service.js +586 -0
- package/storage/image-config.d.ts +3 -0
- package/storage/image-config.js +11 -0
- package/storage/resources/file/model.d.ts +2 -0
- package/storage/resources/file/model.js +80 -0
- package/types/auth.d.ts +23 -0
- package/types/auth.js +2 -0
- package/types/generated.d.ts +366 -0
- package/types/generated.js +3 -0
- package/types/storage.d.ts +35 -0
- package/types/storage.js +2 -0
- package/utils/export-util.d.ts +8 -0
- package/utils/export-util.js +26 -0
- package/utils/file-util.d.ts +106 -0
- package/utils/file-util.js +254 -0
- package/utils/model-util.d.ts +36 -0
- package/utils/model-util.js +154 -0
- package/utils/policy-util.d.ts +18 -0
- package/utils/policy-util.js +28 -0
- package/utils/virtual-util.d.ts +11 -0
- package/utils/virtual-util.js +53 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrismaDatabase = void 0;
|
|
4
|
+
const common_1 = require("@appweaver/common");
|
|
5
|
+
const database_config_1 = require("./database-config");
|
|
6
|
+
const create_client_1 = require("./create-client");
|
|
7
|
+
const internal_1 = require("@appweaver/core/internal");
|
|
8
|
+
// Members that act on the connection itself, never on a transaction
|
|
9
|
+
const ROOT_MEMBERS = new Set([
|
|
10
|
+
'$connect',
|
|
11
|
+
'$disconnect',
|
|
12
|
+
'$on',
|
|
13
|
+
'$extends'
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Represents a database utility class that provides methods
|
|
17
|
+
* to connect to and disconnect from the database, as well as
|
|
18
|
+
* access to the database Prisma client instance.
|
|
19
|
+
*/
|
|
20
|
+
class PrismaDatabase extends common_1.Database {
|
|
21
|
+
/** @internal */
|
|
22
|
+
_client;
|
|
23
|
+
/** @internal */
|
|
24
|
+
_proxy;
|
|
25
|
+
/**
|
|
26
|
+
* Connects to the database and checks the connection, logging an error when the database is unreachable instead of
|
|
27
|
+
* failing the start, since most drivers connect lazily and the queries reconnect once it is reachable.
|
|
28
|
+
*/
|
|
29
|
+
async onInit() {
|
|
30
|
+
try {
|
|
31
|
+
await this.connect();
|
|
32
|
+
await this.clientInstance().$queryRaw `SELECT 1`;
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
const dbType = (0, common_1.resolveDatabaseType)(database_config_1.databaseConfig.TYPE, database_config_1.databaseConfig.URL);
|
|
36
|
+
const url = this.redactUrl(database_config_1.databaseConfig.URL);
|
|
37
|
+
common_1.logger.error(error, `Cannot connect to the ${dbType} database${url ? ` at ${url}` : ''}`);
|
|
38
|
+
// The next query creates a new client, which detects the capabilities of the database again
|
|
39
|
+
await this.disconnect().catch(() => undefined);
|
|
40
|
+
this._client = undefined;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async onDestroy() {
|
|
44
|
+
await this.disconnect();
|
|
45
|
+
}
|
|
46
|
+
async connect() {
|
|
47
|
+
if (this._client) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
await this.clientInstance().$connect();
|
|
51
|
+
}
|
|
52
|
+
async disconnect() {
|
|
53
|
+
if (!this._client) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await this._client.$disconnect();
|
|
57
|
+
this._client = undefined;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Returns the Prisma client, which runs its queries in the transaction the
|
|
61
|
+
* current call runs in (see `runTransaction`), and on the connection otherwise.
|
|
62
|
+
*/
|
|
63
|
+
client() {
|
|
64
|
+
this._proxy ??= this.transactionAwareClient();
|
|
65
|
+
return this._proxy;
|
|
66
|
+
}
|
|
67
|
+
async checkHealth() {
|
|
68
|
+
try {
|
|
69
|
+
await this.client().$queryRaw `SELECT 1`;
|
|
70
|
+
return { success: true };
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return { success: false, message: e.message };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Runs the given function in a transaction, or in the current one when called
|
|
78
|
+
* inside a transaction already. See `runTransaction`.
|
|
79
|
+
*
|
|
80
|
+
* @param {Function} fn - The function to run, receiving the transaction client.
|
|
81
|
+
* @param {TransactionOptions} [options] - The transaction options.
|
|
82
|
+
* @return {Promise<Object>} The value returned by the function.
|
|
83
|
+
* @throws The error thrown by the function, after the transaction rolled back.
|
|
84
|
+
*/
|
|
85
|
+
async transaction(fn, options = {}) {
|
|
86
|
+
const isolationLevel = this.isolationLevel(options.isolationLevel);
|
|
87
|
+
const current = (0, internal_1.currentTransaction)();
|
|
88
|
+
if (current) {
|
|
89
|
+
if (options.isolationLevel && current.isolationLevel !== isolationLevel) {
|
|
90
|
+
throw new common_1.DatabaseError(common_1.ErrorCode.DatabaseInvalidTransaction, `Cannot run a ${isolationLevel} transaction inside a ` +
|
|
91
|
+
`${current.isolationLevel ?? 'default'} isolation level transaction`);
|
|
92
|
+
}
|
|
93
|
+
return fn(current.tx);
|
|
94
|
+
}
|
|
95
|
+
for (let attempt = 0;; attempt++) {
|
|
96
|
+
const context = {
|
|
97
|
+
tx: undefined,
|
|
98
|
+
commitCallbacks: new Map(),
|
|
99
|
+
rollbackCallbacks: [],
|
|
100
|
+
isolationLevel
|
|
101
|
+
};
|
|
102
|
+
let result;
|
|
103
|
+
try {
|
|
104
|
+
result = await this.clientInstance().$transaction((tx) => {
|
|
105
|
+
context.tx = tx;
|
|
106
|
+
return (0, internal_1.runInTransaction)(context, () => fn(tx));
|
|
107
|
+
}, { isolationLevel, timeout: options.timeout, maxWait: options.maxWait });
|
|
108
|
+
}
|
|
109
|
+
catch (e) {
|
|
110
|
+
await (0, internal_1.settleTransaction)(context, false);
|
|
111
|
+
if (attempt < (options.retries ?? 0) && this.isWriteConflict(e)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
await (0, internal_1.settleTransaction)(context, true);
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/** @internal */
|
|
121
|
+
clientInstance() {
|
|
122
|
+
if (!this._client) {
|
|
123
|
+
this._client = (0, create_client_1.createClient)();
|
|
124
|
+
}
|
|
125
|
+
return this._client;
|
|
126
|
+
}
|
|
127
|
+
/** @internal */
|
|
128
|
+
isolationLevel(level) {
|
|
129
|
+
// SQLite runs every transaction serializable and rejects the other levels
|
|
130
|
+
const dbType = (0, common_1.resolveDatabaseType)(database_config_1.databaseConfig.TYPE, database_config_1.databaseConfig.URL);
|
|
131
|
+
if (dbType === common_1.DatabaseType.Sqlite) {
|
|
132
|
+
return common_1.IsolationLevel.Serializable;
|
|
133
|
+
}
|
|
134
|
+
return level;
|
|
135
|
+
}
|
|
136
|
+
/** @internal */
|
|
137
|
+
transactionAwareClient() {
|
|
138
|
+
return new Proxy({}, {
|
|
139
|
+
get: (_target, property) => {
|
|
140
|
+
const root = this.clientInstance();
|
|
141
|
+
const context = (0, internal_1.currentTransaction)();
|
|
142
|
+
if (property === '$transaction') {
|
|
143
|
+
return (arg, options) => this.joinTransaction(root, context, arg, options);
|
|
144
|
+
}
|
|
145
|
+
const source = context && !ROOT_MEMBERS.has(property) ? context.tx : root;
|
|
146
|
+
const value = Reflect.get(source, property);
|
|
147
|
+
return typeof value === 'function' ? value.bind(source) : value;
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** @internal */
|
|
152
|
+
joinTransaction(root, context, arg, options) {
|
|
153
|
+
if (typeof arg === 'function') {
|
|
154
|
+
return this.transaction(arg, options);
|
|
155
|
+
}
|
|
156
|
+
// The batch form, whose queries were created on the current transaction
|
|
157
|
+
return context
|
|
158
|
+
? Promise.all(arg)
|
|
159
|
+
: root.$transaction(arg, options);
|
|
160
|
+
}
|
|
161
|
+
/** @internal */
|
|
162
|
+
redactUrl(url) {
|
|
163
|
+
return url
|
|
164
|
+
.replace(/(\/\/[^:/@]*):[^@/]*@/, '$1:********@')
|
|
165
|
+
.replace(/((?:password|pwd)=)[^;&]*/gi, '$1********');
|
|
166
|
+
}
|
|
167
|
+
/** @internal */
|
|
168
|
+
isWriteConflict(error) {
|
|
169
|
+
// Followed through the errors wrapping it, i.e. a service's DatabaseError
|
|
170
|
+
for (let e = error; e; e = e.cause) {
|
|
171
|
+
if (e.code === 'P2034' || e.code === common_1.ErrorCode.DatabaseWriteConflict) {
|
|
172
|
+
return true;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.PrismaDatabase = PrismaDatabase;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IsolationLevel, TransactionOptions } from '@appweaver/common';
|
|
2
|
+
type TransactionFn<T, Tx> = (tx: Tx) => Promise<T>;
|
|
3
|
+
/**
|
|
4
|
+
* Runs the given function in a database transaction. Every service call and
|
|
5
|
+
* every query of the database client made while it runs is part of the
|
|
6
|
+
* transaction, which commits once the function resolves. An error thrown out of
|
|
7
|
+
* the function rolls the transaction back and is rethrown, while an error caught
|
|
8
|
+
* inside it does not.
|
|
9
|
+
*
|
|
10
|
+
* The resource events, the cache invalidation, and the removal of stored files
|
|
11
|
+
* wait for the commit, and are dropped on rollback. A call made inside another
|
|
12
|
+
* transaction joins it.
|
|
13
|
+
*
|
|
14
|
+
* @param {IsolationLevel | TransactionOptions} [options] - The isolation level or
|
|
15
|
+
* the transaction options, falling back to the database default and the config.
|
|
16
|
+
* @param {Function} fn - The function to run, receiving the transaction client.
|
|
17
|
+
* @return {Promise<Object>} The value returned by the function.
|
|
18
|
+
*/
|
|
19
|
+
export declare function runTransaction<T, Tx = any>(fn: TransactionFn<T, Tx>): Promise<T>;
|
|
20
|
+
export declare function runTransaction<T, Tx = any>(options: IsolationLevel | `${IsolationLevel}` | TransactionOptions, fn: TransactionFn<T, Tx>): Promise<T>;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runTransaction = runTransaction;
|
|
4
|
+
const common_1 = require("@appweaver/common");
|
|
5
|
+
const core_1 = require("@appweaver/core");
|
|
6
|
+
function runTransaction(optionsOrFn, fn) {
|
|
7
|
+
const db = (0, core_1.inject)(common_1.Database);
|
|
8
|
+
if (typeof optionsOrFn === 'function') {
|
|
9
|
+
return db.transaction(optionsOrFn);
|
|
10
|
+
}
|
|
11
|
+
const options = typeof optionsOrFn === 'string'
|
|
12
|
+
? { isolationLevel: optionsOrFn }
|
|
13
|
+
: optionsOrFn;
|
|
14
|
+
return db.transaction(fn, options);
|
|
15
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exportConfig = void 0;
|
|
4
|
+
const typebox_1 = require("@sinclair/typebox");
|
|
5
|
+
const common_1 = require("@appweaver/common");
|
|
6
|
+
exports.exportConfig = (0, common_1.defineConfig)('export', {
|
|
7
|
+
/** Number of records per batch during export. Default: `1000`. */
|
|
8
|
+
BATCH_SIZE: typebox_1.Type.Integer({ default: 1000 }),
|
|
9
|
+
/** CSV field delimiter character. Default: `';'`. */
|
|
10
|
+
CSV_DELIMITER: typebox_1.Type.String({ default: ';' }),
|
|
11
|
+
/** Delimiter for joining array values in CSV cells. Default: `','`. */
|
|
12
|
+
CSV_JOIN_DELIMITER: typebox_1.Type.String({ default: ',' }),
|
|
13
|
+
/** Include a header row in CSV exports. Default: `true`. */
|
|
14
|
+
CSV_ADD_HEADERS: typebox_1.Type.Boolean({ default: true }),
|
|
15
|
+
/** Add a separator row (BOM) for Excel compatibility. Default: `false`. */
|
|
16
|
+
CSV_ADD_SEP_ROW: typebox_1.Type.Boolean({ default: false })
|
|
17
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Readable } from 'node:stream';
|
|
2
|
+
import { QuerySort } from '@appweaver/common';
|
|
3
|
+
export type ExportStream = {
|
|
4
|
+
stream: Readable;
|
|
5
|
+
fileName: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
};
|
|
8
|
+
export declare class ExportService {
|
|
9
|
+
/**
|
|
10
|
+
* Exports data as a CSV stream based on the provided filter and sort parameters.
|
|
11
|
+
* The records are read batch by batch, each following the cursor of the
|
|
12
|
+
* previous one, so the export never counts the records up front.
|
|
13
|
+
*
|
|
14
|
+
* @param {string} modelName - The resource name for which to export data.
|
|
15
|
+
* @param {Object} [filter={}] - The filter conditions to apply when retrieving the data. Default is an empty object.
|
|
16
|
+
* @param {QuerySort} [sort='-createdAt'] - The sorting criteria for the data, given either as a comma-separated
|
|
17
|
+
* field list or as an object of field directions.
|
|
18
|
+
* @return {Promise<ExportStream>} A promise resolving to the export stream object, which includes the readable
|
|
19
|
+
* stream, MIME type, and file name for the CSV.
|
|
20
|
+
*/
|
|
21
|
+
exportCsv(modelName: string, filter?: any, sort?: QuerySort): Promise<ExportStream>;
|
|
22
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExportService = void 0;
|
|
4
|
+
const node_stream_1 = require("node:stream");
|
|
5
|
+
const common_1 = require("@appweaver/common");
|
|
6
|
+
const export_config_1 = require("./export-config");
|
|
7
|
+
const core_1 = require("@appweaver/core");
|
|
8
|
+
const file_util_1 = require("../utils/file-util");
|
|
9
|
+
const export_util_1 = require("../utils/export-util");
|
|
10
|
+
class ExportService {
|
|
11
|
+
/**
|
|
12
|
+
* Exports data as a CSV stream based on the provided filter and sort parameters.
|
|
13
|
+
* The records are read batch by batch, each following the cursor of the
|
|
14
|
+
* previous one, so the export never counts the records up front.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} modelName - The resource name for which to export data.
|
|
17
|
+
* @param {Object} [filter={}] - The filter conditions to apply when retrieving the data. Default is an empty object.
|
|
18
|
+
* @param {QuerySort} [sort='-createdAt'] - The sorting criteria for the data, given either as a comma-separated
|
|
19
|
+
* field list or as an object of field directions.
|
|
20
|
+
* @return {Promise<ExportStream>} A promise resolving to the export stream object, which includes the readable
|
|
21
|
+
* stream, MIME type, and file name for the CSV.
|
|
22
|
+
*/
|
|
23
|
+
async exportCsv(modelName, filter = {}, sort = '-createdAt') {
|
|
24
|
+
const service = (0, core_1.injectService)(modelName);
|
|
25
|
+
let exportStream;
|
|
26
|
+
try {
|
|
27
|
+
const batchSize = export_config_1.exportConfig.BATCH_SIZE;
|
|
28
|
+
const mapValues = (items) => this.mapProperties(modelName, items);
|
|
29
|
+
const readBatch = (cursor) => service.query({
|
|
30
|
+
filter,
|
|
31
|
+
size: batchSize,
|
|
32
|
+
sort,
|
|
33
|
+
cursor,
|
|
34
|
+
totalCount: false
|
|
35
|
+
});
|
|
36
|
+
// Read before the stream is handed out, so a failing query becomes an
|
|
37
|
+
// error response instead of a truncated download
|
|
38
|
+
const firstBatch = await readBatch();
|
|
39
|
+
// A generator rather than a `read` callback, which the stream may re-enter
|
|
40
|
+
// while an awaited batch is pending and would then write it twice
|
|
41
|
+
async function* rows() {
|
|
42
|
+
if (export_config_1.exportConfig.CSV_ADD_SEP_ROW) {
|
|
43
|
+
yield `SEP=${export_config_1.exportConfig.CSV_DELIMITER}\n`;
|
|
44
|
+
}
|
|
45
|
+
let batch = firstBatch;
|
|
46
|
+
let addHeaders = export_config_1.exportConfig.CSV_ADD_HEADERS;
|
|
47
|
+
while (batch) {
|
|
48
|
+
let chunk;
|
|
49
|
+
let next;
|
|
50
|
+
try {
|
|
51
|
+
if (batch.resultCount > 0) {
|
|
52
|
+
chunk = (0, export_util_1.toCsv)(mapValues(batch.items), addHeaders);
|
|
53
|
+
addHeaders = false;
|
|
54
|
+
}
|
|
55
|
+
// A next cursor is returned only while further records match
|
|
56
|
+
next = batch.nextCursor
|
|
57
|
+
? await readBatch(batch.nextCursor)
|
|
58
|
+
: undefined;
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
// End early rather than fail a download already in flight
|
|
62
|
+
common_1.logger.error(e, `${modelName} export error`);
|
|
63
|
+
next = undefined;
|
|
64
|
+
}
|
|
65
|
+
if (chunk !== undefined) {
|
|
66
|
+
yield chunk;
|
|
67
|
+
}
|
|
68
|
+
batch = next;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exportStream = node_stream_1.Readable.from(rows(), { objectMode: false });
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
throw new common_1.ExportError(common_1.ErrorCode.ExportFailed, `${modelName} export failed`, { model: modelName }, { cause: e });
|
|
75
|
+
}
|
|
76
|
+
const fileName = this.generateExportFileName(modelName, 'csv');
|
|
77
|
+
common_1.logger.debug({ modelName, filter, fileName }, 'CSV File export');
|
|
78
|
+
return { stream: exportStream, mimeType: 'text/csv', fileName };
|
|
79
|
+
}
|
|
80
|
+
/** @internal */
|
|
81
|
+
mapProperties(resourceName, items) {
|
|
82
|
+
const properties = [];
|
|
83
|
+
for (const item of items) {
|
|
84
|
+
const property = this.mapProperty(resourceName, item);
|
|
85
|
+
properties.push(property);
|
|
86
|
+
}
|
|
87
|
+
return properties;
|
|
88
|
+
}
|
|
89
|
+
/** @internal */
|
|
90
|
+
mapProperty(resourceName, item, resourceExportConfig, parentKey = '') {
|
|
91
|
+
const resourceModel = (0, core_1.injectModel)(resourceName);
|
|
92
|
+
const readModel = resourceModel.readModel;
|
|
93
|
+
const relationsModel = resourceModel.relationsModel;
|
|
94
|
+
const filesModel = resourceModel.filesModel;
|
|
95
|
+
const fieldsConfig = resourceExportConfig ?? resourceModel.config.export;
|
|
96
|
+
const property = {};
|
|
97
|
+
for (const key in item) {
|
|
98
|
+
let value = item[key];
|
|
99
|
+
const modelSchema = (0, common_1.extractSchemaProperties)(readModel, key);
|
|
100
|
+
const relationSchema = (0, common_1.extractSchemaProperties)(relationsModel, key);
|
|
101
|
+
const fileSchema = (0, common_1.extractSchemaProperties)(filesModel, key);
|
|
102
|
+
if (!modelSchema &&
|
|
103
|
+
!relationSchema &&
|
|
104
|
+
!fileSchema &&
|
|
105
|
+
!(0, common_1.isCountField)(key)) {
|
|
106
|
+
// Skip mapping for non-exposed fields.
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
// The records are read from the database rather than through a response
|
|
110
|
+
// schema, so the fields no response carries are dropped here instead.
|
|
111
|
+
if (modelSchema?.['hidden'] === true ||
|
|
112
|
+
resourceModel.config.virtual?.[key]?.output?.type === 'none') {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
let header = parentKey ? `${parentKey}.${key}` : key;
|
|
116
|
+
const isArrayValue = (0, common_1.isArray)(value);
|
|
117
|
+
const exportField = fieldsConfig?.[key];
|
|
118
|
+
if (exportField) {
|
|
119
|
+
if (exportField.exclude === true) {
|
|
120
|
+
// Skip mapping for excluded fields.
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if ((0, common_1.isString)(exportField.headerName)) {
|
|
124
|
+
header = parentKey
|
|
125
|
+
? `${parentKey}.${exportField.headerName}`
|
|
126
|
+
: exportField.headerName;
|
|
127
|
+
}
|
|
128
|
+
const mapValue = exportField.mapValue;
|
|
129
|
+
if (mapValue) {
|
|
130
|
+
// A scalar value has no properties to read the configured field name
|
|
131
|
+
// off, so it is resolved against the exported record itself.
|
|
132
|
+
if ((0, common_1.isString)(mapValue) && !relationSchema && !fileSchema) {
|
|
133
|
+
value = item?.[mapValue];
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
const mappedValues = [];
|
|
137
|
+
// Transform value items using mapValue configuration.
|
|
138
|
+
const subItems = isArrayValue ? value : [value];
|
|
139
|
+
for (const subItem of subItems) {
|
|
140
|
+
if ((0, common_1.isFunction)(mapValue)) {
|
|
141
|
+
try {
|
|
142
|
+
mappedValues.push(mapValue(subItem));
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
mappedValues.push('');
|
|
146
|
+
common_1.logger.error(e, 'Export value mapping error.');
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if ((0, common_1.isString)(mapValue)) {
|
|
150
|
+
mappedValues.push(subItem?.[mapValue]);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
value = mappedValues.join(',');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// Recursively map relation and file fields if the value is an object.
|
|
158
|
+
// Arrays of plain values are written as a single column below.
|
|
159
|
+
if ((0, common_1.isPlainObject)(value) || (isArrayValue && (0, common_1.isPlainObject)(value[0]))) {
|
|
160
|
+
const relationName = (0, common_1.extractResourceName)(relationSchema ?? fileSchema);
|
|
161
|
+
if (!relationName) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const subItems = isArrayValue ? value : [value];
|
|
165
|
+
for (const subItem of subItems) {
|
|
166
|
+
const mappedItem = this.mapProperty(relationName, subItem, exportField, header);
|
|
167
|
+
for (const subKey in mappedItem) {
|
|
168
|
+
if (subKey in property) {
|
|
169
|
+
property[subKey] = [property[subKey], mappedItem[subKey]].join(export_config_1.exportConfig.CSV_JOIN_DELIMITER);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
property[subKey] = mappedItem[subKey];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
property[header] = value;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return property;
|
|
182
|
+
}
|
|
183
|
+
/** @internal */
|
|
184
|
+
generateExportFileName(modelName, extension) {
|
|
185
|
+
return (0, file_util_1.generateFileName)(`${(0, common_1.plural)(modelName)}.${extension}`, '{name}_{year}{month}{day}_{hours}{minutes}{seconds}.{extension}');
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
exports.ExportService = ExportService;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './export-service';
|
package/export/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./export-service"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ResourceModel, ResourceModelConfig } from '@appweaver/common';
|
|
2
|
+
/**
|
|
3
|
+
* Creates a resource model from its configuration and defines it in the application context. The names of the
|
|
4
|
+
* declared fields are inferred from the configuration, so the `read`, `create`, and `update` options only accept
|
|
5
|
+
* existing fields.
|
|
6
|
+
*
|
|
7
|
+
* @param {ResourceModelConfig} config - The model configuration.
|
|
8
|
+
* @param {boolean} [override=false] - Whether to replace a model already defined under the same name.
|
|
9
|
+
* @return {ResourceModel} The created resource model.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createModel<S extends string, R extends string, F extends string, V extends string>(modelConfig: ResourceModelConfig<S, R, F, V>, override?: boolean): ResourceModel;
|