@adobe/spacecat-shared-data-access 3.4.0 → 3.4.2
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/CLAUDE.md +204 -0
- package/README.md +77 -0
- package/docker-compose.test.yml +1 -1
- package/package.json +4 -4
- package/src/models/base/base.collection.js +26 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-data-access-v3.4.2](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.4.1...@adobe/spacecat-shared-data-access-v3.4.2) (2026-03-02)
|
|
2
|
+
|
|
3
|
+
### Bug Fixes
|
|
4
|
+
|
|
5
|
+
* **deps:** update external fixes ([#1223](https://github.com/adobe/spacecat-shared/issues/1223)) ([7ee8461](https://github.com/adobe/spacecat-shared/commit/7ee8461c99223d07a2f47bd6838b6942fcb30f28))
|
|
6
|
+
|
|
7
|
+
## [@adobe/spacecat-shared-data-access-v3.4.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.4.0...@adobe/spacecat-shared-data-access-v3.4.1) (2026-03-01)
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **data-access:** chunk batchGetByKeys to avoid 414 URI Too Large ([#1391](https://github.com/adobe/spacecat-shared/issues/1391)) ([25b0c0d](https://github.com/adobe/spacecat-shared/commit/25b0c0dab34b4e8e8b5fef9f4033cbcc698652f5)), closes [#1390](https://github.com/adobe/spacecat-shared/issues/1390)
|
|
12
|
+
|
|
1
13
|
## [@adobe/spacecat-shared-data-access-v3.4.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-data-access-v3.3.0...@adobe/spacecat-shared-data-access-v3.4.0) (2026-02-26)
|
|
2
14
|
|
|
3
15
|
### Features
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# spacecat-shared-data-access
|
|
2
|
+
|
|
3
|
+
Shared data-access layer for SpaceCat services. Provides entity models/collections backed by PostgreSQL via PostgREST.
|
|
4
|
+
|
|
5
|
+
## Architecture
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
Lambda/ECS service
|
|
9
|
+
-> this package (@adobe/spacecat-shared-data-access)
|
|
10
|
+
-> @supabase/postgrest-js
|
|
11
|
+
-> mysticat-data-service (PostgREST + Aurora PostgreSQL)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- **Database schema**: lives in [mysticat-data-service](https://github.com/adobe/mysticat-data-service) as dbmate SQL migrations
|
|
15
|
+
- **This package**: JavaScript model/collection layer mapping camelCase entities to snake_case PostgREST API
|
|
16
|
+
- **v2 (retired)**: ElectroDB -> DynamoDB. Published as `@adobe/spacecat-shared-data-access-v2`
|
|
17
|
+
- **v3 (current)**: PostgREST client -> mysticat-data-service
|
|
18
|
+
|
|
19
|
+
## Key Files
|
|
20
|
+
|
|
21
|
+
| File | Purpose |
|
|
22
|
+
|------|---------|
|
|
23
|
+
| `src/index.js` | Default export: `dataAccessWrapper(fn)` for Helix/Lambda handlers |
|
|
24
|
+
| `src/service/index.js` | `createDataAccess(config, log?, client?)` factory |
|
|
25
|
+
| `src/models/base/schema.builder.js` | DSL for defining entity schemas (attributes, references, indexes) |
|
|
26
|
+
| `src/models/base/base.model.js` | Base entity class (auto-generated getters/setters, save, remove) |
|
|
27
|
+
| `src/models/base/base.collection.js` | Base collection class (findById, all, query, count) |
|
|
28
|
+
| `src/models/base/entity.registry.js` | Registers all entity collections |
|
|
29
|
+
| `src/util/postgrest.utils.js` | camelCase<->snake_case field mapping, query builders, cursor pagination |
|
|
30
|
+
| `src/models/index.js` | Barrel export of all entity models |
|
|
31
|
+
|
|
32
|
+
## Entity Structure
|
|
33
|
+
|
|
34
|
+
Each entity lives in `src/models/<entity>/` with 4 files:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
src/models/site/
|
|
38
|
+
site.schema.js # SchemaBuilder definition (attributes, references, indexes)
|
|
39
|
+
site.model.js # Extends BaseModel (business logic, constants)
|
|
40
|
+
site.collection.js # Extends BaseCollection (custom queries)
|
|
41
|
+
index.js # Re-exports model, collection, schema
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Schema Definition Pattern
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
const schema = new SchemaBuilder(Site, SiteCollection)
|
|
48
|
+
.addReference('belongs_to', 'Organization') // FK -> organizations.id
|
|
49
|
+
.addReference('has_many', 'Audits') // One-to-many relationship
|
|
50
|
+
.addAttribute('baseURL', {
|
|
51
|
+
type: 'string',
|
|
52
|
+
required: true,
|
|
53
|
+
validate: (value) => isValidUrl(value),
|
|
54
|
+
})
|
|
55
|
+
.addAttribute('deliveryType', {
|
|
56
|
+
type: Object.values(Site.DELIVERY_TYPES), // Enum validation
|
|
57
|
+
default: Site.DEFAULT_DELIVERY_TYPE,
|
|
58
|
+
required: true,
|
|
59
|
+
})
|
|
60
|
+
.addAttribute('config', {
|
|
61
|
+
type: 'any',
|
|
62
|
+
default: DEFAULT_CONFIG,
|
|
63
|
+
get: (value) => Config(value), // Transform on read
|
|
64
|
+
})
|
|
65
|
+
.addAllIndex(['imsOrgId']) // Query index
|
|
66
|
+
.build();
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Attribute Options
|
|
70
|
+
|
|
71
|
+
| Option | Purpose |
|
|
72
|
+
|--------|---------|
|
|
73
|
+
| `type` | `'string'`, `'number'`, `'boolean'`, `'any'`, `'map'`, or array of enum values |
|
|
74
|
+
| `required` | Validation on save |
|
|
75
|
+
| `default` | Default value or factory function |
|
|
76
|
+
| `validate` | Custom validation function |
|
|
77
|
+
| `readOnly` | No setter generated |
|
|
78
|
+
| `postgrestField` | Custom DB column name (default: `camelToSnake(name)`) |
|
|
79
|
+
| `postgrestIgnore` | Virtual attribute, not sent to DB |
|
|
80
|
+
| `hidden` | Excluded from `toJSON()` |
|
|
81
|
+
| `watch` | Array of field names that trigger this attribute's setter |
|
|
82
|
+
| `set` | Custom setter `(value, allAttrs) => transformedValue` |
|
|
83
|
+
| `get` | Custom getter `(value) => transformedValue` |
|
|
84
|
+
|
|
85
|
+
### Field Mapping
|
|
86
|
+
|
|
87
|
+
Models use camelCase, database uses snake_case. Mapping is automatic:
|
|
88
|
+
|
|
89
|
+
| Model field | DB column | Notes |
|
|
90
|
+
|-------------|-----------|-------|
|
|
91
|
+
| `siteId` (idName) | `id` | Primary key always maps to `id` |
|
|
92
|
+
| `baseURL` | `base_url` | Auto camelToSnake |
|
|
93
|
+
| `organizationId` | `organization_id` | FK from `belongs_to` reference |
|
|
94
|
+
| `isLive` | `is_live` | Auto camelToSnake |
|
|
95
|
+
|
|
96
|
+
Override with `postgrestField: 'custom_name'` on the attribute.
|
|
97
|
+
|
|
98
|
+
## Changing Entities
|
|
99
|
+
|
|
100
|
+
Changes require **two repos**:
|
|
101
|
+
|
|
102
|
+
### 1. Database schema — [mysticat-data-service](https://github.com/adobe/mysticat-data-service)
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
make migrate-new name=add_foo_to_sites
|
|
106
|
+
# Edit the migration SQL (table, columns, indexes, grants, comments)
|
|
107
|
+
make migrate && make test
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Every migration must include: indexes on FKs, `GRANT` to `postgrest_anon`/`postgrest_writer`, `COMMENT ON` for OpenAPI docs. See [mysticat-data-service CLAUDE.md](https://github.com/adobe/mysticat-data-service/blob/main/CLAUDE.md).
|
|
111
|
+
|
|
112
|
+
### 2. Model layer — this package
|
|
113
|
+
|
|
114
|
+
- Add attribute in `<entity>.schema.js` -> auto-generates getter/setter
|
|
115
|
+
- Add business logic in `<entity>.model.js`
|
|
116
|
+
- Add custom queries in `<entity>.collection.js`
|
|
117
|
+
- New entity: create 4 files + register in `src/models/index.js`
|
|
118
|
+
|
|
119
|
+
### 3. Integration test
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npm run test:it # Spins up PostgREST via Docker, runs mocha suite
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Testing
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm test # Unit tests (mocha + sinon + chai)
|
|
129
|
+
npm run test:debug # Unit tests with debugger
|
|
130
|
+
npm run test:it # Integration tests (Docker: Postgres + PostgREST)
|
|
131
|
+
npm run lint # ESLint
|
|
132
|
+
npm run lint:fix # Auto-fix lint issues
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Integration Test Setup
|
|
136
|
+
|
|
137
|
+
Integration tests pull the `mysticat-data-service` Docker image from ECR:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# ECR login (one-time)
|
|
141
|
+
aws ecr get-login-password --profile spacecat-dev --region us-east-1 \
|
|
142
|
+
| docker login --username AWS --password-stdin 682033462621.dkr.ecr.us-east-1.amazonaws.com
|
|
143
|
+
|
|
144
|
+
# Override image tag
|
|
145
|
+
export MYSTICAT_DATA_SERVICE_TAG=v1.13.0
|
|
146
|
+
npm run test:it
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Unit Test Conventions
|
|
150
|
+
|
|
151
|
+
- Tests in `test/unit/models/<entity>/`
|
|
152
|
+
- PostgREST calls are stubbed via sinon
|
|
153
|
+
- Each entity model and collection has its own test file
|
|
154
|
+
|
|
155
|
+
## Common Patterns
|
|
156
|
+
|
|
157
|
+
### Collection query with WHERE clause
|
|
158
|
+
|
|
159
|
+
```js
|
|
160
|
+
// In a collection method
|
|
161
|
+
async findByStatus(status) {
|
|
162
|
+
return this.all(
|
|
163
|
+
(attrs, op) => op.eq(attrs.status, status),
|
|
164
|
+
{ limit: 100, order: { field: 'createdAt', direction: 'desc' } }
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Reference traversal
|
|
170
|
+
|
|
171
|
+
```js
|
|
172
|
+
// belongs_to: site.getOrganization() -> fetches parent org
|
|
173
|
+
// has_many: organization.getSites() -> fetches child sites
|
|
174
|
+
const site = await dataAccess.Site.findById(id);
|
|
175
|
+
const org = await site.getOrganization();
|
|
176
|
+
const audits = await site.getAudits();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### PostgREST WHERE operators
|
|
180
|
+
|
|
181
|
+
`eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `is`, `in`, `contains`, `like`, `ilike`
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
// Usage in collection.all()
|
|
185
|
+
const liveSites = await dataAccess.Site.all(
|
|
186
|
+
(attrs, op) => op.eq(attrs.isLive, true)
|
|
187
|
+
);
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Environment Variables
|
|
191
|
+
|
|
192
|
+
| Variable | Required | Purpose |
|
|
193
|
+
|----------|----------|---------|
|
|
194
|
+
| `POSTGREST_URL` | Yes | PostgREST base URL (e.g. `http://data-svc.internal`) |
|
|
195
|
+
| `POSTGREST_SCHEMA` | No | Schema name (default: `public`) |
|
|
196
|
+
| `POSTGREST_API_KEY` | No | JWT for `postgrest_writer` role (enables UPDATE/DELETE) |
|
|
197
|
+
| `S3_CONFIG_BUCKET` | No | Only for `Configuration` entity |
|
|
198
|
+
| `AWS_REGION` | No | Only for `Configuration` entity |
|
|
199
|
+
|
|
200
|
+
## Special Entities
|
|
201
|
+
|
|
202
|
+
- **Configuration**: S3-backed (not PostgREST). Requires `S3_CONFIG_BUCKET`.
|
|
203
|
+
- **KeyEvent**: Deprecated in v3. All methods throw.
|
|
204
|
+
- **LatestAudit**: Virtual entity computed from `Audit` queries (no dedicated table).
|
package/README.md
CHANGED
|
@@ -134,12 +134,88 @@ Current exported entities include:
|
|
|
134
134
|
- `TrialUser`
|
|
135
135
|
- `TrialUserActivity`
|
|
136
136
|
|
|
137
|
+
## Architecture
|
|
138
|
+
|
|
139
|
+
```
|
|
140
|
+
Lambda / ECS service
|
|
141
|
+
-> @adobe/spacecat-shared-data-access (this package)
|
|
142
|
+
-> @supabase/postgrest-js
|
|
143
|
+
-> mysticat-data-service (PostgREST + Aurora PostgreSQL)
|
|
144
|
+
https://github.com/adobe/mysticat-data-service
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
**v2 (retired):** ElectroDB -> DynamoDB (direct, schema-in-code)
|
|
148
|
+
**v3 (current):** PostgREST client -> [mysticat-data-service](https://github.com/adobe/mysticat-data-service) (schema-in-database)
|
|
149
|
+
|
|
150
|
+
The database schema (tables, indexes, enums, grants) lives in **mysticat-data-service** as dbmate migrations.
|
|
151
|
+
This package provides the JavaScript model/collection layer that maps camelCase entities to the snake_case PostgREST API.
|
|
152
|
+
|
|
137
153
|
## V3 Behavior Notes
|
|
138
154
|
|
|
139
155
|
- `Configuration` remains S3-backed in v3.
|
|
140
156
|
- `KeyEvent` is deprecated in v3 and intentionally throws on access/mutation methods.
|
|
141
157
|
- `LatestAudit` is virtual in v3 and derived from `Audit` queries (no dedicated table required).
|
|
142
158
|
|
|
159
|
+
## Changing Entities
|
|
160
|
+
|
|
161
|
+
Adding or modifying an entity now requires changes in **two repositories**:
|
|
162
|
+
|
|
163
|
+
### 1. Database schema — [mysticat-data-service](https://github.com/adobe/mysticat-data-service)
|
|
164
|
+
|
|
165
|
+
Create a dbmate migration for the schema change (table, columns, indexes, grants, enums):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# In mysticat-data-service
|
|
169
|
+
make migrate-new name=add_foo_column_to_sites
|
|
170
|
+
# Edit db/migrations/YYYYMMDDHHMMSS_add_foo_column_to_sites.sql
|
|
171
|
+
make migrate
|
|
172
|
+
docker compose -f docker/docker-compose.yml restart postgrest
|
|
173
|
+
make test
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
See the [mysticat-data-service CLAUDE.md](https://github.com/adobe/mysticat-data-service/blob/main/CLAUDE.md) for migration conventions (required grants, indexes, comments, etc.).
|
|
177
|
+
|
|
178
|
+
### 2. Model/collection layer — this package
|
|
179
|
+
|
|
180
|
+
Update the entity schema, model, and/or collection in `src/models/<entity>/`:
|
|
181
|
+
|
|
182
|
+
| File | What to change |
|
|
183
|
+
|------|---------------|
|
|
184
|
+
| `<entity>.schema.js` | Add/modify attributes, references, indexes |
|
|
185
|
+
| `<entity>.model.js` | Add business logic methods |
|
|
186
|
+
| `<entity>.collection.js` | Add custom query methods |
|
|
187
|
+
|
|
188
|
+
**Adding a new attribute example:**
|
|
189
|
+
|
|
190
|
+
```js
|
|
191
|
+
// In <entity>.schema.js, add to the SchemaBuilder chain:
|
|
192
|
+
.addAttribute('myNewField', {
|
|
193
|
+
type: 'string',
|
|
194
|
+
required: false,
|
|
195
|
+
// Optional: custom DB column name (default: camelToSnake)
|
|
196
|
+
// postgrestField: 'custom_column_name',
|
|
197
|
+
})
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
This automatically generates `getMyNewField()` and `setMyNewField()` on the model.
|
|
201
|
+
|
|
202
|
+
**Adding a new entity:** Create 4 files following the pattern in any existing entity folder:
|
|
203
|
+
- `<entity>.schema.js` — SchemaBuilder definition
|
|
204
|
+
- `<entity>.model.js` — extends `BaseModel`
|
|
205
|
+
- `<entity>.collection.js` — extends `BaseCollection`
|
|
206
|
+
- `index.js` — re-exports model, collection, schema
|
|
207
|
+
|
|
208
|
+
Then register the entity in `src/models/index.js`.
|
|
209
|
+
|
|
210
|
+
### 3. Integration test the full stack
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# In this package — runs PostgREST in Docker
|
|
214
|
+
npm run test:it
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Integration tests pull the `mysticat-data-service` Docker image from ECR, so new schema changes must be published as a new image tag first (or test against a local PostgREST).
|
|
218
|
+
|
|
143
219
|
## Migrating from V2
|
|
144
220
|
|
|
145
221
|
If you are upgrading from DynamoDB/ElectroDB-based v2:
|
|
@@ -154,6 +230,7 @@ If you are upgrading from DynamoDB/ElectroDB-based v2:
|
|
|
154
230
|
|
|
155
231
|
- Backing store is now Postgres via PostgREST, not DynamoDB/ElectroDB.
|
|
156
232
|
- You must provide `postgrestUrl` (or `POSTGREST_URL` via wrapper env).
|
|
233
|
+
- Schema changes now go through [mysticat-data-service](https://github.com/adobe/mysticat-data-service) migrations, not code.
|
|
157
234
|
- `Configuration` remains S3-backed (requires `s3Bucket`/`S3_CONFIG_BUCKET` when used).
|
|
158
235
|
- `KeyEvent` is deprecated in v3 and now throws.
|
|
159
236
|
- `LatestAudit` is no longer a dedicated table and is computed from `Audit` queries.
|
package/docker-compose.test.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adobe/spacecat-shared-data-access",
|
|
3
|
-
"version": "3.4.
|
|
3
|
+
"version": "3.4.2",
|
|
4
4
|
"description": "Shared modules of the Spacecat Services - Data Access",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"pluralize": "8.0.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
|
-
"chai": "6.2.
|
|
52
|
+
"chai": "6.2.2",
|
|
53
53
|
"chai-as-promised": "8.0.2",
|
|
54
|
-
"nock": "14.0.
|
|
55
|
-
"sinon": "21.0.
|
|
54
|
+
"nock": "14.0.11",
|
|
55
|
+
"sinon": "21.0.1",
|
|
56
56
|
"sinon-chai": "4.0.1"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -617,21 +617,37 @@ class BaseCollection {
|
|
|
617
617
|
const dbField = this.#toDbField(bulkKeyField);
|
|
618
618
|
const values = keys.map((key) => key[bulkKeyField]);
|
|
619
619
|
const select = this.#buildSelect(options.attributes);
|
|
620
|
-
const { data, error } = await this.postgrestService
|
|
621
|
-
.from(this.tableName)
|
|
622
|
-
.select(select)
|
|
623
|
-
.in(dbField, values);
|
|
624
620
|
|
|
625
|
-
|
|
621
|
+
// Chunk values to avoid 414 URI Too Large from PostgREST GET URLs.
|
|
622
|
+
// Each UUID is ~36 chars; 50 × 36 ≈ 1,800 chars, well under the 8KB URL limit.
|
|
623
|
+
const CHUNK_SIZE = 50;
|
|
624
|
+
const allData = [];
|
|
625
|
+
let hadInvalidInput = false;
|
|
626
|
+
|
|
627
|
+
for (let i = 0; i < values.length; i += CHUNK_SIZE) {
|
|
628
|
+
const chunk = values.slice(i, i + CHUNK_SIZE);
|
|
629
|
+
// eslint-disable-next-line no-await-in-loop
|
|
630
|
+
const { data, error } = await this.postgrestService
|
|
631
|
+
.from(this.tableName)
|
|
632
|
+
.select(select)
|
|
633
|
+
.in(dbField, chunk);
|
|
634
|
+
|
|
635
|
+
if (error) {
|
|
636
|
+
if (!this.#isInvalidInputError(error)) {
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
hadInvalidInput = true;
|
|
640
|
+
break;
|
|
641
|
+
}
|
|
642
|
+
allData.push(...(data || []));
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
if (!hadInvalidInput) {
|
|
626
646
|
return {
|
|
627
|
-
data: this.#createInstances(
|
|
647
|
+
data: this.#createInstances(allData.map((record) => this.#toModelRecord(record))),
|
|
628
648
|
unprocessed: [],
|
|
629
649
|
};
|
|
630
650
|
}
|
|
631
|
-
|
|
632
|
-
if (!this.#isInvalidInputError(error)) {
|
|
633
|
-
throw error;
|
|
634
|
-
}
|
|
635
651
|
}
|
|
636
652
|
|
|
637
653
|
const records = await Promise.all(
|