@appweaver/data 2.0.2 → 2.1.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.
@@ -1,4 +1,4 @@
1
- import { PrismaClient } from '../prisma/client/client';
1
+ import { Prisma, PrismaClient } from '../prisma/client/client';
2
2
  /**
3
3
  * Creates and returns a PrismaClient instance based on the configured database type.
4
4
  *
@@ -9,3 +9,5 @@ import { PrismaClient } from '../prisma/client/client';
9
9
  * @return {PrismaClient} A PrismaClient instance configured for the selected database type.
10
10
  */
11
11
  export declare function createClient(): PrismaClient;
12
+ /** Logs a query, as a warning without its parameters when it is slow. */
13
+ export declare function logQuery(e: Prisma.QueryEvent): void;
@@ -4,15 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createClient = createClient;
7
+ exports.logQuery = logQuery;
7
8
  const node_path_1 = __importDefault(require("node:path"));
8
9
  const common_1 = require("@appweaver/common");
9
10
  const database_config_1 = require("./database-config");
11
+ const logger = (0, common_1.createLogger)('database');
10
12
  const options = {
11
13
  transactionOptions: {
12
14
  maxWait: database_config_1.databaseConfig.TRANSACTION_MAX_WAIT,
13
15
  timeout: database_config_1.databaseConfig.TRANSACTION_TIMEOUT
14
16
  },
15
- log: database_config_1.databaseConfig.LOG_EVENTS.map((e) => ({ emit: 'event', level: e }))
17
+ log: emittedEvents().map((e) => ({ emit: 'event', level: e }))
16
18
  };
17
19
  /**
18
20
  * Creates and returns a PrismaClient instance based on the configured database type.
@@ -129,17 +131,33 @@ function createPrismaClient(adapter) {
129
131
  const { PrismaClient } = (0, common_1.requireModule)(clientPath, true).value;
130
132
  const client = new PrismaClient({ adapter, ...options });
131
133
  // Add configured Prisma event logging
132
- for (const event of database_config_1.databaseConfig.LOG_EVENTS) {
134
+ for (const event of emittedEvents()) {
133
135
  if (event === common_1.DatabaseEvent.Query) {
134
- client.$on(event, (e) => {
135
- common_1.logger.debug(e, 'Prisma query');
136
- });
136
+ client.$on(event, logQuery);
137
137
  }
138
138
  else {
139
139
  client.$on(event, (e) => {
140
- common_1.logger[event](e, `Prisma ${event} log`);
140
+ logger[event](e, `Prisma ${event} log`);
141
141
  });
142
142
  }
143
143
  }
144
144
  return client;
145
145
  }
146
+ /** The events Prisma emits: the logged ones, and the queries to time with a slow query threshold. */
147
+ function emittedEvents() {
148
+ const events = new Set(database_config_1.databaseConfig.LOG_EVENTS);
149
+ if (database_config_1.databaseConfig.SLOW_QUERY_THRESHOLD > 0) {
150
+ events.add(common_1.DatabaseEvent.Query);
151
+ }
152
+ return [...events];
153
+ }
154
+ /** Logs a query, as a warning without its parameters when it is slow. */
155
+ function logQuery(e) {
156
+ const threshold = database_config_1.databaseConfig.SLOW_QUERY_THRESHOLD;
157
+ if (threshold > 0 && e.duration >= threshold) {
158
+ logger.warn({ query: e.query, duration: e.duration, threshold }, 'Slow database query');
159
+ }
160
+ else if (database_config_1.databaseConfig.LOG_EVENTS.includes(common_1.DatabaseEvent.Query)) {
161
+ logger.debug(e, 'Prisma query');
162
+ }
163
+ }
@@ -10,5 +10,6 @@ export declare const databaseConfig: Readonly<{
10
10
  TRANSACTION_TIMEOUT: number;
11
11
  AI_DANGEROUS_ACTIONS_ENABLED: boolean;
12
12
  LOG_EVENTS: DatabaseEvent[];
13
+ SLOW_QUERY_THRESHOLD: number;
13
14
  PROVIDER: string;
14
15
  }>;
