@adobe/spacecat-shared-data-access 4.0.0 → 4.1.1
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 +1 -1
- package/src/models/brand/brand.model.js +17 -0
- package/src/models/brand/brand.schema.js +28 -5
- package/src/models/brand/index.d.ts +11 -0
- package/src/models/brand-semrush-project/brand-semrush-project.collection.js +140 -0
- package/src/models/brand-semrush-project/brand-semrush-project.schema.js +23 -2
- package/src/models/brand-semrush-project/index.d.ts +22 -0
- package/src/models/geo-experiment/geo-experiment.model.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v4.1.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.1.0...@adobe/spacecat-shared-data-access-v4.1.1) (2026-07-06)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* add new phases to IVE states ([#1768](https://github.com/adobe/spacecat-shared/issues/1768)) ([91c5182](https://github.com/adobe/spacecat-shared/commit/91c5182cfbba5d9d9fcb7a53e45598bbccf93e19))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v4.1.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v4.0.0...@adobe/spacecat-shared-data-access-v4.1.0) (2026-07-06)
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **data-access:** mapping-table site_id/deletedAt + Brand workspace-id rename ([#1763](https://github.com/adobe/spacecat-shared/issues/1763)) ([c91faf0](https://github.com/adobe/spacecat-shared/commit/c91faf009b2643c5451a9912a627426ce97be0d1)), closes [#765](https://github.com/adobe/spacecat-shared/issues/765) [adobe/mysticat-data-service#765](https://github.com/adobe/mysticat-data-service/issues/765) [adobe/spacecat-api-service#2739](https://github.com/adobe/spacecat-api-service/issues/2739) [#765](https://github.com/adobe/spacecat-shared/issues/765) [mysticat-data-service#765](https://github.com/adobe/mysticat-data-service/issues/765)
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-data-access-v4.0.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.81.0...@adobe/spacecat-shared-data-access-v4.0.0) (2026-07-03)
|
|
2
14
|
|
|
3
15
|
### ⚠ BREAKING CHANGES
|
package/package.json
CHANGED
|
@@ -39,6 +39,23 @@ class Brand extends BaseModel {
|
|
|
39
39
|
* `pending`; customer offboard writes `deleted`.
|
|
40
40
|
*/
|
|
41
41
|
static STATUSES = Object.freeze(['pending', 'active', 'deleted', 'ignored']);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Deprecated BC-compat setter. `semrushWorkspaceId` is `readOnly: true` in
|
|
45
|
+
* the schema (mirrored by the mysticat-data-service sync trigger), so no
|
|
46
|
+
* setter is auto-generated for it — this manual method exists purely so an
|
|
47
|
+
* existing external caller of `setSemrushWorkspaceId` does not get a
|
|
48
|
+
* semver-breaking runtime error on upgrade. Delegates to the real
|
|
49
|
+
* write-of-record attribute. Remove once every direct caller has migrated
|
|
50
|
+
* to `setSemrushSubWorkspaceId` (see brand.schema.js).
|
|
51
|
+
*
|
|
52
|
+
* @deprecated Use setSemrushSubWorkspaceId instead.
|
|
53
|
+
* @param {string|null} value
|
|
54
|
+
* @returns {Brand}
|
|
55
|
+
*/
|
|
56
|
+
setSemrushWorkspaceId(value) {
|
|
57
|
+
return this.setSemrushSubWorkspaceId(value);
|
|
58
|
+
}
|
|
42
59
|
}
|
|
43
60
|
|
|
44
61
|
export default Brand;
|
|
@@ -35,14 +35,31 @@ const schema = new SchemaBuilder(Brand, BrandCollection)
|
|
|
35
35
|
type: Brand.STATUSES,
|
|
36
36
|
validate: (value) => value == null || Brand.STATUSES.includes(value),
|
|
37
37
|
})
|
|
38
|
+
// DEPRECATED (serenity-docs brand-semrush-mapping-maintenance.md §10
|
|
39
|
+
// rename, write-of-record cutover): read-only mirror of
|
|
40
|
+
// semrushSubWorkspaceId below, maintained entirely by the
|
|
41
|
+
// mysticat-data-service brands_sync_semrush_workspace_id trigger
|
|
42
|
+
// (migration 20260702094229). No schema-generated setter — app code must
|
|
43
|
+
// write semrushSubWorkspaceId instead. brand.model.js still defines a
|
|
44
|
+
// manual, deprecated setSemrushWorkspaceId() that delegates to
|
|
45
|
+
// setSemrushSubWorkspaceId(), so an existing external caller of the old
|
|
46
|
+
// setter is not broken (a bare readOnly flip here would be a semver-breaking
|
|
47
|
+
// removal for any @adobe/spacecat-shared-data-access consumer). Will be
|
|
48
|
+
// retired (attribute, column, and trigger) once every direct external
|
|
49
|
+
// reader has migrated.
|
|
50
|
+
.addAttribute('semrushWorkspaceId', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
readOnly: true,
|
|
53
|
+
})
|
|
38
54
|
// Brand → Semrush sub-workspace. Nullable (NULL = no sub-workspace
|
|
39
55
|
// connected). Same minimum guard as organizations.semrushWorkspaceId: the
|
|
40
56
|
// shared `hasText` rejects the empty string (and non-strings) while letting
|
|
41
57
|
// null/undefined short-circuit. Note hasText does NOT trim, so a
|
|
42
58
|
// whitespace-only value would pass — acceptable here because this column is
|
|
43
59
|
// only ever written by the activate flow with a real Semrush workspace UUID,
|
|
44
|
-
// never user input.
|
|
45
|
-
.
|
|
60
|
+
// never user input. This is now the write-of-record (see semrushWorkspaceId
|
|
61
|
+
// above for the deprecated BC mirror).
|
|
62
|
+
.addAttribute('semrushSubWorkspaceId', {
|
|
46
63
|
type: 'string',
|
|
47
64
|
validate: (value) => value == null || hasText(value),
|
|
48
65
|
})
|
|
@@ -55,9 +72,15 @@ const schema = new SchemaBuilder(Brand, BrandCollection)
|
|
|
55
72
|
type: 'any',
|
|
56
73
|
validate: (value) => value == null || (typeof value === 'object' && !Array.isArray(value)),
|
|
57
74
|
})
|
|
58
|
-
// Uniqueness is enforced at the DB level via the UNIQUE constraint on
|
|
59
|
-
// brands.semrush_workspace_id (mysticat-data-service migration
|
|
75
|
+
// Uniqueness is enforced at the DB level via the UNIQUE constraint on the
|
|
76
|
+
// deprecated brands.semrush_workspace_id (mysticat-data-service migration
|
|
60
77
|
// 20260615102123), so findBySemrushWorkspaceId returns at most one row.
|
|
61
|
-
|
|
78
|
+
// Kept for BC lookups against the mirrored column; new code should prefer
|
|
79
|
+
// findBySemrushSubWorkspaceId below.
|
|
80
|
+
.addAllIndex(['semrushWorkspaceId'])
|
|
81
|
+
// Same uniqueness guarantee on the write-of-record column
|
|
82
|
+
// (brands.semrush_sub_workspace_id, mysticat-data-service migration
|
|
83
|
+
// 20260702091920).
|
|
84
|
+
.addAllIndex(['semrushSubWorkspaceId']);
|
|
62
85
|
|
|
63
86
|
export default schema.build();
|
|
@@ -17,13 +17,24 @@ import type {
|
|
|
17
17
|
export interface Brand extends BaseModel {
|
|
18
18
|
getName(): string;
|
|
19
19
|
getStatus(): string;
|
|
20
|
+
// Deprecated BC mirror (brands.semrush_workspace_id), maintained by the
|
|
21
|
+
// mysticat-data-service sync trigger. No schema-generated setter; the
|
|
22
|
+
// deprecated setSemrushWorkspaceId below is a manual delegate defined in
|
|
23
|
+
// brand.model.js, kept only for backward compatibility. Use
|
|
24
|
+
// getSemrushSubWorkspaceId/setSemrushSubWorkspaceId instead. See
|
|
25
|
+
// brand.schema.js.
|
|
20
26
|
getSemrushWorkspaceId(): string | null;
|
|
27
|
+
getSemrushSubWorkspaceId(): string | null;
|
|
21
28
|
setName(value: string): Brand;
|
|
22
29
|
setStatus(value: string): Brand;
|
|
30
|
+
setSemrushSubWorkspaceId(value: string | null): Brand;
|
|
31
|
+
/** @deprecated Use setSemrushSubWorkspaceId instead. */
|
|
23
32
|
setSemrushWorkspaceId(value: string | null): Brand;
|
|
24
33
|
}
|
|
25
34
|
|
|
26
35
|
export interface BrandCollection extends BaseCollection<Brand> {
|
|
27
36
|
allBySemrushWorkspaceId(semrushWorkspaceId: string): Promise<Brand[]>;
|
|
28
37
|
findBySemrushWorkspaceId(semrushWorkspaceId: string): Promise<Brand | null>;
|
|
38
|
+
allBySemrushSubWorkspaceId(semrushSubWorkspaceId: string): Promise<Brand[]>;
|
|
39
|
+
findBySemrushSubWorkspaceId(semrushSubWorkspaceId: string): Promise<Brand | null>;
|
|
29
40
|
}
|
|
@@ -10,11 +10,31 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { isValidUUID } from '@adobe/spacecat-shared-utils';
|
|
14
|
+
|
|
13
15
|
import BaseCollection from '../base/base.collection.js';
|
|
16
|
+
import DataAccessError from '../../errors/data-access.error.js';
|
|
17
|
+
import { DEFAULT_PAGE_SIZE } from '../../util/postgrest.utils.js';
|
|
18
|
+
|
|
19
|
+
const BRAND_FK = 'brand_to_semrush_projects_brand_id_fkey';
|
|
20
|
+
|
|
21
|
+
// Safety cutoff for #fetchOrgRows' pagination loop. The per-FK base-collection
|
|
22
|
+
// queries this package generates elsewhere are naturally bounded (one brand's
|
|
23
|
+
// rows); this org-level fan-out is not, so an unexpectedly large org (or a
|
|
24
|
+
// runaway loop from an accessor bug) is capped rather than growing memory
|
|
25
|
+
// unbounded. Not expected to be hit in practice — see the truncation log.
|
|
26
|
+
const MAX_ORG_ROWS = 50_000;
|
|
14
27
|
|
|
15
28
|
/**
|
|
16
29
|
* BrandSemrushProjectCollection - collection of BrandSemrushProject rows.
|
|
17
30
|
*
|
|
31
|
+
* Tombstone contract: only `allByOrganizationId` filters `deletedAt` by
|
|
32
|
+
* default. Every other accessor here (including the auto-generated
|
|
33
|
+
* `allByBrandId` / `findBySemrushProjectId`) returns tombstoned rows
|
|
34
|
+
* alongside live ones — there is no package-level soft-delete scope. Callers
|
|
35
|
+
* that need only live rows must filter explicitly, or use the sanctioned
|
|
36
|
+
* accessor. See serenity-docs brand-semrush-mapping-maintenance.md §7.2.
|
|
37
|
+
*
|
|
18
38
|
* @class BrandSemrushProjectCollection
|
|
19
39
|
* @extends BaseCollection
|
|
20
40
|
*/
|
|
@@ -26,6 +46,10 @@ class BrandSemrushProjectCollection extends BaseCollection {
|
|
|
26
46
|
* null. Used by spacecat-api-service POST /v2/orgs/.../serenity/markets to
|
|
27
47
|
* 409 on a duplicate slice before calling the upstream.
|
|
28
48
|
*
|
|
49
|
+
* Returns tombstoned rows too (see class doc) — flat mode never tombstones,
|
|
50
|
+
* so this only matters for sub-workspace callers, which do not use this
|
|
51
|
+
* method today.
|
|
52
|
+
*
|
|
29
53
|
* @param {string} brandId
|
|
30
54
|
* @param {number} geoTargetId Google Ads Geo Target ID.
|
|
31
55
|
* @param {string} languageCode BCP-47 primary subtag.
|
|
@@ -34,6 +58,122 @@ class BrandSemrushProjectCollection extends BaseCollection {
|
|
|
34
58
|
async findBySlice(brandId, geoTargetId, languageCode) {
|
|
35
59
|
return this.findByIndexKeys({ brandId, geoTargetId, languageCode });
|
|
36
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Returns identity rows (brand/project/slice/site, plus the embedded
|
|
64
|
+
* sub-workspace id) for every mapping row under the given organization, via
|
|
65
|
+
* a single PostgREST embedded-join query (INNER JOIN on brand_id FK) — one
|
|
66
|
+
* round trip, no per-brand fan-out. This is the sanctioned read path for
|
|
67
|
+
* cross-team consumers (spec §7.2): it deliberately returns plain frozen
|
|
68
|
+
* identity DTOs, not model instances, so a consumer cannot update/remove
|
|
69
|
+
* through it or reach beyond the projected columns.
|
|
70
|
+
*
|
|
71
|
+
* Tombstones are filtered by default (`deletedAt IS NULL`) — pass
|
|
72
|
+
* `{ includeDeleted: true }` to see them (e.g. for history/debugging).
|
|
73
|
+
*
|
|
74
|
+
* @param {string} organizationId - UUID of the organization.
|
|
75
|
+
* @param {object} [options]
|
|
76
|
+
* @param {boolean} [options.includeDeleted=false]
|
|
77
|
+
* @returns {Promise<Array<{
|
|
78
|
+
* brandId: string,
|
|
79
|
+
* semrushProjectId: string,
|
|
80
|
+
* geoTargetId: number,
|
|
81
|
+
* languageCode: string,
|
|
82
|
+
* siteId: string|null,
|
|
83
|
+
* organizationId: string|null,
|
|
84
|
+
* semrushSubWorkspaceId: string|null,
|
|
85
|
+
* }>>}
|
|
86
|
+
*/
|
|
87
|
+
async allByOrganizationId(organizationId, { includeDeleted = false } = {}) {
|
|
88
|
+
if (!organizationId || !isValidUUID(organizationId)) {
|
|
89
|
+
throw new DataAccessError(
|
|
90
|
+
'organizationId is required and must be a valid UUID',
|
|
91
|
+
{ entityName: this.entityName, tableName: this.tableName },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// `!inner` is explicit rather than relying on the implicit INNER-JOIN-on-
|
|
96
|
+
// embedded-filter behavior added in PostgREST v11 — on an older server the
|
|
97
|
+
// same `.eq('brands.organization_id', ...)` filter alone is a LEFT JOIN,
|
|
98
|
+
// returning every mapping row with `brands: null` for non-matching
|
|
99
|
+
// organizations instead of excluding them.
|
|
100
|
+
// eslint-disable-next-line max-len
|
|
101
|
+
const select = `brand_id, semrush_project_id, semrush_location_id, language, site_id, brands!${BRAND_FK}!inner(organization_id, semrush_sub_workspace_id)`;
|
|
102
|
+
let query = this.postgrestService.from(this.tableName).select(select)
|
|
103
|
+
.eq('brands.organization_id', organizationId);
|
|
104
|
+
if (!includeDeleted) {
|
|
105
|
+
query = query.is('deleted_at', null);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return this.#fetchOrgRows(query);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Offset-paginates `query` and returns plain frozen identity DTOs.
|
|
113
|
+
*
|
|
114
|
+
* This deliberately rolls its own offset loop instead of reusing the
|
|
115
|
+
* cursor-based helper in `postgrest.utils.js`: that helper hydrates model
|
|
116
|
+
* instances through the base collection, whereas this accessor's contract
|
|
117
|
+
* (spec §7.2) is to return frozen, read-only identity DTOs from an
|
|
118
|
+
* embedded-join projection. Offset pagination over a fully-ordered query is
|
|
119
|
+
* sufficient here and keeps the projected columns / freeze in one place.
|
|
120
|
+
*
|
|
121
|
+
* @param {object} query - PostgREST query builder
|
|
122
|
+
* @returns {Promise<Array<object>>}
|
|
123
|
+
* @private
|
|
124
|
+
*/
|
|
125
|
+
async #fetchOrgRows(query) {
|
|
126
|
+
const allResults = [];
|
|
127
|
+
let offset = 0;
|
|
128
|
+
let keepGoing = true;
|
|
129
|
+
// Secondary sort on semrush_project_id: brand_id alone does not give a
|
|
130
|
+
// stable page boundary when one brand has many projects (rows within a
|
|
131
|
+
// brand could otherwise straddle a page split in a different order on
|
|
132
|
+
// each call). semrush_project_id is unique per row, so this fully
|
|
133
|
+
// determines row order.
|
|
134
|
+
const orderedQuery = query.order('brand_id').order('semrush_project_id');
|
|
135
|
+
|
|
136
|
+
while (keepGoing) {
|
|
137
|
+
// `.range()` returns a NEW builder rather than mutating in place (true
|
|
138
|
+
// for the current supabase-js / postgrest-js lineage), so re-deriving
|
|
139
|
+
// each page from the shared `orderedQuery` is safe. A client fork that
|
|
140
|
+
// mutated in place would break this loop.
|
|
141
|
+
// eslint-disable-next-line no-await-in-loop
|
|
142
|
+
const { data, error } = await orderedQuery.range(offset, offset + DEFAULT_PAGE_SIZE - 1);
|
|
143
|
+
|
|
144
|
+
if (error) {
|
|
145
|
+
this.log.error(`[${this.entityName}] Failed to query mapping rows by organization - ${error.message}`, error);
|
|
146
|
+
throw new DataAccessError(
|
|
147
|
+
'Failed to query mapping rows by organization',
|
|
148
|
+
{ entityName: this.entityName, tableName: this.tableName },
|
|
149
|
+
error,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!data || data.length === 0) {
|
|
154
|
+
keepGoing = false;
|
|
155
|
+
} else {
|
|
156
|
+
allResults.push(...data);
|
|
157
|
+
offset += DEFAULT_PAGE_SIZE;
|
|
158
|
+
if (allResults.length >= MAX_ORG_ROWS) {
|
|
159
|
+
this.log.warn(`[${this.entityName}] allByOrganizationId truncated at ${MAX_ORG_ROWS} rows`);
|
|
160
|
+
keepGoing = false;
|
|
161
|
+
} else {
|
|
162
|
+
keepGoing = data.length >= DEFAULT_PAGE_SIZE;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return allResults.map((row) => Object.freeze({
|
|
168
|
+
brandId: row.brand_id,
|
|
169
|
+
semrushProjectId: row.semrush_project_id,
|
|
170
|
+
geoTargetId: row.semrush_location_id,
|
|
171
|
+
languageCode: row.language,
|
|
172
|
+
siteId: row.site_id ?? null,
|
|
173
|
+
organizationId: row.brands?.organization_id ?? null,
|
|
174
|
+
semrushSubWorkspaceId: row.brands?.semrush_sub_workspace_id ?? null,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
37
177
|
}
|
|
38
178
|
|
|
39
179
|
export default BrandSemrushProjectCollection;
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
* governing permissions and limitations under the License.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
hasText, isIsoDate, isValidUUID,
|
|
15
|
+
} from '@adobe/spacecat-shared-utils';
|
|
14
16
|
|
|
15
17
|
import SchemaBuilder from '../base/schema.builder.js';
|
|
16
18
|
import BrandSemrushProject from './brand-semrush-project.model.js';
|
|
@@ -68,6 +70,25 @@ const schema = new SchemaBuilder(BrandSemrushProject, BrandSemrushProjectCollect
|
|
|
68
70
|
validate: (value) => hasText(value) && LANGUAGE_TAG_REGEX.test(value),
|
|
69
71
|
postgrestField: 'language',
|
|
70
72
|
})
|
|
71
|
-
.addAllIndex(['semrushProjectId'])
|
|
73
|
+
.addAllIndex(['semrushProjectId'])
|
|
74
|
+
// Market-mirror Site for this project. Nullable — the mirror is created
|
|
75
|
+
// best-effort and may lag the row. Not unique: mirror Sites are per
|
|
76
|
+
// (brand, domain), so many projects of a brand sharing a domain share one
|
|
77
|
+
// Site. See serenity-docs brand-semrush-mapping-maintenance.md §5.1.
|
|
78
|
+
.addAttribute('siteId', {
|
|
79
|
+
type: 'string',
|
|
80
|
+
validate: (value) => !value || isValidUUID(value),
|
|
81
|
+
})
|
|
82
|
+
// Soft-delete tombstone. NULL = live (project exists in the child
|
|
83
|
+
// workspace, draft or live). Set = the project was deleted upstream —
|
|
84
|
+
// Semrush data does not survive deletion, so this is the only remaining
|
|
85
|
+
// record it existed. Mirrors the ApiKey.deletedAt precedent
|
|
86
|
+
// (api-key.schema.js) — no package-level soft-delete scope exists, so
|
|
87
|
+
// every accessor except `allByOrganizationId` (collection.js) returns
|
|
88
|
+
// tombstones; callers that need only live rows must filter explicitly.
|
|
89
|
+
.addAttribute('deletedAt', {
|
|
90
|
+
type: 'string',
|
|
91
|
+
validate: (value) => !value || isIsoDate(value),
|
|
92
|
+
});
|
|
72
93
|
|
|
73
94
|
export default schema.build();
|
|
@@ -25,9 +25,27 @@ export interface BrandSemrushProject extends BaseModel {
|
|
|
25
25
|
getSemrushProjectId(): string;
|
|
26
26
|
getGeoTargetId(): number;
|
|
27
27
|
getLanguageCode(): string;
|
|
28
|
+
getSiteId(): string | undefined;
|
|
29
|
+
getDeletedAt(): string | undefined;
|
|
28
30
|
setSemrushProjectId(value: string): BrandSemrushProject;
|
|
29
31
|
setGeoTargetId(value: number): BrandSemrushProject;
|
|
30
32
|
setLanguageCode(value: string): BrandSemrushProject;
|
|
33
|
+
setSiteId(value: string): BrandSemrushProject;
|
|
34
|
+
setDeletedAt(value: string): BrandSemrushProject;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface BrandSemrushProjectOrgRow {
|
|
38
|
+
brandId: string;
|
|
39
|
+
semrushProjectId: string;
|
|
40
|
+
geoTargetId: number;
|
|
41
|
+
languageCode: string;
|
|
42
|
+
siteId: string | null;
|
|
43
|
+
// Expected non-null under the select's `!inner` join (which excludes
|
|
44
|
+
// non-matching parents), but kept nullable to match the defensive runtime
|
|
45
|
+
// mapping (`row.brands?.organization_id ?? null`) — see
|
|
46
|
+
// brand-semrush-project.collection.js's #fetchOrgRows.
|
|
47
|
+
organizationId: string | null;
|
|
48
|
+
semrushSubWorkspaceId: string | null;
|
|
31
49
|
}
|
|
32
50
|
|
|
33
51
|
export interface BrandSemrushProjectCollection extends
|
|
@@ -41,4 +59,8 @@ export interface BrandSemrushProjectCollection extends
|
|
|
41
59
|
geoTargetId: number,
|
|
42
60
|
languageCode: string,
|
|
43
61
|
): Promise<BrandSemrushProject | null>;
|
|
62
|
+
allByOrganizationId(
|
|
63
|
+
organizationId: string,
|
|
64
|
+
options?: { includeDeleted?: boolean },
|
|
65
|
+
): Promise<BrandSemrushProjectOrgRow[]>;
|
|
44
66
|
}
|
|
@@ -29,6 +29,9 @@ class GeoExperiment extends BaseModel {
|
|
|
29
29
|
};
|
|
30
30
|
|
|
31
31
|
static PHASES = {
|
|
32
|
+
INITIATED: 'initiated',
|
|
33
|
+
PROMPT_GENERATION_STARTED: 'prompt_generation_started',
|
|
34
|
+
PROMPT_GENERATION_COMPLETED: 'prompt_generation_completed',
|
|
32
35
|
PRE_ANALYSIS_STARTED: 'pre_analysis_started',
|
|
33
36
|
PRE_ANALYSIS_DONE: 'pre_analysis_done',
|
|
34
37
|
DEPLOYMENT_STARTED: 'deployment_started',
|