@adobe/spacecat-shared-data-access 2.108.0 → 3.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/CHANGELOG.md +21 -0
- package/README.md +260 -245
- package/docker-compose.test.yml +39 -0
- package/package.json +5 -6
- package/src/index.js +16 -5
- package/src/models/audit/audit.collection.js +6 -20
- package/src/models/base/base.collection.js +699 -395
- package/src/models/base/base.model.js +13 -6
- package/src/models/base/entity.registry.js +25 -5
- package/src/models/base/schema.builder.js +2 -0
- package/src/models/base/schema.js +15 -4
- package/src/models/consumer/consumer.collection.js +149 -0
- package/src/models/consumer/consumer.model.js +61 -0
- package/src/models/consumer/consumer.schema.js +65 -0
- package/src/models/consumer/index.d.ts +39 -0
- package/src/models/consumer/index.js +19 -0
- package/src/models/fix-entity-suggestion/fix-entity-suggestion.model.js +15 -0
- package/src/models/fix-entity-suggestion/fix-entity-suggestion.schema.js +12 -0
- package/src/models/index.d.ts +1 -0
- package/src/models/index.js +1 -0
- package/src/models/key-event/key-event.collection.js +23 -1
- package/src/models/latest-audit/latest-audit.collection.js +77 -3
- package/src/models/scrape-job/scrape-job.collection.js +38 -0
- package/src/models/scrape-job/scrape-job.schema.js +4 -2
- package/src/models/site/site.schema.js +1 -0
- package/src/models/site-enrollment/site-enrollment.collection.js +11 -1
- package/src/models/trial-user/trial-user.collection.js +0 -2
- package/src/models/trial-user/trial-user.schema.js +5 -1
- package/src/service/index.d.ts +7 -1
- package/src/service/index.js +33 -52
- package/src/util/index.js +2 -1
- package/src/util/logger-registry.js +12 -0
- package/src/util/patcher.js +64 -112
- package/src/util/postgrest.utils.js +203 -0
- package/tsconfig.json +24 -0
|
@@ -10,8 +10,10 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import { isNonEmptyArray } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
14
15
|
import { guardId, guardString } from '../../util/index.js';
|
|
16
|
+
import BaseCollection from '../base/base.collection.js';
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* LatestAuditCollection - A collection class responsible for managing LatestAudit entities.
|
|
@@ -23,8 +25,80 @@ import { guardId, guardString } from '../../util/index.js';
|
|
|
23
25
|
class LatestAuditCollection extends BaseCollection {
|
|
24
26
|
static COLLECTION_NAME = 'LatestAuditCollection';
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
// LatestAudit is a virtual view in v3; writes are not supported.
|
|
29
|
+
// eslint-disable-next-line class-methods-use-this
|
|
30
|
+
async create() {
|
|
31
|
+
throw new DataAccessError('LatestAudit is derived from Audit in v3 and cannot be created directly', this);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line class-methods-use-this
|
|
35
|
+
async createMany() {
|
|
36
|
+
throw new DataAccessError('LatestAudit is derived from Audit in v3 and cannot be created directly', this);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
static #groupLatest(items, groupFields) {
|
|
40
|
+
const grouped = new Map();
|
|
41
|
+
items.forEach((item) => {
|
|
42
|
+
const key = groupFields.map((field) => item.record[field]).join('#');
|
|
43
|
+
const existing = grouped.get(key);
|
|
44
|
+
if (!existing || existing.getAuditedAt() < item.getAuditedAt()) {
|
|
45
|
+
grouped.set(key, item);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
return [...grouped.values()];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async #allAuditsByKeys(keys, options = {}) {
|
|
52
|
+
const auditCollection = this.entityRegistry.getCollection('AuditCollection');
|
|
53
|
+
return auditCollection.allByIndexKeys(keys, {
|
|
54
|
+
...options,
|
|
55
|
+
fetchAllPages: true,
|
|
56
|
+
order: 'desc',
|
|
57
|
+
returnCursor: false,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async all(sortKeys = {}, options = {}) {
|
|
62
|
+
return this.allByIndexKeys(sortKeys, options);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async findByAll(sortKeys = {}, options = {}) {
|
|
66
|
+
return this.findByIndexKeys(sortKeys, options);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async findByIndexKeys(keys, options = {}) {
|
|
70
|
+
const auditCollection = this.entityRegistry.getCollection('AuditCollection');
|
|
71
|
+
|
|
72
|
+
if (keys.siteId && keys.auditType) {
|
|
73
|
+
return auditCollection.findByIndexKeys(keys, { ...options, order: 'desc' });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// For single-key lookups, we only need the latest row, not all pages.
|
|
77
|
+
return auditCollection.findByIndexKeys(keys, { ...options, order: 'desc' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async allByIndexKeys(keys, options = {}) {
|
|
81
|
+
const audits = await this.#allAuditsByKeys(keys, options);
|
|
82
|
+
if (!isNonEmptyArray(audits)) {
|
|
83
|
+
return options.returnCursor ? { data: [], cursor: null } : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let groupFields = ['siteId', 'auditType'];
|
|
87
|
+
if (keys.siteId && !keys.auditType) {
|
|
88
|
+
groupFields = ['auditType'];
|
|
89
|
+
} else if (!keys.siteId && keys.auditType) {
|
|
90
|
+
groupFields = ['siteId'];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const latest = LatestAuditCollection.#groupLatest(audits, groupFields);
|
|
94
|
+
// Preserve v2 behavior: default order is descending (most recent first).
|
|
95
|
+
const ascending = options.order === 'asc';
|
|
96
|
+
latest.sort((a, b) => (ascending
|
|
97
|
+
? a.getAuditedAt().localeCompare(b.getAuditedAt())
|
|
98
|
+
: b.getAuditedAt().localeCompare(a.getAuditedAt())));
|
|
99
|
+
const limited = Number.isInteger(options.limit) ? latest.slice(0, options.limit) : latest;
|
|
100
|
+
|
|
101
|
+
return options.returnCursor ? { data: limited, cursor: null } : limited;
|
|
28
102
|
}
|
|
29
103
|
|
|
30
104
|
async allByAuditType(auditType) {
|
|
@@ -42,6 +42,44 @@ class ScrapeJobCollection extends BaseCollection {
|
|
|
42
42
|
},
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
async allByBaseURLAndProcessingTypeAndOptEnableJavascriptAndOptHideConsentBanner(
|
|
47
|
+
baseURL,
|
|
48
|
+
processingType,
|
|
49
|
+
optEnableJavascript,
|
|
50
|
+
optHideConsentBanner,
|
|
51
|
+
options = {},
|
|
52
|
+
) {
|
|
53
|
+
const keys = {
|
|
54
|
+
baseURL,
|
|
55
|
+
processingType,
|
|
56
|
+
optEnableJavascript,
|
|
57
|
+
optHideConsentBanner,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
return this.allByIndexKeys(keys, options);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async findByBaseURLAndProcessingTypeAndOptEnableJavascriptAndOptHideConsentBanner(
|
|
64
|
+
baseURL,
|
|
65
|
+
processingType,
|
|
66
|
+
optEnableJavascript,
|
|
67
|
+
optHideConsentBanner,
|
|
68
|
+
options = {},
|
|
69
|
+
) {
|
|
70
|
+
const jobs = await this
|
|
71
|
+
.allByBaseURLAndProcessingTypeAndOptEnableJavascriptAndOptHideConsentBanner(
|
|
72
|
+
baseURL,
|
|
73
|
+
processingType,
|
|
74
|
+
optEnableJavascript,
|
|
75
|
+
optHideConsentBanner,
|
|
76
|
+
{ ...options, limit: options.limit || 1 },
|
|
77
|
+
);
|
|
78
|
+
if (Array.isArray(jobs)) {
|
|
79
|
+
return jobs[0] || null;
|
|
80
|
+
}
|
|
81
|
+
return jobs || null;
|
|
82
|
+
}
|
|
45
83
|
}
|
|
46
84
|
|
|
47
85
|
export default ScrapeJobCollection;
|
|
@@ -130,17 +130,19 @@ const schema = new SchemaBuilder(ScrapeJob, ScrapeJobCollection)
|
|
|
130
130
|
})
|
|
131
131
|
.addAttribute('optEnableJavascript', {
|
|
132
132
|
type: 'string',
|
|
133
|
+
postgrestIgnore: true,
|
|
133
134
|
hidden: true,
|
|
134
135
|
readOnly: true,
|
|
135
136
|
watch: ['options'],
|
|
136
|
-
set: (_, { options }) => (options[ScrapeJob.ScrapeOptions.ENABLE_JAVASCRIPT] ? 'T' : 'F'),
|
|
137
|
+
set: (_, { options }) => (options?.[ScrapeJob.ScrapeOptions.ENABLE_JAVASCRIPT] ? 'T' : 'F'),
|
|
137
138
|
})
|
|
138
139
|
.addAttribute('optHideConsentBanner', {
|
|
139
140
|
type: 'string',
|
|
141
|
+
postgrestIgnore: true,
|
|
140
142
|
hidden: true,
|
|
141
143
|
readOnly: true,
|
|
142
144
|
watch: ['options'],
|
|
143
|
-
set: (_, { options }) => (options[ScrapeJob.ScrapeOptions.HIDE_CONSENT_BANNER] ? 'T' : 'F'),
|
|
145
|
+
set: (_, { options }) => (options?.[ScrapeJob.ScrapeOptions.HIDE_CONSENT_BANNER] ? 'T' : 'F'),
|
|
144
146
|
})
|
|
145
147
|
// access pattern: get all jobs sorted by startedAt
|
|
146
148
|
.addAllIndex(['startedAt'])
|
|
@@ -22,7 +22,17 @@ import BaseCollection from '../base/base.collection.js';
|
|
|
22
22
|
class SiteEnrollmentCollection extends BaseCollection {
|
|
23
23
|
static COLLECTION_NAME = 'SiteEnrollmentCollection';
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
async create(item, options = {}) {
|
|
26
|
+
if (item?.siteId && item?.entitlementId) {
|
|
27
|
+
const existing = await this.findByIndexKeys({
|
|
28
|
+
siteId: item.siteId,
|
|
29
|
+
entitlementId: item.entitlementId,
|
|
30
|
+
});
|
|
31
|
+
if (existing) return existing;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return super.create(item, options);
|
|
35
|
+
}
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
export default SiteEnrollmentCollection;
|
|
@@ -54,6 +54,10 @@ const schema = new SchemaBuilder(TrialUser, TrialUserCollection)
|
|
|
54
54
|
type: 'any',
|
|
55
55
|
validate: (value) => !value || isObject(value),
|
|
56
56
|
})
|
|
57
|
-
.addAllIndex(['emailId'])
|
|
57
|
+
.addAllIndex(['emailId'])
|
|
58
|
+
.addIndex(
|
|
59
|
+
{ composite: ['organizationId'] },
|
|
60
|
+
{ composite: ['updatedAt'] },
|
|
61
|
+
);
|
|
58
62
|
|
|
59
63
|
export default schema.build();
|
package/src/service/index.d.ts
CHANGED
|
@@ -11,10 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
interface DataAccessConfig {
|
|
14
|
-
|
|
14
|
+
postgrestUrl: string;
|
|
15
|
+
postgrestSchema?: string;
|
|
16
|
+
postgrestApiKey?: string;
|
|
17
|
+
postgrestHeaders?: object;
|
|
18
|
+
s3Bucket?: string;
|
|
19
|
+
region?: string;
|
|
15
20
|
}
|
|
16
21
|
|
|
17
22
|
export function createDataAccess(
|
|
18
23
|
config: DataAccessConfig,
|
|
19
24
|
logger: object,
|
|
25
|
+
client?: object,
|
|
20
26
|
): object;
|
package/src/service/index.js
CHANGED
|
@@ -10,10 +10,8 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
14
|
-
import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
|
|
15
13
|
import { S3Client } from '@aws-sdk/client-s3';
|
|
16
|
-
import {
|
|
14
|
+
import { PostgrestClient } from '@supabase/postgrest-js';
|
|
17
15
|
|
|
18
16
|
import { instrumentAWSClient } from '@adobe/spacecat-shared-utils';
|
|
19
17
|
import { EntityRegistry } from '../models/index.js';
|
|
@@ -23,47 +21,31 @@ export * from '../errors/index.js';
|
|
|
23
21
|
export * from '../models/index.js';
|
|
24
22
|
export * from '../util/index.js';
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const createRawClient = (client = undefined) => {
|
|
30
|
-
const rawClient = client || (() => {
|
|
31
|
-
if (!defaultDynamoDBClient) {
|
|
32
|
-
defaultDynamoDBClient = new DynamoDB();
|
|
33
|
-
}
|
|
34
|
-
return defaultDynamoDBClient;
|
|
35
|
-
})();
|
|
36
|
-
|
|
37
|
-
let documentClient = documentClientCache.get(rawClient);
|
|
38
|
-
if (!documentClient) {
|
|
39
|
-
documentClient = DynamoDBDocument.from(instrumentAWSClient(rawClient), {
|
|
40
|
-
marshallOptions: {
|
|
41
|
-
convertEmptyValues: true,
|
|
42
|
-
removeUndefinedValues: true,
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
documentClientCache.set(rawClient, documentClient);
|
|
24
|
+
const createPostgrestService = (config, client = undefined) => {
|
|
25
|
+
if (client) {
|
|
26
|
+
return client;
|
|
46
27
|
}
|
|
47
28
|
|
|
48
|
-
|
|
49
|
-
|
|
29
|
+
const {
|
|
30
|
+
postgrestUrl,
|
|
31
|
+
postgrestSchema = 'public',
|
|
32
|
+
postgrestApiKey,
|
|
33
|
+
postgrestHeaders = {},
|
|
34
|
+
} = config;
|
|
35
|
+
|
|
36
|
+
if (!postgrestUrl) {
|
|
37
|
+
throw new Error('postgrestUrl is required to create data access');
|
|
38
|
+
}
|
|
50
39
|
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const logger = (event) => {
|
|
55
|
-
log.debug(JSON.stringify(event, null, 4));
|
|
40
|
+
const headers = {
|
|
41
|
+
...postgrestHeaders,
|
|
42
|
+
...postgrestApiKey ? { apikey: postgrestApiKey, Authorization: `Bearer ${postgrestApiKey}` } : {},
|
|
56
43
|
};
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
client,
|
|
63
|
-
table,
|
|
64
|
-
logger,
|
|
65
|
-
},
|
|
66
|
-
);
|
|
44
|
+
|
|
45
|
+
return new PostgrestClient(postgrestUrl, {
|
|
46
|
+
schema: postgrestSchema,
|
|
47
|
+
headers,
|
|
48
|
+
});
|
|
67
49
|
};
|
|
68
50
|
|
|
69
51
|
/**
|
|
@@ -91,31 +73,30 @@ const createS3Service = (config) => {
|
|
|
91
73
|
* Creates a services dictionary containing all datastore services.
|
|
92
74
|
* Each collection can declare which service it needs via its DATASTORE_TYPE.
|
|
93
75
|
*
|
|
94
|
-
* @param {
|
|
76
|
+
* @param {PostgrestClient} postgrestService - PostgREST client
|
|
95
77
|
* @param {object} config - Configuration object
|
|
96
|
-
* @returns {object} Services dictionary with
|
|
78
|
+
* @returns {object} Services dictionary with postgrest and s3 services
|
|
97
79
|
*/
|
|
98
|
-
const createServices = (
|
|
99
|
-
|
|
80
|
+
const createServices = (postgrestService, config) => ({
|
|
81
|
+
postgrest: postgrestService,
|
|
100
82
|
s3: createS3Service(config),
|
|
101
83
|
});
|
|
102
84
|
|
|
103
85
|
/**
|
|
104
|
-
* Creates a data access layer for interacting with
|
|
86
|
+
* Creates a data access layer for interacting with Postgres via PostgREST.
|
|
105
87
|
*
|
|
106
|
-
* @param {{
|
|
107
|
-
*
|
|
88
|
+
* @param {{postgrestUrl: string, postgrestSchema?: string, postgrestApiKey?: string,
|
|
89
|
+
* postgrestHeaders?: object, s3Bucket?: string, region?: string}} config - Configuration object
|
|
108
90
|
* @param {object} log - Logger instance, defaults to console
|
|
109
|
-
* @param {
|
|
91
|
+
* @param {PostgrestClient} [client] - Optional custom Postgrest client instance
|
|
110
92
|
* @returns {object} Data access collections for interacting with entities
|
|
111
93
|
*/
|
|
112
94
|
export const createDataAccess = (config, log = console, client = undefined) => {
|
|
113
95
|
registerLogger(log);
|
|
114
96
|
|
|
115
|
-
const
|
|
116
|
-
const
|
|
117
|
-
const
|
|
118
|
-
const entityRegistry = new EntityRegistry(services, log);
|
|
97
|
+
const postgrestService = createPostgrestService(config, client);
|
|
98
|
+
const services = createServices(postgrestService, config);
|
|
99
|
+
const entityRegistry = new EntityRegistry(services, config, log);
|
|
119
100
|
|
|
120
101
|
return entityRegistry.getCollections();
|
|
121
102
|
};
|
package/src/util/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export {
|
|
|
25
25
|
export {
|
|
26
26
|
registerLogger,
|
|
27
27
|
getLogger,
|
|
28
|
+
resetLoggerRegistry,
|
|
28
29
|
} from './logger-registry.js';
|
|
29
30
|
|
|
30
31
|
/**
|
|
@@ -33,6 +34,6 @@ export {
|
|
|
33
34
|
* @enum {string}
|
|
34
35
|
*/
|
|
35
36
|
export const DATASTORE_TYPE = Object.freeze({
|
|
36
|
-
|
|
37
|
+
POSTGREST: 'postgrest',
|
|
37
38
|
S3: 's3',
|
|
38
39
|
});
|
|
@@ -29,6 +29,10 @@ class LoggerRegistry {
|
|
|
29
29
|
getLogger() {
|
|
30
30
|
return this.#logger || console;
|
|
31
31
|
}
|
|
32
|
+
|
|
33
|
+
reset() {
|
|
34
|
+
this.#logger = null;
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
/**
|
|
@@ -48,3 +52,11 @@ export function registerLogger(logger) {
|
|
|
48
52
|
export function getLogger() {
|
|
49
53
|
return LoggerRegistry.getInstance().getLogger();
|
|
50
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Resets the registered logger to default console logger.
|
|
58
|
+
* Primarily intended for test isolation.
|
|
59
|
+
*/
|
|
60
|
+
export function resetLoggerRegistry() {
|
|
61
|
+
LoggerRegistry.getInstance().reset();
|
|
62
|
+
}
|
package/src/util/patcher.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { isObject } from '@adobe/spacecat-shared-utils';
|
|
14
14
|
|
|
15
15
|
import ValidationError from '../errors/validation.error.js';
|
|
16
16
|
|
|
@@ -26,13 +26,6 @@ import {
|
|
|
26
26
|
guardString,
|
|
27
27
|
} from './index.js';
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Checks if a property is read-only and throws an error if it is.
|
|
31
|
-
* @param {string} propertyName - The name of the property to check.
|
|
32
|
-
* @param {Object} attribute - The attribute to check.
|
|
33
|
-
* @throws {Error} - Throws an error if the property is read-only.
|
|
34
|
-
* @private
|
|
35
|
-
*/
|
|
36
29
|
const checkReadOnly = (propertyName, attribute) => {
|
|
37
30
|
if (attribute.readOnly) {
|
|
38
31
|
throw new ValidationError(`The property ${propertyName} is read-only and cannot be updated.`);
|
|
@@ -48,137 +41,68 @@ const checkUpdatesAllowed = (schema) => {
|
|
|
48
41
|
class Patcher {
|
|
49
42
|
/**
|
|
50
43
|
* Creates a new Patcher instance for an entity.
|
|
51
|
-
* @param {object}
|
|
44
|
+
* @param {object} collection - The backing collection instance.
|
|
52
45
|
* @param {Schema} schema - The schema for the entity.
|
|
53
46
|
* @param {object} record - The record to patch.
|
|
54
47
|
*/
|
|
55
|
-
constructor(
|
|
56
|
-
this.
|
|
48
|
+
constructor(collection, schema, record) {
|
|
49
|
+
this.collection = collection;
|
|
57
50
|
this.schema = schema;
|
|
58
51
|
this.record = record;
|
|
59
52
|
|
|
60
53
|
this.entityName = schema.getEntityName();
|
|
61
|
-
this.model = entity.model;
|
|
62
54
|
this.idName = schema.getIdName();
|
|
63
55
|
|
|
64
|
-
// holds the previous value of updated attributes
|
|
65
56
|
this.previous = {};
|
|
66
|
-
|
|
67
|
-
// holds the updates to the attributes
|
|
68
57
|
this.updates = {};
|
|
69
58
|
|
|
59
|
+
this.legacyEntity = collection && typeof collection.patch === 'function'
|
|
60
|
+
? collection
|
|
61
|
+
: null;
|
|
70
62
|
this.patchRecord = null;
|
|
71
63
|
}
|
|
72
64
|
|
|
73
|
-
/**
|
|
74
|
-
* Checks if a property is nullable.
|
|
75
|
-
* @param {string} propertyName - The name of the property to check.
|
|
76
|
-
* @return {boolean} True if the property is nullable, false otherwise.
|
|
77
|
-
* @private
|
|
78
|
-
*/
|
|
79
65
|
#isAttributeNullable(propertyName) {
|
|
80
|
-
return !this.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Composite keys have to be provided to ElectroDB in order to update a record across
|
|
85
|
-
* multiple indexes. This method retrieves the composite values for the entity from
|
|
86
|
-
* the schema indexes and filters out any values that are being updated.
|
|
87
|
-
* @return {{}} - An object containing the composite values for the entity.
|
|
88
|
-
* @private
|
|
89
|
-
*/
|
|
90
|
-
#getCompositeValues() {
|
|
91
|
-
const { indexes } = this.model;
|
|
92
|
-
const result = {};
|
|
93
|
-
|
|
94
|
-
const processComposite = (index, compositeType) => {
|
|
95
|
-
const compositeArray = index[compositeType]?.facets;
|
|
96
|
-
if (isNonEmptyArray(compositeArray)) {
|
|
97
|
-
compositeArray.forEach((compositeKey) => {
|
|
98
|
-
if (
|
|
99
|
-
!Object.keys(this.updates).includes(compositeKey)
|
|
100
|
-
&& this.record[compositeKey] !== undefined
|
|
101
|
-
) {
|
|
102
|
-
result[compositeKey] = this.record[compositeKey];
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
Object.values(indexes).forEach((index) => {
|
|
109
|
-
processComposite(index, 'pk');
|
|
110
|
-
processComposite(index, 'sk');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
return result;
|
|
66
|
+
return !this.schema.getAttribute(propertyName)?.required;
|
|
114
67
|
}
|
|
115
68
|
|
|
116
|
-
|
|
117
|
-
* Sets a property on the record and updates the patch record.
|
|
118
|
-
* @param {string} attribute - The attribute to set.
|
|
119
|
-
* @param {any} value - The value to set for the property.
|
|
120
|
-
* @private
|
|
121
|
-
*/
|
|
122
|
-
#set(attribute, value) {
|
|
123
|
-
this.patchRecord = this.#getPatchRecord().set({ [attribute.name]: value });
|
|
124
|
-
|
|
125
|
-
const transmutedValue = attribute.get(value, () => {});
|
|
69
|
+
#set(propertyName, attribute, value) {
|
|
126
70
|
const update = {
|
|
127
|
-
[
|
|
128
|
-
previous: this.record[
|
|
129
|
-
current:
|
|
71
|
+
[propertyName]: {
|
|
72
|
+
previous: this.record[propertyName],
|
|
73
|
+
current: value,
|
|
130
74
|
},
|
|
131
75
|
};
|
|
132
76
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
77
|
+
const hydratedValue = typeof attribute.get === 'function'
|
|
78
|
+
? attribute.get(value)
|
|
79
|
+
: value;
|
|
80
|
+
this.record[propertyName] = hydratedValue;
|
|
137
81
|
this.updates = { ...this.updates, ...update };
|
|
82
|
+
|
|
83
|
+
if (this.legacyEntity) {
|
|
84
|
+
if (!this.patchRecord) {
|
|
85
|
+
this.patchRecord = this.legacyEntity.patch(this.#getPrimaryKeyValues());
|
|
86
|
+
}
|
|
87
|
+
this.patchRecord = this.patchRecord.set({ [propertyName]: value });
|
|
88
|
+
}
|
|
138
89
|
}
|
|
139
90
|
|
|
140
|
-
/**
|
|
141
|
-
* Gets the primary key values for the entity from the schema's primary index.
|
|
142
|
-
* This supports composite primary keys (e.g., siteId + url).
|
|
143
|
-
* @return {Object} - An object containing the primary key values.
|
|
144
|
-
* @private
|
|
145
|
-
*/
|
|
146
91
|
#getPrimaryKeyValues() {
|
|
147
92
|
const primaryKeys = this.schema.getIndexKeys('primary');
|
|
148
|
-
if (
|
|
93
|
+
if (Array.isArray(primaryKeys) && primaryKeys.length > 0) {
|
|
149
94
|
return primaryKeys.reduce((acc, key) => {
|
|
150
95
|
acc[key] = this.record[key];
|
|
151
96
|
return acc;
|
|
152
97
|
}, {});
|
|
153
98
|
}
|
|
154
|
-
// Fallback to default id name
|
|
155
99
|
return { [this.idName]: this.record[this.idName] };
|
|
156
100
|
}
|
|
157
101
|
|
|
158
|
-
/**
|
|
159
|
-
* Gets the patch record for the entity. If it does not exist, it will be created.
|
|
160
|
-
* @return {Object} - The patch record for the entity.
|
|
161
|
-
* @private
|
|
162
|
-
*/
|
|
163
|
-
#getPatchRecord() {
|
|
164
|
-
if (!this.patchRecord) {
|
|
165
|
-
this.patchRecord = this.entity.patch(this.#getPrimaryKeyValues());
|
|
166
|
-
}
|
|
167
|
-
return this.patchRecord;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Patches a value for a given property on the entity. This method will validate the value
|
|
172
|
-
* against the schema and throw an error if the value is invalid. If the value is declared as
|
|
173
|
-
* a reference, it will validate the ID format.
|
|
174
|
-
* @param {string} propertyName - The name of the property to patch.
|
|
175
|
-
* @param {any} value - The value to patch.
|
|
176
|
-
* @param {boolean} [isReference=false] - Whether the value is a reference to another entity.
|
|
177
|
-
*/
|
|
178
102
|
patchValue(propertyName, value, isReference = false) {
|
|
179
103
|
checkUpdatesAllowed(this.schema);
|
|
180
104
|
|
|
181
|
-
const attribute = this.
|
|
105
|
+
const attribute = this.schema.getAttribute(propertyName);
|
|
182
106
|
if (!isObject(attribute)) {
|
|
183
107
|
throw new ValidationError(`Property ${propertyName} does not exist on entity ${this.entityName}.`);
|
|
184
108
|
}
|
|
@@ -189,6 +113,8 @@ class Patcher {
|
|
|
189
113
|
|
|
190
114
|
if (isReference) {
|
|
191
115
|
guardId(propertyName, value, this.entityName, nullable);
|
|
116
|
+
} else if (Array.isArray(attribute.type)) {
|
|
117
|
+
guardEnum(propertyName, value, attribute.type, this.entityName, nullable);
|
|
192
118
|
} else {
|
|
193
119
|
switch (attribute.type) {
|
|
194
120
|
case 'any':
|
|
@@ -220,14 +146,9 @@ class Patcher {
|
|
|
220
146
|
}
|
|
221
147
|
}
|
|
222
148
|
|
|
223
|
-
this.#set(attribute, value);
|
|
149
|
+
this.#set(propertyName, attribute, value);
|
|
224
150
|
}
|
|
225
151
|
|
|
226
|
-
/**
|
|
227
|
-
* Saves the current state of the entity to the database.
|
|
228
|
-
* @return {Promise<void>}
|
|
229
|
-
* @throws {Error} - Throws an error if the save operation fails.
|
|
230
|
-
*/
|
|
231
152
|
async save() {
|
|
232
153
|
checkUpdatesAllowed(this.schema);
|
|
233
154
|
|
|
@@ -235,11 +156,42 @@ class Patcher {
|
|
|
235
156
|
return;
|
|
236
157
|
}
|
|
237
158
|
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
159
|
+
const previousUpdatedAt = this.record.updatedAt;
|
|
160
|
+
let nextUpdatedAt = new Date().toISOString();
|
|
161
|
+
if (typeof previousUpdatedAt === 'string' && previousUpdatedAt === nextUpdatedAt) {
|
|
162
|
+
const previousDate = new Date(previousUpdatedAt);
|
|
163
|
+
if (!Number.isNaN(previousDate.getTime())) {
|
|
164
|
+
nextUpdatedAt = new Date(previousDate.getTime() + 1000).toISOString();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
this.record.updatedAt = nextUpdatedAt;
|
|
168
|
+
this.updates.updatedAt = {
|
|
169
|
+
previous: previousUpdatedAt,
|
|
170
|
+
current: nextUpdatedAt,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const keys = this.#getPrimaryKeyValues();
|
|
174
|
+
const updates = Object.keys(this.updates).reduce((acc, key) => {
|
|
175
|
+
acc[key] = this.updates[key].current;
|
|
176
|
+
return acc;
|
|
177
|
+
}, {});
|
|
178
|
+
|
|
179
|
+
if (this.collection
|
|
180
|
+
&& typeof this.collection.applyUpdateWatchers === 'function'
|
|
181
|
+
&& typeof this.collection.updateByKeys === 'function') {
|
|
182
|
+
const watched = this.collection.applyUpdateWatchers(this.record, updates);
|
|
183
|
+
this.record = watched.record;
|
|
184
|
+
await this.collection.updateByKeys(keys, watched.updates);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (this.patchRecord && typeof this.patchRecord.go === 'function') {
|
|
189
|
+
this.patchRecord = this.patchRecord.set({ updatedAt: nextUpdatedAt });
|
|
190
|
+
await this.patchRecord.go();
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
throw new ValidationError(`No persistence strategy available for ${this.entityName}`);
|
|
243
195
|
}
|
|
244
196
|
|
|
245
197
|
getUpdates() {
|