@@ -35,6 +35,9 @@ exports.databaseConfig = (0, common_1.defineConfig)('database', {
35
35
  LOG_EVENTS: typebox_1.Type.Array(typebox_1.Type.Enum(common_1.DatabaseEvent), {
36
36
  default: []
37
37
  }),
38
+ /** Duration in milliseconds from which a query is logged as a warning by the `database` logger, `0` to disable it.
39
+ * Default: `0`. */
40
+ SLOW_QUERY_THRESHOLD: typebox_1.Type.Integer({ default: 0, minimum: 0 }),
38
41
  /** Database provider: `auto` or `prisma`, or `none` for an application without a database. Default: `'auto'`. */
39
42
  PROVIDER: typebox_1.Type.String({ default: 'auto' })
40
43
  });
@@ -5,6 +5,7 @@ const common_1 = require("@appweaver/common");
5
5
  const database_config_1 = require("./database-config");
6
6
  const create_client_1 = require("./create-client");
7
7
  const internal_1 = require("@appweaver/core/internal");
8
+ const logger = (0, common_1.createLogger)('database');
8
9
  // Members that act on the connection itself, never on a transaction
9
10
  const ROOT_MEMBERS = new Set([
10
11
  '$connect',
@@ -27,14 +28,15 @@ class PrismaDatabase extends common_1.Database {
27
28
  * failing the start, since most drivers connect lazily and the queries reconnect once it is reachable.
28
29
  */
29
30
  async onInit() {
31
+ const dbType = (0, common_1.resolveDatabaseType)(database_config_1.databaseConfig.TYPE, database_config_1.databaseConfig.URL);
32
+ const url = this.redactUrl(database_config_1.databaseConfig.URL);
30
33
  try {
31
34
  await this.connect();
32
35
  await this.clientInstance().$queryRaw `SELECT 1`;
36
+ logger.info(`Connected to the ${dbType} database${url ? ` at ${url}` : ''}`);
33
37
  }
34
38
  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}` : ''}`);
39
+ logger.error(error, `Cannot connect to the ${dbType} database${url ? ` at ${url}` : ''}`);
38
40
  // The next query creates a new client, which detects the capabilities of the database again
39
41
  await this.disconnect().catch(() => undefined);
40
42
  this._client = undefined;
@@ -55,6 +57,7 @@ class PrismaDatabase extends common_1.Database {
55
57
  }
56
58
  await this._client.$disconnect();
57
59
  this._client = undefined;
60
+ logger.debug('Disconnected from the database');
58
61
  }
59
62
  /**
60
63
  * Returns the Prisma client, which runs its queries in the transaction the
@@ -109,8 +112,10 @@ class PrismaDatabase extends common_1.Database {
109
112
  catch (e) {
110
113
  await (0, internal_1.settleTransaction)(context, false);
111
114
  if (attempt < (options.retries ?? 0) && this.isWriteConflict(e)) {
115
+ logger.debug({ attempt: attempt + 1, retries: options.retries }, 'Transaction write conflict, retrying');
112
116
  continue;
113
117
  }
118
+ logger.debug({ error: e.message }, 'Transaction rolled back');
114
119
  throw e;
115
120
  }
116
121
  await (0, internal_1.settleTransaction)(context, true);
@@ -7,6 +7,7 @@ const export_config_1 = require("./export-config");
7
7
  const core_1 = require("@appweaver/core");
8
8
  const file_util_1 = require("../utils/file-util");
9
9
  const export_util_1 = require("../utils/export-util");
10
+ const logger = (0, common_1.createLogger)('resource');
10
11
  class ExportService {
11
12
  /**
12
13
  * Exports data as a CSV stream based on the provided filter and sort parameters.
@@ -59,7 +60,7 @@ class ExportService {
59
60
  }
60
61
  catch (e) {
61
62
  // End early rather than fail a download already in flight
62
- common_1.logger.error(e, `${modelName} export error`);
63
+ logger.error(e, `${modelName} export error`);
63
64
  next = undefined;
64
65
  }
65
66
  if (chunk !== undefined) {
@@ -74,7 +75,7 @@ class ExportService {
74
75
  throw new common_1.ExportError(common_1.ErrorCode.ExportFailed, `${modelName} export failed`, { model: modelName }, { cause: e });
75
76
  }
76
77
  const fileName = this.generateExportFileName(modelName, 'csv');
77
- common_1.logger.debug({ modelName, filter, fileName }, 'CSV File export');
78
+ logger.debug({ modelName, filter, fileName }, 'CSV File export');
78
79
  return { stream: exportStream, mimeType: 'text/csv', fileName };
79
80
  }
80
81
  /** @internal */
@@ -143,7 +144,7 @@ class ExportService {
143
144
  }
144
145
  catch (e) {
145
146
  mappedValues.push('');
146
- common_1.logger.error(e, 'Export value mapping error.');
147
+ logger.error(e, 'Export value mapping error.');
147
148
  }
148
149
  }
