@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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +61 -2
  3. package/config.d.ts +6 -0
  4. package/config.js +22 -0
  5. package/database/create-client.d.ts +11 -0
  6. package/database/create-client.js +145 -0
  7. package/database/database-config.d.ts +14 -0
  8. package/database/database-config.js +40 -0
  9. package/database/database-error.d.ts +49 -0
  10. package/database/database-error.js +216 -0
  11. package/database/index.d.ts +3 -0
  12. package/database/index.js +19 -0
  13. package/database/prisma-database.d.ts +33 -0
  14. package/database/prisma-database.js +178 -0
  15. package/database/run-transaction.d.ts +21 -0
  16. package/database/run-transaction.js +15 -0
  17. package/export/export-config.d.ts +7 -0
  18. package/export/export-config.js +17 -0
  19. package/export/export-service.d.ts +22 -0
  20. package/export/export-service.js +188 -0
  21. package/export/index.d.ts +1 -0
  22. package/export/index.js +17 -0
  23. package/factory/create-model.d.ts +11 -0
  24. package/factory/create-model.js +479 -0
  25. package/factory/create-policy.d.ts +12 -0
  26. package/factory/create-policy.js +20 -0
  27. package/factory/create-service.d.ts +14 -0
  28. package/factory/create-service.js +116 -0
  29. package/index.d.ts +22 -0
  30. package/index.js +38 -0
  31. package/internal.d.ts +2 -0
  32. package/internal.js +6 -0
  33. package/module.d.ts +3 -0
  34. package/module.js +81 -0
  35. package/package.json +67 -3
  36. package/prisma/client/browser.d.ts +39 -0
  37. package/prisma/client/browser.js +56 -0
  38. package/prisma/client/client.d.ts +58 -0
  39. package/prisma/client/client.js +70 -0
  40. package/prisma/client/commonInputTypes.d.ts +345 -0
  41. package/prisma/client/commonInputTypes.js +11 -0
  42. package/prisma/client/enums.d.ts +1 -0
  43. package/prisma/client/enums.js +11 -0
  44. package/prisma/client/internal/class.d.ts +198 -0
  45. package/prisma/client/internal/class.js +85 -0
  46. package/prisma/client/internal/prismaNamespace.d.ts +1125 -0
  47. package/prisma/client/internal/prismaNamespace.js +204 -0
  48. package/prisma/client/internal/prismaNamespaceBrowser.d.ts +137 -0
  49. package/prisma/client/internal/prismaNamespaceBrowser.js +175 -0
  50. package/prisma/client/models/ApiKey.d.ts +1167 -0
  51. package/prisma/client/models/ApiKey.js +2 -0
  52. package/prisma/client/models/ConnectedAccount.d.ts +1094 -0
  53. package/prisma/client/models/ConnectedAccount.js +2 -0
  54. package/prisma/client/models/File.d.ts +1291 -0
  55. package/prisma/client/models/File.js +2 -0
  56. package/prisma/client/models/OneTimeToken.d.ts +1055 -0
  57. package/prisma/client/models/OneTimeToken.js +2 -0
  58. package/prisma/client/models/Permission.d.ts +1209 -0
  59. package/prisma/client/models/Permission.js +2 -0
  60. package/prisma/client/models/Role.d.ts +1209 -0
  61. package/prisma/client/models/Role.js +2 -0
  62. package/prisma/client/models/Seeder.d.ts +1023 -0
  63. package/prisma/client/models/Seeder.js +2 -0
  64. package/prisma/client/models.d.ts +8 -0
  65. package/prisma/client/models.js +2 -0
  66. package/resource/load-data-resources.d.ts +13 -0
  67. package/resource/load-data-resources.js +37 -0
  68. package/resource/model-schema.d.ts +34 -0
  69. package/resource/model-schema.js +54 -0
  70. package/resource/resource-config.d.ts +16 -0
  71. package/resource/resource-config.js +62 -0
  72. package/resource/resource-loader.d.ts +25 -0
  73. package/resource/resource-loader.js +138 -0
  74. package/resource/resource-service.d.ts +462 -0
  75. package/resource/resource-service.js +1042 -0
  76. package/resource/schemas/index.d.ts +3 -0
  77. package/resource/schemas/index.js +19 -0
  78. package/resource/schemas/resource-aggregate-schema.d.ts +53 -0
  79. package/resource/schemas/resource-aggregate-schema.js +130 -0
  80. package/resource/schemas/resource-filter-schema.d.ts +39 -0
  81. package/resource/schemas/resource-filter-schema.js +153 -0
  82. package/resource/schemas/resource-sort-schema.d.ts +38 -0
  83. package/resource/schemas/resource-sort-schema.js +113 -0
  84. package/resource/utils/aggregate-util.d.ts +113 -0
  85. package/resource/utils/aggregate-util.js +322 -0
  86. package/resource/utils/cursor-util.d.ts +60 -0
  87. package/resource/utils/cursor-util.js +116 -0
  88. package/resource/utils/date-util.d.ts +22 -0
  89. package/resource/utils/date-util.js +112 -0
  90. package/resource/utils/delete-util.d.ts +131 -0
  91. package/resource/utils/delete-util.js +385 -0
  92. package/resource/utils/filter-util.d.ts +24 -0
  93. package/resource/utils/filter-util.js +289 -0
  94. package/resource/utils/prune-util.d.ts +11 -0
  95. package/resource/utils/prune-util.js +56 -0
  96. package/resource/utils/relation-util.d.ts +75 -0
  97. package/resource/utils/relation-util.js +400 -0
  98. package/resource/utils/sort-util.d.ts +31 -0
  99. package/resource/utils/sort-util.js +235 -0
  100. package/resource/utils/text-search-util.d.ts +14 -0
  101. package/resource/utils/text-search-util.js +39 -0
  102. package/resources.d.ts +10 -0
  103. package/resources.js +26 -0
  104. package/security/auth-config.d.ts +23 -0
  105. package/security/auth-config.js +43 -0
  106. package/security/create-auth-resources.d.ts +28 -0
  107. package/security/create-auth-resources.js +163 -0
  108. package/security/helper.d.ts +109 -0
  109. package/security/helper.js +298 -0
  110. package/security/resources/api-key/model.d.ts +2 -0
  111. package/security/resources/api-key/model.js +67 -0
  112. package/security/resources/api-key/policy.d.ts +2 -0
  113. package/security/resources/api-key/policy.js +27 -0
  114. package/security/resources/api-key/service.d.ts +3 -0
  115. package/security/resources/api-key/service.js +34 -0
  116. package/security/resources/connected-account/model.d.ts +2 -0
  117. package/security/resources/connected-account/model.js +57 -0
  118. package/security/resources/connected-account/service.d.ts +2 -0
  119. package/security/resources/connected-account/service.js +7 -0
  120. package/security/resources/one-time-token/model.d.ts +2 -0
  121. package/security/resources/one-time-token/model.js +36 -0
  122. package/security/resources/permission/model.d.ts +2 -0
  123. package/security/resources/permission/model.js +16 -0
  124. package/security/resources/role/model.d.ts +2 -0
  125. package/security/resources/role/model.js +22 -0
  126. package/security/store/database-security-store.d.ts +5 -0
  127. package/security/store/database-security-store.js +54 -0
  128. package/seeder/create-seeder.d.ts +29 -0
  129. package/seeder/create-seeder.js +28 -0
  130. package/seeder/index.d.ts +2 -0
  131. package/seeder/index.js +18 -0
  132. package/seeder/resources/seeder/model.d.ts +2 -0
  133. package/seeder/resources/seeder/model.js +38 -0
  134. package/seeder/seeder.d.ts +29 -0
  135. package/seeder/seeder.js +197 -0
  136. package/storage/file-service.d.ts +133 -0
  137. package/storage/file-service.js +586 -0
  138. package/storage/image-config.d.ts +3 -0
  139. package/storage/image-config.js +11 -0
  140. package/storage/resources/file/model.d.ts +2 -0
  141. package/storage/resources/file/model.js +80 -0
  142. package/types/auth.d.ts +23 -0
  143. package/types/auth.js +2 -0
  144. package/types/generated.d.ts +366 -0
  145. package/types/generated.js +3 -0
  146. package/types/storage.d.ts +35 -0
  147. package/types/storage.js +2 -0
  148. package/utils/export-util.d.ts +8 -0
  149. package/utils/export-util.js +26 -0
  150. package/utils/file-util.d.ts +106 -0
  151. package/utils/file-util.js +254 -0
  152. package/utils/model-util.d.ts +36 -0
  153. package/utils/model-util.js +154 -0
  154. package/utils/policy-util.d.ts +18 -0
  155. package/utils/policy-util.js +28 -0
  156. package/utils/virtual-util.d.ts +11 -0
  157. package/utils/virtual-util.js +53 -0
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseIsoDate = parseIsoDate;
4
+ exports.addSeconds = addSeconds;
5
+ exports.addMinutes = addMinutes;
6
+ exports.addHours = addHours;
7
+ exports.addDays = addDays;
8
+ exports.addMonths = addMonths;
9
+ exports.addYears = addYears;
10
+ exports.differenceInSeconds = differenceInSeconds;
11
+ exports.differenceInMonths = differenceInMonths;
12
+ exports.differenceInYears = differenceInYears;
13
+ const MS_IN_SECOND = 1000;
14
+ const MS_IN_MINUTE = 60 * MS_IN_SECOND;
15
+ const MS_IN_HOUR = 60 * MS_IN_MINUTE;
16
+ /** A date-only ISO string, which reads as the local midnight instead of the UTC one of the `Date` constructor. */
17
+ const DATE_ONLY_PATTERN = /^(\d{4})-(\d{2})-(\d{2})$/;
18
+ /**
19
+ * Parses an ISO 8601 date string. A date without a time is the local midnight of that day, a date and time without an
20
+ * offset is local, and one with an offset is that exact instant.
21
+ *
22
+ * @param {string} value - The ISO date string.
23
+ * @return {Date} The parsed date, invalid when the string is not a date.
24
+ */
25
+ function parseIsoDate(value) {
26
+ const dateOnly = DATE_ONLY_PATTERN.exec(value);
27
+ if (dateOnly) {
28
+ const [, year, month, day] = dateOnly;
29
+ return new Date(Number(year), Number(month) - 1, Number(day));
30
+ }
31
+ return new Date(value);
32
+ }
33
+ function addSeconds(date, amount) {
34
+ return new Date(date.getTime() + amount * MS_IN_SECOND);
35
+ }
36
+ function addMinutes(date, amount) {
37
+ return new Date(date.getTime() + amount * MS_IN_MINUTE);
38
+ }
39
+ function addHours(date, amount) {
40
+ return new Date(date.getTime() + amount * MS_IN_HOUR);
41
+ }
42
+ /** Adds whole local days, keeping the time of day across daylight saving time changes. */
43
+ function addDays(date, amount) {
44
+ const result = new Date(date.getTime());
45
+ if (amount) {
46
+ result.setDate(result.getDate() + amount);
47
+ }
48
+ return result;
49
+ }
50
+ /** Adds whole local months, clamping the day to the last day of a shorter month (January 31 plus one is February 28). */
51
+ function addMonths(date, amount) {
52
+ const result = new Date(date.getTime());
53
+ if (!amount) {
54
+ return result;
55
+ }
56
+ const dayOfMonth = result.getDate();
57
+ const endOfDesiredMonth = new Date(date.getTime());
58
+ endOfDesiredMonth.setMonth(result.getMonth() + amount + 1, 0);
59
+ if (dayOfMonth >= endOfDesiredMonth.getDate()) {
60
+ return endOfDesiredMonth;
61
+ }
62
+ result.setFullYear(endOfDesiredMonth.getFullYear(), endOfDesiredMonth.getMonth(), dayOfMonth);
63
+ return result;
64
+ }
65
+ function addYears(date, amount) {
66
+ return addMonths(date, amount * 12);
67
+ }
68
+ /** Returns the whole seconds between two dates, truncated toward zero. */
69
+ function differenceInSeconds(laterDate, earlierDate) {
70
+ return Math.trunc((laterDate.getTime() - earlierDate.getTime()) / MS_IN_SECOND);
71
+ }
72
+ /** Returns the full local months between two dates, negative when the first date is the earlier one. */
73
+ function differenceInMonths(laterDate, earlierDate) {
74
+ const sign = compare(laterDate, earlierDate);
75
+ const difference = Math.abs((laterDate.getFullYear() - earlierDate.getFullYear()) * 12 +
76
+ laterDate.getMonth() -
77
+ earlierDate.getMonth());
78
+ if (difference < 1) {
79
+ return 0;
80
+ }
81
+ const working = new Date(laterDate.getTime());
82
+ if (working.getMonth() === 1 && working.getDate() > 27) {
83
+ working.setDate(30);
84
+ }
85
+ working.setMonth(working.getMonth() - sign * difference);
86
+ // The last month counts only once its day is reached, unless the later date ends a month
87
+ let isLastMonthNotFull = compare(working, earlierDate) === -sign;
88
+ if (isLastDayOfMonth(laterDate) && difference === 1 && sign === 1) {
89
+ isLastMonthNotFull = false;
90
+ }
91
+ return sign * (difference - Number(isLastMonthNotFull)) || 0;
92
+ }
93
+ /** Returns the full local years between two dates, negative when the first date is the earlier one. */
94
+ function differenceInYears(laterDate, earlierDate) {
95
+ const sign = compare(laterDate, earlierDate);
96
+ const difference = Math.abs(laterDate.getFullYear() - earlierDate.getFullYear());
97
+ // Compares the dates within the same leap year to tell whether the last year is full
98
+ const later = new Date(laterDate.getTime());
99
+ const earlier = new Date(earlierDate.getTime());
100
+ later.setFullYear(1584);
101
+ earlier.setFullYear(1584);
102
+ const isLastYearNotFull = compare(later, earlier) === -sign;
103
+ return sign * (difference - Number(isLastYearNotFull)) || 0;
104
+ }
105
+ function compare(left, right) {
106
+ return Math.sign(left.getTime() - right.getTime());
107
+ }
108
+ function isLastDayOfMonth(date) {
109
+ const next = new Date(date.getTime());
110
+ next.setDate(next.getDate() + 1);
111
+ return next.getMonth() !== date.getMonth();
112
+ }
@@ -0,0 +1,131 @@
1
+ import { ResourceId } from '@appweaver/common';
2
+ /** The nested write actions a single relation field can be mapped to. */
3
+ export type RelationActions = Record<string, Partial<{
4
+ connect: any;
5
+ create: any;
6
+ update: any;
7
+ connectOrCreate: any;
8
+ disconnect: any;
9
+ delete: any;
10
+ }>>;
11
+ /** The records of every model affected by a delete, keyed by model name. */
12
+ export type AffectedRecords = Record<string, ResourceId[]>;
13
+ /** The records deleted by one action, split by how they were deleted. */
14
+ export type DeletedRecords = {
15
+ /** Records marked with the soft delete columns */
16
+ soft: AffectedRecords;
17
+ /** Records removed from the database */
18
+ hard: AffectedRecords;
19
+ };
20
+ /** The columns marking a record as soft deleted. */
21
+ export type SoftDeleteData = {
22
+ deletedAt: Date;
23
+ deletedById?: ResourceId | null;
24
+ };
25
+ /**
26
+ * Restricts a mapped relation filter to the related records that are not soft deleted, so a filter never matches
27
+ * through a deleted record, and a null check treats a deleted related record as missing.
28
+ *
29
+ * @param {*} condition - The mapped database condition of the relation field.
30
+ * @param {string} resourceName - The name of the related model.
31
+ * @param {boolean} isArrayType - Whether the relation is a list (to-many) relation.
32
+ * @return {*} The condition restricted to the live related records, or unchanged if the related model does not soft
33
+ * delete its records.
34
+ */
35
+ export declare function liveRelationFilter(condition: any, resourceName: string, isArrayType: boolean): any;
36
+ /**
37
+ * Builds the inclusion filter of a list relation, so only the related records that are not soft deleted are read.
38
+ *
39
+ * @param {string} [resourceName] - The name of the related model.
40
+ * @return {Object|undefined} The `where` clause of the inclusion, or undefined if the related model does not soft
41
+ * delete its records.
42
+ */
43
+ export declare function liveInclusionFilter(resourceName?: string): {
44
+ where: Record<string, any>;
45
+ } | undefined;
46
+ /**
47
+ * Replaces the soft deleted records in the single relations of a resource with null, recursively. Unlike the list
48
+ * relations, a single relation cannot be filtered when it is read.
49
+ *
50
+ * @param {Object} resource - The resource as returned by the database client.
51
+ * @param {string} resourceName - The name of the model describing the resource.
52
+ * @return {Object} A copy of the resource without the soft deleted related records, or the resource itself if it
53
+ * holds no relations.
54
+ */
55
+ export declare function hideDeletedRelations<T>(resource: T, resourceName: string): T;
56
+ /**
57
+ * Builds the column values marking a record as soft deleted now by the current user, who is only recorded when the
58
+ * application defines an auth model.
59
+ *
60
+ * @return {SoftDeleteData} The soft delete column values.
61
+ */
62
+ export declare function softDeleteData(): SoftDeleteData;
63
+ /**
64
+ * Mirrors the referential actions of a database delete for soft deleted records: cascading records are soft deleted
65
+ * with the same column values, level by level, a restricting relation aborts the delete, and references set to null on
66
+ * delete are kept for a manual restore. The records themselves are not updated.
67
+ *
68
+ * @param {Object} tx - The transaction client the delete runs in.
69
+ * @param {string} resourceName - The name of the model of the deleted records.
70
+ * @param {ResourceId[]} ids - The ids of the deleted records.
71
+ * @param {SoftDeleteData} data - The soft delete column values applied to the cascaded records.
72
+ * @return {Promise<AffectedRecords>} The ids of the soft deleted records per model, excluding the records themselves.
73
+ * @throws {ResourceError} `RESOURCE_DELETE_RESTRICTED` if a live record references a deleted record through a
74
+ * restricting relation.
75
+ */
76
+ export declare function softDeleteCascade(tx: any, resourceName: string, ids: ResourceId[], data: SoftDeleteData): Promise<AffectedRecords>;
77
+ /**
78
+ * Collects the records the database cascade removes together with deleted records, so their files and caches can be
79
+ * cleaned up once the delete commits.
80
+ *
81
+ * @param {Object} tx - The transaction client the delete runs in.
82
+ * @param {string} resourceName - The name of the model of the deleted records.
83
+ * @param {ResourceId[]} ids - The ids of the deleted records.
84
+ * @return {Promise<AffectedRecords>} The ids of the cascaded records per model, excluding the records themselves.
85
+ */
86
+ export declare function cascadedRecords(tx: any, resourceName: string, ids: ResourceId[]): Promise<AffectedRecords>;
87
+ /**
88
+ * Removes the orphans an update maps to a nested `delete` action (`orphanRemoval`). Orphans of a soft deleted model are
89
+ * soft deleted with their cascade and keep their references, so their `delete` action is dropped; the others are left
90
+ * to the `delete` action, collecting the records its cascade removes.
91
+ *
92
+ * @param {Object} tx - The transaction client the update runs in.
93
+ * @param {string} resourceName - The name of the model the relation actions belong to.
94
+ * @param {RelationActions} relationActions - The mapped relation write actions, from which the nested `delete` actions
95
+ * of the soft deleted orphans are removed.
96
+ * @param {SoftDeleteData} data - The soft delete column values applied to the soft deleted orphans.
97
+ * @return {Promise<DeletedRecords>} The ids of the soft deleted and the removed records per model.
98
+ * @throws {ResourceError} `RESOURCE_DELETE_RESTRICTED` if a live record references a soft deleted orphan through a
99
+ * restricting relation.
100
+ */
101
+ export declare function removeOrphans(tx: any, resourceName: string, relationActions: RelationActions, data: SoftDeleteData): Promise<DeletedRecords>;
102
+ /**
103
+ * Marks the file rows of the files the deleted records keep (`onResourceDeleted` / `onResourceSoftDeleted: 'keep'`) as
104
+ * deleted with the values of the delete, so they stay stored, e.g. for audit, but are no longer served.
105
+ *
106
+ * @param {Object} tx - The transaction client the delete runs in.
107
+ * @param {DeletedRecords} deleted - The soft deleted and the removed records.
108
+ * @param {SoftDeleteData} data - The soft delete column values of the delete.
109
+ * @return {Promise<void>} Resolves when the kept files are marked deleted.
110
+ */
111
+ export declare function retainDeletedFiles(tx: any, deleted: DeletedRecords, data: SoftDeleteData): Promise<void>;
112
+ /**
113
+ * Merges the affected records of several deletes into the target, keeping the ids of every model unique.
114
+ *
115
+ * @param {AffectedRecords} target - The affected records merged into.
116
+ * @param {AffectedRecords[]} sources - The affected records to add.
117
+ * @return {AffectedRecords} The target holding the ids of every source.
118
+ */
119
+ export declare function mergeAffectedRecords(target: AffectedRecords, ...sources: AffectedRecords[]): AffectedRecords;
120
+ /**
121
+ * Rejects the relation write actions pointing at soft deleted records, which the database would otherwise connect or
122
+ * update, since it does not know the record is deleted.
123
+ *
124
+ * @param {Object} client - The database client, or the transaction client the write runs in.
125
+ * @param {string} resourceName - The name of the model the relation actions belong to.
126
+ * @param {RelationActions} relationActions - The mapped relation write actions.
127
+ * @return {Promise<void>} Resolves when no action points at a soft deleted record.
128
+ * @throws {ResourceError} `RESOURCE_INVALID_RELATION` if a connect, update or connect-or-create action matches a soft
129
+ * deleted record.
130
+ */
131
+ export declare function assertLiveRelationTargets(client: any, resourceName: string, relationActions: RelationActions): Promise<void>;
@@ -0,0 +1,385 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.liveRelationFilter = liveRelationFilter;
4
+ exports.liveInclusionFilter = liveInclusionFilter;
5
+ exports.hideDeletedRelations = hideDeletedRelations;
6
+ exports.softDeleteData = softDeleteData;
7
+ exports.softDeleteCascade = softDeleteCascade;
8
+ exports.cascadedRecords = cascadedRecords;
9
+ exports.removeOrphans = removeOrphans;
10
+ exports.retainDeletedFiles = retainDeletedFiles;
11
+ exports.mergeAffectedRecords = mergeAffectedRecords;
12
+ exports.assertLiveRelationTargets = assertLiveRelationTargets;
13
+ const common_1 = require("@appweaver/common");
14
+ const internal_1 = require("@appweaver/core/internal");
15
+ const core_1 = require("@appweaver/core");
16
+ const helper_1 = require("../../security/helper");
17
+ const file_util_1 = require("../../utils/file-util");
18
+ const model_util_1 = require("../../utils/model-util");
19
+ /** The condition matching the records that are not soft deleted. */
20
+ const LIVE_RECORD = Object.freeze({ deletedAt: null });
21
+ /** The condition matching the soft deleted records. */
22
+ const DELETED_RECORD = Object.freeze({ deletedAt: { not: null } });
23
+ /** Referential actions that prevent deleting a record while others reference it. */
24
+ const RESTRICTING_ACTIONS = ['restrict', 'noAction'];
25
+ /**
26
+ * Restricts a mapped relation filter to the related records that are not soft deleted, so a filter never matches
27
+ * through a deleted record, and a null check treats a deleted related record as missing.
28
+ *
29
+ * @param {*} condition - The mapped database condition of the relation field.
30
+ * @param {string} resourceName - The name of the related model.
31
+ * @param {boolean} isArrayType - Whether the relation is a list (to-many) relation.
32
+ * @return {*} The condition restricted to the live related records, or unchanged if the related model does not soft
33
+ * delete its records.
34
+ */
35
+ function liveRelationFilter(condition, resourceName, isArrayType) {
36
+ if (!(0, model_util_1.isSoftDeleteModel)(resourceName)) {
37
+ return condition;
38
+ }
39
+ if (isArrayType) {
40
+ if (!(0, common_1.isPlainObject)(condition)) {
41
+ return condition;
42
+ }
43
+ const mapped = { ...condition };
44
+ for (const quantifier of ['some', 'none']) {
45
+ if (mapped[quantifier] !== undefined) {
46
+ mapped[quantifier] = { AND: [mapped[quantifier], LIVE_RECORD] };
47
+ }
48
+ }
49
+ if (mapped.every !== undefined) {
50
+ mapped.every = { OR: [mapped.every, DELETED_RECORD] };
51
+ }
52
+ return mapped;
53
+ }
54
+ if (condition === null) {
55
+ return { isNot: LIVE_RECORD };
56
+ }
57
+ if (!(0, common_1.isPlainObject)(condition)) {
58
+ return condition;
59
+ }
60
+ const { is, isNot, ...fields } = condition;
61
+ const matches = [];
62
+ const mapped = {};
63
+ if (is === null) {
64
+ mapped.isNot = LIVE_RECORD;
65
+ }
66
+ else if (is !== undefined) {
67
+ matches.push(is);
68
+ }
69
+ if (isNot === null) {
70
+ matches.push({});
71
+ }
72
+ else if (isNot !== undefined) {
73
+ mapped.isNot = { AND: [isNot, LIVE_RECORD] };
74
+ }
75
+ // The fields given without `is` are its shorthand
76
+ if (Object.keys(fields).length > 0) {
77
+ matches.push(fields);
78
+ }
79
+ if (matches.length > 0) {
80
+ mapped.is = { AND: [...matches, LIVE_RECORD] };
81
+ }
82
+ return mapped;
83
+ }
84
+ /**
85
+ * Builds the inclusion filter of a list relation, so only the related records that are not soft deleted are read.
86
+ *
87
+ * @param {string} [resourceName] - The name of the related model.
88
+ * @return {Object|undefined} The `where` clause of the inclusion, or undefined if the related model does not soft
89
+ * delete its records.
90
+ */
91
+ function liveInclusionFilter(resourceName) {
92
+ return (0, model_util_1.isSoftDeleteModel)(resourceName)
93
+ ? { where: { ...LIVE_RECORD } }
94
+ : undefined;
95
+ }
96
+ /**
97
+ * Replaces the soft deleted records in the single relations of a resource with null, recursively. Unlike the list
98
+ * relations, a single relation cannot be filtered when it is read.
99
+ *
100
+ * @param {Object} resource - The resource as returned by the database client.
101
+ * @param {string} resourceName - The name of the model describing the resource.
102
+ * @return {Object} A copy of the resource without the soft deleted related records, or the resource itself if it
103
+ * holds no relations.
104
+ */
105
+ function hideDeletedRelations(resource, resourceName) {
106
+ const relations = (0, core_1.injectModel)(resourceName, false)?.config?.relations;
107
+ if (!(0, common_1.isPlainObject)(resource) || !relations) {
108
+ return resource;
109
+ }
110
+ const projected = { ...resource };
111
+ for (const [key, relation] of Object.entries(relations)) {
112
+ const value = projected[key];
113
+ const relatedName = (0, common_1.capitalize)(relation.model);
114
+ if ((0, common_1.isArray)(value)) {
115
+ projected[key] = value.map((item) => hideDeletedRelations(item, relatedName));
116
+ }
117
+ else if ((0, common_1.isPlainObject)(value)) {
118
+ projected[key] =
119
+ value.deletedAt != null && (0, model_util_1.isSoftDeleteModel)(relatedName)
120
+ ? null
121
+ : hideDeletedRelations(value, relatedName);
122
+ }
123
+ }
124
+ return projected;
125
+ }
126
+ /**
127
+ * Builds the column values marking a record as soft deleted now by the current user, who is only recorded when the
128
+ * application defines an auth model.
129
+ *
130
+ * @return {SoftDeleteData} The soft delete column values.
131
+ */
132
+ function softDeleteData() {
133
+ const data = { deletedAt: new Date() };
134
+ if ((0, helper_1.resourceAuthModel)()) {
135
+ data.deletedById = (0, core_1.currentAuthUser)()?.id ?? null;
136
+ }
137
+ return data;
138
+ }
139
+ /**
140
+ * Mirrors the referential actions of a database delete for soft deleted records: cascading records are soft deleted
141
+ * with the same column values, level by level, a restricting relation aborts the delete, and references set to null on
142
+ * delete are kept for a manual restore. The records themselves are not updated.
143
+ *
144
+ * @param {Object} tx - The transaction client the delete runs in.
145
+ * @param {string} resourceName - The name of the model of the deleted records.
146
+ * @param {ResourceId[]} ids - The ids of the deleted records.
147
+ * @param {SoftDeleteData} data - The soft delete column values applied to the cascaded records.
148
+ * @return {Promise<AffectedRecords>} The ids of the soft deleted records per model, excluding the records themselves.
149
+ * @throws {ResourceError} `RESOURCE_DELETE_RESTRICTED` if a live record references a deleted record through a
150
+ * restricting relation.
151
+ */
152
+ async function softDeleteCascade(tx, resourceName, ids, data) {
153
+ return walkReferencingRecords(tx, resourceName, ids, async (relation) => {
154
+ const client = tx[(0, common_1.uncapitalize)(relation.modelName)];
155
+ const where = {
156
+ [relation.foreignKey]: { in: relation.ids },
157
+ ...(0, model_util_1.liveRecordFilter)(relation.modelName)
158
+ };
159
+ if (RESTRICTING_ACTIONS.includes(relation.onDelete)) {
160
+ await assertNotReferenced(client, where, relation);
161
+ return [];
162
+ }
163
+ if (relation.onDelete !== 'cascade') {
164
+ return [];
165
+ }
166
+ // Loading validates every cascade of a soft deleted model, so this only
167
+ // guards the models registered after it
168
+ if (!(0, model_util_1.isSoftDeleteModel)(relation.modelName)) {
169
+ throw new common_1.ConfigurationError(common_1.ErrorCode.ConfigurationInvalid, `${relation.modelName} must enable soft delete to cascade from ${relation.referencedName}`);
170
+ }
171
+ const records = await client.findMany({ where, select: { id: true } });
172
+ const cascadedIds = records.map((record) => record.id);
173
+ if (cascadedIds.length > 0) {
174
+ await client.updateMany({ where: { id: { in: cascadedIds } }, data });
175
+ }
176
+ return cascadedIds;
177
+ });
178
+ }
179
+ /**
180
+ * Collects the records the database cascade removes together with deleted records, so their files and caches can be
181
+ * cleaned up once the delete commits.
182
+ *
183
+ * @param {Object} tx - The transaction client the delete runs in.
184
+ * @param {string} resourceName - The name of the model of the deleted records.
185
+ * @param {ResourceId[]} ids - The ids of the deleted records.
186
+ * @return {Promise<AffectedRecords>} The ids of the cascaded records per model, excluding the records themselves.
187
+ */
188
+ async function cascadedRecords(tx, resourceName, ids) {
189
+ return walkReferencingRecords(tx, resourceName, ids, async (relation) => {
190
+ if (relation.onDelete !== 'cascade') {
191
+ return [];
192
+ }
193
+ // Soft deleted records are removed by the cascade as well
194
+ const records = await tx[(0, common_1.uncapitalize)(relation.modelName)].findMany({
195
+ where: { [relation.foreignKey]: { in: relation.ids } },
196
+ select: { id: true }
197
+ });
198
+ return records.map((record) => record.id);
199
+ });
200
+ }
201
+ /**
202
+ * Removes the orphans an update maps to a nested `delete` action (`orphanRemoval`). Orphans of a soft deleted model are
203
+ * soft deleted with their cascade and keep their references, so their `delete` action is dropped; the others are left
204
+ * to the `delete` action, collecting the records its cascade removes.
205
+ *
206
+ * @param {Object} tx - The transaction client the update runs in.
207
+ * @param {string} resourceName - The name of the model the relation actions belong to.
208
+ * @param {RelationActions} relationActions - The mapped relation write actions, from which the nested `delete` actions
209
+ * of the soft deleted orphans are removed.
210
+ * @param {SoftDeleteData} data - The soft delete column values applied to the soft deleted orphans.
211
+ * @return {Promise<DeletedRecords>} The ids of the soft deleted and the removed records per model.
212
+ * @throws {ResourceError} `RESOURCE_DELETE_RESTRICTED` if a live record references a soft deleted orphan through a
213
+ * restricting relation.
214
+ */
215
+ async function removeOrphans(tx, resourceName, relationActions, data) {
216
+ const relations = (0, core_1.injectModel)(resourceName, false)?.config?.relations ?? {};
217
+ const deleted = { soft: {}, hard: {} };
218
+ for (const [key, actions] of Object.entries(relationActions)) {
219
+ const relation = relations[key];
220
+ if (!actions?.delete || !relation) {
221
+ continue;
222
+ }
223
+ const relatedName = (0, common_1.capitalize)(relation.model);
224
+ const client = tx[(0, common_1.uncapitalize)(relatedName)];
225
+ const orphans = await client.findMany({
226
+ where: { OR: asList(actions.delete) },
227
+ select: { id: true }
228
+ });
229
+ const ids = orphans.map((orphan) => orphan.id);
230
+ if (ids.length === 0) {
231
+ continue;
232
+ }
233
+ if ((0, model_util_1.isSoftDeleteModel)(relatedName)) {
234
+ // Left undefined rather than removed, so the raw input value of the
235
+ // relation does not reach the database write in its place
236
+ delete actions.delete;
237
+ if (Object.keys(actions).length === 0) {
238
+ relationActions[key] = undefined;
239
+ }
240
+ const cascaded = await softDeleteCascade(tx, relatedName, ids, data);
241
+ await client.updateMany({ where: { id: { in: ids } }, data });
242
+ mergeAffectedRecords(deleted.soft, { [relatedName]: ids }, cascaded);
243
+ }
244
+ else {
245
+ const cascaded = await cascadedRecords(tx, relatedName, ids);
246
+ mergeAffectedRecords(deleted.hard, { [relatedName]: ids }, cascaded);
247
+ }
248
+ }
249
+ return deleted;
250
+ }
251
+ /**
252
+ * Marks the file rows of the files the deleted records keep (`onResourceDeleted` / `onResourceSoftDeleted: 'keep'`) as
253
+ * deleted with the values of the delete, so they stay stored, e.g. for audit, but are no longer served.
254
+ *
255
+ * @param {Object} tx - The transaction client the delete runs in.
256
+ * @param {DeletedRecords} deleted - The soft deleted and the removed records.
257
+ * @param {SoftDeleteData} data - The soft delete column values of the delete.
258
+ * @return {Promise<void>} Resolves when the kept files are marked deleted.
259
+ */
260
+ async function retainDeletedFiles(tx, deleted, data) {
261
+ const groups = [
262
+ [true, deleted.soft],
263
+ [false, deleted.hard]
264
+ ];
265
+ for (const [softDeleted, records] of groups) {
266
+ for (const [modelName, ids] of Object.entries(records)) {
267
+ const files = (0, core_1.injectModel)(modelName, false)?.config.files;
268
+ const { kept } = (0, file_util_1.deletedResourceFileFields)(files, softDeleted);
269
+ if (kept.length === 0 || ids.length === 0) {
270
+ continue;
271
+ }
272
+ await tx.file.updateMany({
273
+ where: {
274
+ resourceName: modelName,
275
+ // Owning ids are stored as text, whatever the model primary key is
276
+ resourceId: { in: ids.map(String) },
277
+ resourceField: { in: kept },
278
+ ...(0, model_util_1.liveRecordFilter)('File')
279
+ },
280
+ data
281
+ });
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Merges the affected records of several deletes into the target, keeping the ids of every model unique.
287
+ *
288
+ * @param {AffectedRecords} target - The affected records merged into.
289
+ * @param {AffectedRecords[]} sources - The affected records to add.
290
+ * @return {AffectedRecords} The target holding the ids of every source.
291
+ */
292
+ function mergeAffectedRecords(target, ...sources) {
293
+ for (const source of sources) {
294
+ for (const [modelName, ids] of Object.entries(source)) {
295
+ const seen = new Set((target[modelName] ?? []).map(String));
296
+ target[modelName] = [
297
+ ...(target[modelName] ?? []),
298
+ ...ids.filter((id) => !seen.has(String(id)))
299
+ ];
300
+ }
301
+ }
302
+ return target;
303
+ }
304
+ /**
305
+ * Rejects the relation write actions pointing at soft deleted records, which the database would otherwise connect or
306
+ * update, since it does not know the record is deleted.
307
+ *
308
+ * @param {Object} client - The database client, or the transaction client the write runs in.
309
+ * @param {string} resourceName - The name of the model the relation actions belong to.
310
+ * @param {RelationActions} relationActions - The mapped relation write actions.
311
+ * @return {Promise<void>} Resolves when no action points at a soft deleted record.
312
+ * @throws {ResourceError} `RESOURCE_INVALID_RELATION` if a connect, update or connect-or-create action matches a soft
313
+ * deleted record.
314
+ */
315
+ async function assertLiveRelationTargets(client, resourceName, relationActions) {
316
+ const relations = (0, core_1.injectModel)(resourceName, false)?.config?.relations ?? {};
317
+ for (const [key, actions] of Object.entries(relationActions)) {
318
+ const relatedName = relations[key]?.model;
319
+ if (!actions || !(0, model_util_1.isSoftDeleteModel)(relatedName)) {
320
+ continue;
321
+ }
322
+ const matches = [
323
+ ...asList(actions.connect).map((item) => ({ id: item.id })),
324
+ ...asList(actions.update).map((item) => item.where),
325
+ ...asList(actions.connectOrCreate).map((item) => item.where)
326
+ ];
327
+ if (matches.length === 0) {
328
+ continue;
329
+ }
330
+ const deletedCount = await client[(0, common_1.uncapitalize)(relatedName)].count({
331
+ where: { AND: [DELETED_RECORD, { OR: matches }] }
332
+ });
333
+ if (deletedCount > 0) {
334
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceInvalidRelation, `${resourceName} relation '${key}' references a ${(0, common_1.capitalize)(relatedName)} record that does not exist`, { model: resourceName, relation: key });
335
+ }
336
+ }
337
+ }
338
+ /** Wraps a single relation action value into a list. */
339
+ function asList(value) {
340
+ return value === undefined ? [] : (0, common_1.isArray)(value) ? value : [value];
341
+ }
342
+ /**
343
+ * Rejects a delete while records reference the deleted ones through a restricting relation.
344
+ */
345
+ async function assertNotReferenced(client, where, relation) {
346
+ const count = await client.count({ where });
347
+ if (count > 0) {
348
+ throw new common_1.ResourceError(common_1.ErrorCode.ResourceDeleteRestricted, `${relation.referencedName} cannot be deleted while ${relation.modelName} records reference it`, { model: relation.referencedName, referencedBy: relation.modelName });
349
+ }
350
+ }
351
+ /**
352
+ * Walks the records referencing the given records breadth first. The visitor gets every referencing relation with the
353
+ * referenced ids and returns the ids of the records the walk continues from.
354
+ */
355
+ async function walkReferencingRecords(tx, resourceName, ids, visit) {
356
+ const models = Object.fromEntries(internal_1.context.resource.models);
357
+ const affected = {};
358
+ const queue = [
359
+ { modelName: resourceName, ids }
360
+ ];
361
+ while (queue.length > 0) {
362
+ const level = queue.shift();
363
+ for (const relation of (0, common_1.referencingRelations)(models, level.modelName)) {
364
+ const visitedIds = await visit({
365
+ ...relation,
366
+ ids: level.ids,
367
+ referencedName: level.modelName
368
+ });
369
+ // A record reached twice, i.e. through a self relation, is walked once
370
+ const seen = new Set((affected[relation.modelName] ?? []).map(String));
371
+ if (relation.modelName === resourceName) {
372
+ ids.forEach((id) => seen.add(String(id)));
373
+ }
374
+ const newIds = visitedIds.filter((value) => !seen.has(String(value)));
375
+ if (newIds.length > 0) {
376
+ affected[relation.modelName] = [
377
+ ...(affected[relation.modelName] ?? []),
378
+ ...newIds
379
+ ];
380
+ queue.push({ modelName: relation.modelName, ids: newIds });
381
+ }
382
+ }
383
+ }
384
+ return affected;
385
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Maps a request filter to a Prisma `where` clause based on the schema of the
3
+ * given resource model. The logical operators (`_and`, `_or`, `_not`, `_nor`)
4
+ * combine their nested conditions with the matching connective, the field
5
+ * operator objects (`_eq`, `_gt`, `_like`, `_exists`, ...) are translated to
6
+ * their database conditions, and the list relation quantifiers (`_some`,
7
+ * `_every`, `_none`) filter the related records. Plain values keep their
8
+ * shorthand meaning: relation and file fields are matched by id (wrapped in a
9
+ * `some` condition for the array relations), array fields use the `has` and
10
+ * `hasSome` operators, an array value on a numeric or date field becomes an
11
+ * inclusive `gte`/`lte` range, other array values become an `in` inclusion,
12
+ * nested objects are mapped recursively against the related model, and null
13
+ * values are passed through to match records without a value or a related
14
+ * record. Values that match no known field are passed through with only their
15
+ * operators translated, so the Prisma operators can still be used directly.
16
+ *
17
+ * @param {Object} filter The request filter object to map.
18
+ * @param {string} resourceName The name of the model the filter is matched
19
+ * against. It is set to the related model name when recursing into a nested
20
+ * relation or file filter.
21
+ * @return {Object} The `where` clause with every recognized field mapped to its
22
+ * database condition and the unrecognized values passed through unchanged.
23
+ */
24
+ export declare function mapQueryFilter(filter: any, resourceName: string): any;