@adobe/spacecat-shared-data-access 3.22.0 → 3.24.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 +12 -0
- package/package.json +2 -2
- package/src/models/audit/audit.model.js +1 -0
- package/src/models/base/entity.registry.js +6 -0
- package/src/models/index.js +2 -0
- package/src/models/suggestion-grant/index.d.ts +30 -0
- package/src/models/suggestion-grant/index.js +19 -0
- package/src/models/suggestion-grant/suggestion-grant.collection.js +177 -0
- package/src/models/suggestion-grant/suggestion-grant.model.js +26 -0
- package/src/models/suggestion-grant/suggestion-grant.schema.js +74 -0
- package/src/models/token/index.js +19 -0
- package/src/models/token/token.collection.js +71 -0
- package/src/models/token/token.model.js +36 -0
- package/src/models/token/token.schema.js +57 -0
- package/src/util/postgrest.utils.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v3.24.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.23.0...@adobe/spacecat-shared-data-access-v3.24.0) (2026-03-19)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* SITES-40623 - token system in Spacecat ([#1414](https://github.com/adobe/spacecat-shared/issues/1414)) ([9c540ba](https://github.com/adobe/spacecat-shared/commit/9c540babf47984e15737bde509b54df9b5233c54))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v3.23.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.22.0...@adobe/spacecat-shared-data-access-v3.23.0) (2026-03-18)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* add cited-analsys as audit types ([#1444](https://github.com/adobe/spacecat-shared/issues/1444)) ([c87bf2e](https://github.com/adobe/spacecat-shared/commit/c87bf2e0d3ad77589fa44a1f9b9638ce0deaab53))
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-data-access-v3.22.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.21.0...@adobe/spacecat-shared-data-access-v3.22.0) (2026-03-17)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-data-access",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.24.0",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Data Access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@adobe/fetch": "^4.2.3",
|
|
44
|
-
"@adobe/spacecat-shared-utils": "1.
|
|
44
|
+
"@adobe/spacecat-shared-utils": "1.105.0",
|
|
45
45
|
"@supabase/postgrest-js": "2.99.1",
|
|
46
46
|
"@aws-sdk/client-s3": "^3.940.0",
|
|
47
47
|
"@types/joi": "17.2.3",
|
|
@@ -86,6 +86,7 @@ class Audit extends BaseModel {
|
|
|
86
86
|
WIKIPEDIA_ANALYSIS: 'wikipedia-analysis',
|
|
87
87
|
REDDIT_ANALYSIS: 'reddit-analysis',
|
|
88
88
|
YOUTUBE_ANALYSIS: 'youtube-analysis',
|
|
89
|
+
CITED_ANALYSIS: 'cited-analysis',
|
|
89
90
|
COMMERCE_PRODUCT_ENRICHMENTS: 'commerce-product-enrichments',
|
|
90
91
|
COMMERCE_PRODUCT_ENRICHMENTS_YEARLY: 'commerce-product-enrichments-yearly',
|
|
91
92
|
COMMERCE_PRODUCT_PAGE_ENRICHMENT: 'commerce-product-page-enrichment',
|
|
@@ -38,10 +38,12 @@ import SiteEnrollmentCollection from '../site-enrollment/site-enrollment.collect
|
|
|
38
38
|
import SiteTopFormCollection from '../site-top-form/site-top-form.collection.js';
|
|
39
39
|
import SiteTopPageCollection from '../site-top-page/site-top-page.collection.js';
|
|
40
40
|
import SuggestionCollection from '../suggestion/suggestion.collection.js';
|
|
41
|
+
import SuggestionGrantCollection from '../suggestion-grant/suggestion-grant.collection.js';
|
|
41
42
|
import PageIntentCollection from '../page-intent/page-intent.collection.js';
|
|
42
43
|
import ReportCollection from '../report/report.collection.js';
|
|
43
44
|
import TrialUserCollection from '../trial-user/trial-user.collection.js';
|
|
44
45
|
import TrialUserActivityCollection from '../trial-user-activity/trial-user-activity.collection.js';
|
|
46
|
+
import TokenCollection from '../token/token.collection.js';
|
|
45
47
|
import PageCitabilityCollection from '../page-citability/page-citability.collection.js';
|
|
46
48
|
import PlgOnboardingCollection from '../plg-onboarding/plg-onboarding.collection.js';
|
|
47
49
|
import SentimentGuidelineCollection from '../sentiment-guideline/sentiment-guideline.collection.js';
|
|
@@ -71,10 +73,12 @@ import SiteEnrollmentSchema from '../site-enrollment/site-enrollment.schema.js';
|
|
|
71
73
|
import SiteTopFormSchema from '../site-top-form/site-top-form.schema.js';
|
|
72
74
|
import SiteTopPageSchema from '../site-top-page/site-top-page.schema.js';
|
|
73
75
|
import SuggestionSchema from '../suggestion/suggestion.schema.js';
|
|
76
|
+
import SuggestionGrantSchema from '../suggestion-grant/suggestion-grant.schema.js';
|
|
74
77
|
import PageIntentSchema from '../page-intent/page-intent.schema.js';
|
|
75
78
|
import ReportSchema from '../report/report.schema.js';
|
|
76
79
|
import TrialUserSchema from '../trial-user/trial-user.schema.js';
|
|
77
80
|
import TrialUserActivitySchema from '../trial-user-activity/trial-user-activity.schema.js';
|
|
81
|
+
import TokenSchema from '../token/token.schema.js';
|
|
78
82
|
import PageCitabilitySchema from '../page-citability/page-citability.schema.js';
|
|
79
83
|
import PlgOnboardingSchema from '../plg-onboarding/plg-onboarding.schema.js';
|
|
80
84
|
import SentimentGuidelineSchema from '../sentiment-guideline/sentiment-guideline.schema.js';
|
|
@@ -199,10 +203,12 @@ EntityRegistry.registerEntity(SiteEnrollmentSchema, SiteEnrollmentCollection);
|
|
|
199
203
|
EntityRegistry.registerEntity(SiteTopFormSchema, SiteTopFormCollection);
|
|
200
204
|
EntityRegistry.registerEntity(SiteTopPageSchema, SiteTopPageCollection);
|
|
201
205
|
EntityRegistry.registerEntity(SuggestionSchema, SuggestionCollection);
|
|
206
|
+
EntityRegistry.registerEntity(SuggestionGrantSchema, SuggestionGrantCollection);
|
|
202
207
|
EntityRegistry.registerEntity(PageIntentSchema, PageIntentCollection);
|
|
203
208
|
EntityRegistry.registerEntity(ReportSchema, ReportCollection);
|
|
204
209
|
EntityRegistry.registerEntity(TrialUserSchema, TrialUserCollection);
|
|
205
210
|
EntityRegistry.registerEntity(TrialUserActivitySchema, TrialUserActivityCollection);
|
|
211
|
+
EntityRegistry.registerEntity(TokenSchema, TokenCollection);
|
|
206
212
|
EntityRegistry.registerEntity(PageCitabilitySchema, PageCitabilityCollection);
|
|
207
213
|
EntityRegistry.registerEntity(PlgOnboardingSchema, PlgOnboardingCollection);
|
|
208
214
|
EntityRegistry.registerEntity(SentimentGuidelineSchema, SentimentGuidelineCollection);
|
package/src/models/index.js
CHANGED
|
@@ -36,10 +36,12 @@ export * from './site-top-form/index.js';
|
|
|
36
36
|
export * from './site-top-page/index.js';
|
|
37
37
|
export * from './site/index.js';
|
|
38
38
|
export * from './suggestion/index.js';
|
|
39
|
+
export * from './suggestion-grant/index.js';
|
|
39
40
|
export * from './page-intent/index.js';
|
|
40
41
|
export * from './report/index.js';
|
|
41
42
|
export * from './trial-user/index.js';
|
|
42
43
|
export * from './trial-user-activity/index.js';
|
|
44
|
+
export * from './token/index.js';
|
|
43
45
|
export * from './page-citability/index.js';
|
|
44
46
|
export * from './plg-onboarding/index.js';
|
|
45
47
|
export * from './sentiment-guideline/index.js';
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { BaseCollection, BaseModel } from '../index';
|
|
14
|
+
|
|
15
|
+
export interface SuggestionGrant extends BaseModel {
|
|
16
|
+
getGrantId(): string;
|
|
17
|
+
getSuggestionId(): string;
|
|
18
|
+
getSiteId(): string;
|
|
19
|
+
getTokenId(): string;
|
|
20
|
+
getTokenType(): string;
|
|
21
|
+
getGrantedAt(): string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SuggestionGrantCollection extends BaseCollection<SuggestionGrant> {
|
|
25
|
+
findBySuggestionIds(suggestionIds: string[]): Promise<{ data: Array<{ suggestion_id: string; grant_id: string }>; error: object | null }>;
|
|
26
|
+
invokeGrantSuggestionsRpc(suggestionIds: string[], siteId: string, tokenType: string, cycle: string): Promise<{ data: Array | null; error: object | null }>;
|
|
27
|
+
splitSuggestionsByGrantStatus(suggestionIds: string[]): Promise<{ grantedIds: string[]; notGrantedIds: string[]; grantIds: string[] }>;
|
|
28
|
+
isSuggestionGranted(suggestionId: string): Promise<boolean>;
|
|
29
|
+
grantSuggestions(suggestionIds: string[], siteId: string, tokenType: string): Promise<{ success: boolean; reason?: string; grantedSuggestions?: Array }>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import SuggestionGrant from './suggestion-grant.model.js';
|
|
14
|
+
import SuggestionGrantCollection from './suggestion-grant.collection.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
SuggestionGrant,
|
|
18
|
+
SuggestionGrantCollection,
|
|
19
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hasText } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import BaseCollection from '../base/base.collection.js';
|
|
16
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* SuggestionGrantCollection - Manages SuggestionGrant records (suggestion_grants table).
|
|
20
|
+
* Table is insert-only; inserts happen via grant_suggestions RPC. This collection
|
|
21
|
+
* provides read-only lookup by suggestion IDs.
|
|
22
|
+
*
|
|
23
|
+
* @class SuggestionGrantCollection
|
|
24
|
+
* @extends BaseCollection
|
|
25
|
+
*/
|
|
26
|
+
class SuggestionGrantCollection extends BaseCollection {
|
|
27
|
+
static COLLECTION_NAME = 'SuggestionGrantCollection';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Finds all grant rows for the given suggestion IDs (suggestion_id, grant_id only).
|
|
31
|
+
*
|
|
32
|
+
* @async
|
|
33
|
+
* @param {string[]} suggestionIds - Suggestion IDs to look up.
|
|
34
|
+
* @returns {Promise<Array<{suggestion_id: string, grant_id: string}>>}
|
|
35
|
+
* @throws {DataAccessError} - On query failure.
|
|
36
|
+
*/
|
|
37
|
+
async findBySuggestionIds(suggestionIds) {
|
|
38
|
+
if (!Array.isArray(suggestionIds) || suggestionIds.length === 0) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
const { data, error } = await this.postgrestService
|
|
42
|
+
.from(this.tableName)
|
|
43
|
+
.select('suggestion_id,grant_id')
|
|
44
|
+
.in('suggestion_id', suggestionIds);
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
throw new DataAccessError('Failed to find grants by suggestion IDs', this, error);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return data ?? [];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Invokes the grant_suggestions RPC. Inserts suggestion_grants rows and consumes one token.
|
|
55
|
+
* RPC name and parameter shape live in this collection (suggestion_grants).
|
|
56
|
+
*
|
|
57
|
+
* @async
|
|
58
|
+
* @param {string[]} suggestionIds - Suggestion IDs to grant.
|
|
59
|
+
* @param {string} siteId - Site ID.
|
|
60
|
+
* @param {string} tokenType - Token type.
|
|
61
|
+
* @param {string} cycle - Token cycle (e.g. '2025-01').
|
|
62
|
+
* @returns {Promise<{ data: Array|null, error: object|null }>}
|
|
63
|
+
*/
|
|
64
|
+
async invokeGrantSuggestionsRpc(suggestionIds, siteId, tokenType, cycle) {
|
|
65
|
+
return this.postgrestService.rpc('grant_suggestions', {
|
|
66
|
+
p_suggestion_ids: suggestionIds,
|
|
67
|
+
p_site_id: siteId,
|
|
68
|
+
p_token_type: tokenType,
|
|
69
|
+
p_cycle: cycle,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Splits suggestion IDs into those that are granted and those that are not.
|
|
75
|
+
* A suggestion is considered granted if it has at least one row in suggestion_grants.
|
|
76
|
+
*
|
|
77
|
+
* @async
|
|
78
|
+
* @param {string[]} suggestionIds - Suggestion IDs to check.
|
|
79
|
+
* @returns {Promise<{ grantedIds: string[], notGrantedIds: string[], grantIds: string[] }>}
|
|
80
|
+
* @throws {DataAccessError} - On invalid input or query failure.
|
|
81
|
+
*/
|
|
82
|
+
async splitSuggestionsByGrantStatus(suggestionIds) {
|
|
83
|
+
if (!Array.isArray(suggestionIds)) {
|
|
84
|
+
throw new DataAccessError('splitSuggestionsByGrantStatus: suggestionIds must be an array', this);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const deduped = [...new Set(suggestionIds.filter((id) => hasText(id)))];
|
|
88
|
+
|
|
89
|
+
if (deduped.length === 0) {
|
|
90
|
+
return { grantedIds: [], notGrantedIds: [], grantIds: [] };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const rows = await this.findBySuggestionIds(deduped);
|
|
95
|
+
const grantedIdSet = new Set(rows.map((r) => r.suggestion_id));
|
|
96
|
+
const grantedIds = deduped.filter((id) => grantedIdSet.has(id));
|
|
97
|
+
const notGrantedIds = deduped.filter((id) => !grantedIdSet.has(id));
|
|
98
|
+
const grantIds = [...new Set(rows.map((r) => r.grant_id).filter(Boolean))];
|
|
99
|
+
|
|
100
|
+
return { grantedIds, notGrantedIds, grantIds };
|
|
101
|
+
} catch (err) {
|
|
102
|
+
if (err instanceof DataAccessError) throw err;
|
|
103
|
+
this.log.error('splitSuggestionsByGrantStatus failed', err);
|
|
104
|
+
throw new DataAccessError('Failed to split suggestions by grant status', this, err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Returns whether a single suggestion is granted (has at least one row in suggestion_grants).
|
|
110
|
+
*
|
|
111
|
+
* @async
|
|
112
|
+
* @param {string} suggestionId - Suggestion ID to check.
|
|
113
|
+
* @returns {Promise<boolean>} True if the suggestion is granted,
|
|
114
|
+
* false otherwise or if id is empty.
|
|
115
|
+
*/
|
|
116
|
+
async isSuggestionGranted(suggestionId) {
|
|
117
|
+
if (!hasText(suggestionId)) return false;
|
|
118
|
+
const { grantedIds } = await this.splitSuggestionsByGrantStatus([suggestionId]);
|
|
119
|
+
return grantedIds.length > 0;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Grants one or more suggestions by consuming a single token for the given token type.
|
|
124
|
+
* Resolves the current cycle token via TokenCollection#findBySiteIdAndTokenType
|
|
125
|
+
* (which auto-creates if missing), checks that at least one token remains, then calls the
|
|
126
|
+
* grant_suggestions RPC to atomically consume one token and insert suggestion grants for
|
|
127
|
+
* the entire list of IDs.
|
|
128
|
+
*
|
|
129
|
+
* @async
|
|
130
|
+
* @param {string[]} suggestionIds - Suggestion IDs to grant (one token consumed for the list).
|
|
131
|
+
* @param {string} siteId - The site ID that owns the token allocation.
|
|
132
|
+
* @param {string} tokenType - Token type (e.g. 'grant_cwv').
|
|
133
|
+
* @returns {Promise<{ success: boolean, reason?: string, grantedSuggestions?: Array }>}
|
|
134
|
+
* @throws {DataAccessError} - On missing inputs or RPC failure.
|
|
135
|
+
*/
|
|
136
|
+
async grantSuggestions(suggestionIds, siteId, tokenType) {
|
|
137
|
+
if (!Array.isArray(suggestionIds) || suggestionIds.some((id) => !hasText(id))) {
|
|
138
|
+
throw new DataAccessError('grantSuggestions: suggestionIds must be an array of non-empty strings', this);
|
|
139
|
+
}
|
|
140
|
+
if (!hasText(siteId)) {
|
|
141
|
+
throw new DataAccessError('grantSuggestions: siteId is required', this);
|
|
142
|
+
}
|
|
143
|
+
if (!hasText(tokenType)) {
|
|
144
|
+
throw new DataAccessError('grantSuggestions: tokenType is required', this);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const tokenCollection = this.entityRegistry.getCollection('TokenCollection');
|
|
148
|
+
const token = await tokenCollection.findBySiteIdAndTokenType(siteId, tokenType);
|
|
149
|
+
|
|
150
|
+
if (!token || token.getRemaining() < 1) {
|
|
151
|
+
return { success: false, reason: 'no_tokens' };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const cycle = token.getCycle();
|
|
155
|
+
const rpcResult = await this.invokeGrantSuggestionsRpc(
|
|
156
|
+
suggestionIds,
|
|
157
|
+
siteId,
|
|
158
|
+
tokenType,
|
|
159
|
+
cycle,
|
|
160
|
+
);
|
|
161
|
+
const { data, error } = rpcResult;
|
|
162
|
+
|
|
163
|
+
if (error) {
|
|
164
|
+
this.log.error('grantSuggestions: RPC failed', error);
|
|
165
|
+
throw new DataAccessError('Failed to grant suggestions (grant_suggestions)', this, error);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const row = Array.isArray(data) && data.length > 0 ? data[0] : null;
|
|
169
|
+
if (!row || !row.success) {
|
|
170
|
+
return { success: false, reason: row?.reason || 'rpc_no_result' };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return { success: true, grantedSuggestions: row.granted_suggestions };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export default SuggestionGrantCollection;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* SuggestionGrant - Record that a suggestion was granted (has a row in suggestion_grants).
|
|
17
|
+
* Table is insert-only; rows are created via grant_suggestions RPC.
|
|
18
|
+
*
|
|
19
|
+
* @class SuggestionGrant
|
|
20
|
+
* @extends BaseModel
|
|
21
|
+
*/
|
|
22
|
+
class SuggestionGrant extends BaseModel {
|
|
23
|
+
static ENTITY_NAME = 'SuggestionGrant';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default SuggestionGrant;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
16
|
+
import SuggestionGrant from './suggestion-grant.model.js';
|
|
17
|
+
import SuggestionGrantCollection from './suggestion-grant.collection.js';
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* SuggestionGrant: suggestion_grants table (insert-only via grant_suggestions RPC).
|
|
21
|
+
* Columns: id, grant_id, suggestion_id, site_id, token_id, token_type, created_at, granted_at.
|
|
22
|
+
* Table has no updated_at/updated_by; those defaults are ignored for PostgREST.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
const schema = new SchemaBuilder(SuggestionGrant, SuggestionGrantCollection)
|
|
26
|
+
.addAttribute('updatedAt', {
|
|
27
|
+
type: 'string', required: true, readOnly: true, postgrestIgnore: true,
|
|
28
|
+
})
|
|
29
|
+
.addAttribute('updatedBy', {
|
|
30
|
+
type: 'string', required: false, postgrestIgnore: true,
|
|
31
|
+
})
|
|
32
|
+
.addIndex({ composite: ['suggestionId'] }, { composite: [] })
|
|
33
|
+
.addAttribute('suggestionId', {
|
|
34
|
+
type: 'string',
|
|
35
|
+
required: true,
|
|
36
|
+
readOnly: true,
|
|
37
|
+
validate: (value) => isValidUUID(value),
|
|
38
|
+
postgrestField: 'suggestion_id',
|
|
39
|
+
})
|
|
40
|
+
.addAttribute('grantId', {
|
|
41
|
+
type: 'string',
|
|
42
|
+
required: true,
|
|
43
|
+
readOnly: true,
|
|
44
|
+
validate: (value) => isValidUUID(value),
|
|
45
|
+
postgrestField: 'grant_id',
|
|
46
|
+
})
|
|
47
|
+
.addAttribute('siteId', {
|
|
48
|
+
type: 'string',
|
|
49
|
+
required: true,
|
|
50
|
+
readOnly: true,
|
|
51
|
+
validate: (value) => isValidUUID(value),
|
|
52
|
+
postgrestField: 'site_id',
|
|
53
|
+
})
|
|
54
|
+
.addAttribute('tokenId', {
|
|
55
|
+
type: 'string',
|
|
56
|
+
required: true,
|
|
57
|
+
readOnly: true,
|
|
58
|
+
validate: (value) => isValidUUID(value),
|
|
59
|
+
postgrestField: 'token_id',
|
|
60
|
+
})
|
|
61
|
+
.addAttribute('tokenType', {
|
|
62
|
+
type: 'string',
|
|
63
|
+
required: true,
|
|
64
|
+
readOnly: true,
|
|
65
|
+
postgrestField: 'token_type',
|
|
66
|
+
})
|
|
67
|
+
.addAttribute('grantedAt', {
|
|
68
|
+
type: 'string',
|
|
69
|
+
required: true,
|
|
70
|
+
readOnly: true,
|
|
71
|
+
postgrestField: 'granted_at',
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
export default schema.build();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import Token from './token.model.js';
|
|
14
|
+
import TokenCollection from './token.collection.js';
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
Token,
|
|
18
|
+
TokenCollection,
|
|
19
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { hasText, getTokenGrantConfig } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import BaseCollection from '../base/base.collection.js';
|
|
16
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* TokenCollection - Manages Token entities (per-site, per-tokenType, per-cycle).
|
|
20
|
+
* Uses PostgREST table `tokens`.
|
|
21
|
+
*
|
|
22
|
+
* @class TokenCollection
|
|
23
|
+
* @extends BaseCollection
|
|
24
|
+
*/
|
|
25
|
+
class TokenCollection extends BaseCollection {
|
|
26
|
+
static COLLECTION_NAME = 'TokenCollection';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Finds a Token for the current cycle by siteId and tokenType. The cycle is
|
|
30
|
+
* derived from the token-grant-config's cycleFormat. If no token exists for
|
|
31
|
+
* the current cycle and options.createIfNotFound is true, creates one. The
|
|
32
|
+
* token total is the minimum of options.total (if supplied) and the config
|
|
33
|
+
* tokensPerCycle, clamped to at least 1.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} siteId - Site ID (UUID).
|
|
36
|
+
* @param {string} tokenType - Token type (e.g. grant_cwv,
|
|
37
|
+
* grant_broken_backlinks).
|
|
38
|
+
* @param {Object} [options={}] - Options.
|
|
39
|
+
* @param {boolean} [options.createIfNotFound=false] - If true, create a token when none exists.
|
|
40
|
+
* @param {number} [options.total] - Optional supplied total;
|
|
41
|
+
* actual total is min(options.total, config.tokensPerCycle), at least 1.
|
|
42
|
+
* @returns {Promise<import('./token.model.js').default|null>} Token instance
|
|
43
|
+
* (existing or newly created), or null when none exists and createIfNotFound is false.
|
|
44
|
+
*/
|
|
45
|
+
async findBySiteIdAndTokenType(siteId, tokenType, options = {}) {
|
|
46
|
+
if (!hasText(siteId) || !hasText(tokenType)) {
|
|
47
|
+
throw new DataAccessError('TokenCollection.findBySiteIdAndTokenType: siteId and tokenType are required');
|
|
48
|
+
}
|
|
49
|
+
const config = getTokenGrantConfig(tokenType);
|
|
50
|
+
if (!config) {
|
|
51
|
+
throw new DataAccessError(`TokenCollection.findBySiteIdAndTokenType: no token grant config for tokenType: ${tokenType}`);
|
|
52
|
+
}
|
|
53
|
+
const { currentCycle: cycle } = config;
|
|
54
|
+
const existing = await this.findByIndexKeys({ siteId, tokenType, cycle }, { limit: 1 });
|
|
55
|
+
if (existing || options.createIfNotFound !== true) {
|
|
56
|
+
return existing || null;
|
|
57
|
+
}
|
|
58
|
+
const total = options.total != null
|
|
59
|
+
? Math.max(1, Math.min(Number(options.total), config.tokensPerCycle))
|
|
60
|
+
: config.tokensPerCycle;
|
|
61
|
+
return this.create({
|
|
62
|
+
siteId,
|
|
63
|
+
tokenType,
|
|
64
|
+
cycle,
|
|
65
|
+
total,
|
|
66
|
+
used: 0,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default TokenCollection;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import BaseModel from '../base/base.model.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Token - Tracks token allocation and consumption per site, token type (opportunity type),
|
|
17
|
+
* and cycle. Used to enforce per-opportunity monthly (or cycle) limits for granted suggestions.
|
|
18
|
+
*
|
|
19
|
+
* @class Token
|
|
20
|
+
* @extends BaseModel
|
|
21
|
+
*/
|
|
22
|
+
class Token extends BaseModel {
|
|
23
|
+
static ENTITY_NAME = 'Token';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Returns the number of tokens remaining in this cycle (total - used).
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
getRemaining() {
|
|
30
|
+
const total = this.getTotal();
|
|
31
|
+
const used = this.getUsed();
|
|
32
|
+
return Math.max(0, total - used);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default Token;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
15
|
+
import SchemaBuilder from '../base/schema.builder.js';
|
|
16
|
+
import Token from './token.model.js';
|
|
17
|
+
import TokenCollection from './token.collection.js';
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* Token entity: per-site, per-tokenType (opportunity type), per-cycle token allocation.
|
|
21
|
+
* Postgres table: tokens with primary key id, unique (site_id, token_type, cycle).
|
|
22
|
+
* Data access: findBySiteIdAndTokenType(siteId, tokenType).
|
|
23
|
+
* Consume: one token per grant_suggestions call (whole list of IDs)
|
|
24
|
+
* via SuggestionGrantCollection.grantSuggestions().
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const schema = new SchemaBuilder(Token, TokenCollection)
|
|
28
|
+
.addIndex({ composite: ['siteId', 'tokenType'] }, { composite: ['cycle'] })
|
|
29
|
+
.addAttribute('siteId', {
|
|
30
|
+
type: 'string',
|
|
31
|
+
required: true,
|
|
32
|
+
readOnly: true,
|
|
33
|
+
validate: (value) => isValidUUID(value),
|
|
34
|
+
postgrestField: 'site_id',
|
|
35
|
+
})
|
|
36
|
+
.addAttribute('tokenType', {
|
|
37
|
+
type: 'string',
|
|
38
|
+
required: true,
|
|
39
|
+
postgrestField: 'token_type',
|
|
40
|
+
})
|
|
41
|
+
.addAttribute('cycle', {
|
|
42
|
+
type: 'string',
|
|
43
|
+
required: true,
|
|
44
|
+
readOnly: true,
|
|
45
|
+
})
|
|
46
|
+
.addAttribute('total', {
|
|
47
|
+
type: 'number',
|
|
48
|
+
required: true,
|
|
49
|
+
readOnly: true,
|
|
50
|
+
})
|
|
51
|
+
.addAttribute('used', {
|
|
52
|
+
type: 'number',
|
|
53
|
+
required: true,
|
|
54
|
+
default: 0,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export default schema.build();
|