149
150
  else if ((0, common_1.isString)(mapValue)) {
@@ -6,6 +6,7 @@ const common_1 = require("@appweaver/common");
6
6
  const internal_1 = require("@appweaver/core/internal");
7
7
  const core_1 = require("@appweaver/core");
8
8
  const model_schema_1 = require("../resource/model-schema");
9
+ const logger = (0, common_1.createLogger)('resource');
9
10
  /**
10
11
  * Creates a resource model from its configuration and defines it in the application context. The names of the
11
12
  * declared fields are inferred from the configuration, so the `read`, `create`, and `update` options only accept
@@ -18,7 +19,7 @@ const model_schema_1 = require("../resource/model-schema");
18
19
  function createModel(modelConfig, override = false) {
19
20
  const config = normalizeRelations(modelConfig);
20
21
  const resourceModel = buildModel(config);
21
- common_1.logger.debug({ modelName: resourceModel.name }, 'Created resource model');
22
+ logger.debug({ modelName: resourceModel.name }, 'Created resource model');
22
23
  (0, core_1.define)(resourceModel, undefined, override ? 'override' : undefined);
23
24
  // The relation inputs and audit fields reference the primary key of another
24
25
  // model, which may not be loaded yet. Loaders call this hook once every model
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createPolicy = createPolicy;
4
4
  const common_1 = require("@appweaver/common");
5
5
  const core_1 = require("@appweaver/core");
6
+ const logger = (0, common_1.createLogger)('resource');
6
7
  /**
7
8
  * Creates the policy of a model, checking the access to its records and restricting the reads and writes. Once the
8
9
  * types are generated, the records the callbacks receive are typed by the model name.
@@ -14,7 +15,7 @@ const core_1 = require("@appweaver/core");
14
15
  function createPolicy(config, override = false) {
15
16
  config[common_1.RESOURCE_NAME] = (0, common_1.capitalize)(config.modelName);
16
17
  config[common_1.RESOURCE_TYPE] = common_1.RESOURCE_POLICY_TYPE;
17
- common_1.logger.debug({ modelName: config.modelName }, 'Created resource policy');
18
+ logger.debug({ modelName: config.modelName }, 'Created resource policy');
18
19
  (0, core_1.define)(config, undefined, override ? 'override' : undefined);
19
20
  return config;
20
21
  }
@@ -5,6 +5,7 @@ const common_1 = require("@appweaver/common");
5
5
  const core_1 = require("@appweaver/core");
6
6
  const resource_service_1 = require("../resource/resource-service");
7
7
  const text_search_util_1 = require("../resource/utils/text-search-util");
8
+ const logger = (0, common_1.createLogger)('resource');
8
9
  /**
9
10
  * Creates the resource service of a model, running the configured hooks around its operations and applying the
10
11
  * policy of the model. The update hooks run around both an update and a replacement. Once the types are generated,
@@ -110,7 +111,7 @@ function createService(config, override = false) {
110
111
  Service[common_1.CONFIG] = config;
111
112
  Service[common_1.RESOURCE_NAME] = name;
112
113
  Service[common_1.RESOURCE_TYPE] = common_1.RESOURCE_SERVICE_TYPE;
113
- common_1.logger.debug({ modelName: config.modelName }, 'Created resource service');
114
+ logger.debug({ modelName: config.modelName }, 'Created resource service');
114
115
  (0, core_1.define)(Service, undefined, override ? 'override' : undefined);
115
116
  return Service;
116
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appweaver/data",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "Appweaver - the backend framework for AI-first development (@data)",
5
5
  "author": "Luka Matosevic",
6
6
  "license": "MIT",
@@ -51,8 +51,8 @@
51
51
  "csv-stringify": "6.8.0"
52
52
  },
53
53
  "peerDependencies": {
54
- "@appweaver/common": "^2.0.2",
55
- "@appweaver/core": "^2.0.2",
54
+ "@appweaver/common": "^2.1.0",
55
+ "@appweaver/core": "^2.1.0",
56
56
  "@prisma/client": "^7.9.0"
57
57
  },
58
58
  "devDependencies": {
@@ -6,6 +6,7 @@ const common_1 = require("@appweaver/common");
6
6
  const core_1 = require("@appweaver/core");
7
7
  const internal_1 = require("@appweaver/core/internal");
8
8
  const resource_loader_1 = require("./resource-loader");
9
+ const logger = (0, common_1.createLogger)('resource');
9
10
  /**
10
11
  * The setup of `@appweaver/data`: loads the models, services and policies of the application, then the resources of
11
12
  * the modules, unless the `autoLoadResources` parameter of `createApp` is `false`.
@@ -31,7 +32,7 @@ function warnUnprocessedImages() {
31
32
  .filter(([, field]) => field?.image)
32
33
  .map(([name]) => `${model.name}.${name}`));
33
34
  if (fields.length > 0) {
34
- common_1.logger.warn({ fields }, 'The images of these file fields are stored unprocessed, since no image processor is provided: install ' +
35
+ logger.warn({ fields }, 'The images of these file fields are stored unprocessed, since no image processor is provided: install ' +
35
36
  '@appweaver/sharp, or select one in IMAGE_PROVIDER');
36
37
  }
37
38
  }
@@ -8,6 +8,7 @@ const file_util_1 = require("../utils/file-util");
8
8
  const model_util_1 = require("../utils/model-util");
9
9
  const internal_1 = require("@appweaver/core/internal");
10
10
  const prune_util_1 = require("./utils/prune-util");
11
+ const logger = (0, common_1.createLogger)('resource');
11
12
  /**
12
13
  * Loads the application resources: models, services and policies.
13
14
  *
@@ -124,7 +125,7 @@ async function findExports(pattern, cwd, moduleResources) {
124
125
  for (const filePath of await (0, common_1.findFilesByPattern)(pattern, cwd)) {
125
126
  const { value, error } = await (0, common_1.importModule)(filePath);
126
127
  if (error) {
127
- common_1.logger.error(error, 'Resource path import error');
128
+ logger.error(error, 'Resource path import error');
128
129
  continue;
129
130
  }
130
131
  if (value) {
@@ -15,6 +15,7 @@ const relation_util_1 = require("./utils/relation-util");
15
15
  const cursor_util_1 = require("./utils/cursor-util");
16
16
  const filter_util_1 = require("./utils/filter-util");
17
17
  const sort_util_1 = require("./utils/sort-util");
18
+ const logger = (0, common_1.createLogger)('resource');
18
19
  class ResourceService {
19
20
  modelName;
20
21
  /** @internal */
@@ -404,6 +405,7 @@ class ResourceService {
404
405
  this._events.emitResourceEvent(this._client.name, 'create', {
405
406
  current: resource
406
407
  });
408
+ logger.debug({ model: this._client.name, id: resource.id }, 'Resource created');
407
409
  return this.projectResource(resource);
408
410
  }
409
411
  /**
@@ -535,6 +537,7 @@ class ResourceService {
535
537
  this._events.emitResourceEvent(this._client.name, 'delete', {
536
538
  current: resource
537
539
  });
540
+ logger.debug({ model: this._client.name, id, softDelete }, 'Resource deleted');
538
541
  return this.projectResource(resource);
539
542
  }
540
543
  /**
@@ -678,6 +681,7 @@ class ResourceService {
678
681
  previous: updateResource,
679
682
  current: resource
680
683
  });
684
+ logger.debug({ model: this._client.name, id }, 'Resource updated');
681
685
  return {
682
686
  previous: this.projectResource(updateResource),
683
687
  current: this.projectResource(resource)
@@ -826,7 +830,15 @@ class ResourceService {
826
830
  * @internal
827
831
  */
828
832
  async applyAccessCheck(action, resource) {
829
- return (0, policy_util_1.arePoliciesSkipped)() || this.checkAccess(action, resource);
833
+ if ((0, policy_util_1.arePoliciesSkipped)() || (await this.checkAccess(action, resource))) {
834
+ return true;
835
+ }
836
+ logger.debug({
837
+ model: this._client.name,
838
+ action,
839
+ id: resource?.id
840
+ }, 'Resource access denied by its policy');
841
+ return false;
830
842
  }
831
843
  /**
832
844
  * Builds the database conditions of a query filter: the mapped filter, its
@@ -984,7 +996,7 @@ class ResourceService {
984
996
  */
985
997
  async bulk(items, operation, prepare) {
986
998
  try {
987
- return await this._db.client().$transaction(async () => {
999
+ const results = await this._db.client().$transaction(async () => {
988
1000
  await prepare?.();
989
1001
  const results = [];
990
1002
  for (const item of items) {
@@ -995,6 +1007,8 @@ class ResourceService {
995
1007
  }
996
1008
  return results;
997
1009
  }, { timeout: resource_config_1.resourceConfig.BULK_TRANSACTION_TIMEOUT });
1010
+ logger.debug({ model: this._client.name, count: results.length }, 'Bulk operation committed');
1011
+ return results;
998
1012
  }
999
1013
  catch (e) {
1000
1014
  // The errors of the items are translated already, which leaves the
@@ -9,6 +9,7 @@ const policy_util_1 = require("../utils/policy-util");
9
9
  const file_util_1 = require("../utils/file-util");
10
10
  const model_util_1 = require("../utils/model-util");
11
11
  const internal_1 = require("@appweaver/core/internal");
12
+ const logger = (0, common_1.createLogger)('storage');
12
13
  class FileService {
13
14
  /** @internal */
14
15
  _db = (0, core_1.inject)(common_1.Database);
@@ -217,7 +218,7 @@ class FileService {
217
218
  where: { name: fileName }
218
219
  }));
219
220
  deletedFile.url = (0, file_util_1.buildFileUrl)(deletedFile);
220
- common_1.logger.debug({ deletedFile }, 'File deleted');
221
+ logger.debug({ deletedFile }, 'File deleted');
221
222
  return deletedFile;
222
223
  }
223
224
  catch (e) {
@@ -302,7 +303,7 @@ class FileService {
302
303
  }));
303
304
  }
304
305
  catch (e) {
305
- common_1.logger.error({ resourceName, resourceIds, error: e }, 'Error finding resource files for deletion');
306
+ logger.error({ resourceName, resourceIds, error: e }, 'Error finding resource files for deletion');
306
307
  return [];
307
308
  }
308
309
  const deletedFiles = [];
@@ -312,11 +313,11 @@ class FileService {
312
313
  deletedFiles.push(file);
313
314
  }
314
315
  else {
315
- common_1.logger.error({ resourceName, resourceId: file.resourceId, fileName: file.name }, 'Failed to delete file');
316
+ logger.error({ resourceName, resourceId: file.resourceId, fileName: file.name }, 'Failed to delete file');
316
317
  }
317
318
  }
318
319
  if (deletedFiles.length > 0) {
319
- common_1.logger.debug({ resourceName, resourceIds, deletedCount: deletedFiles.length }, 'Resource files deleted');
320
+ logger.debug({ resourceName, resourceIds, deletedCount: deletedFiles.length }, 'Resource files deleted');
320
321
  }
321
322
  return deletedFiles;
322
323
  }
@@ -448,7 +449,7 @@ class FileService {
448
449
  // pipe() does not forward source errors to destinations, so both branches
449
450
  // must be destroyed manually to avoid hanging on a failed upload stream.
450
451
  fileStream.on('error', (e) => {
451
- common_1.logger.error(e, 'Error calculating file checksum');
452
+ logger.error(e, 'Error calculating file checksum');
452
453
  storageStream.destroy(e);
453
454
  checksumStream.destroy(e);
454
455
  });
@@ -502,7 +503,7 @@ class FileService {
502
503
  file = result[data.fieldName].find((f) => f.name === createFile.name);
503
504
  }
504
505
  file.url = (0, file_util_1.buildFileUrl)(file);
505
- common_1.logger.debug({ file }, 'File saved');
506
+ logger.debug({ file }, 'File saved');
506
507
  return file;
507
508
  }
508
509
  catch (e) {