@ftschopp/dynatable-core 1.4.2 → 1.4.4
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 +14 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.js +19 -2
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +25 -0
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/utils/model-utils.d.ts +9 -3
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +15 -7
- package/package.json +19 -2
- package/eslint.config.mjs +0 -4
- package/jest.config.js +0 -11
- package/src/builders/README.md +0 -339
- package/src/builders/batch-get/README.md +0 -43
- package/src/builders/batch-get/create-batch-get-builder.test.ts +0 -193
- package/src/builders/batch-get/create-batch-get-builder.ts +0 -108
- package/src/builders/batch-get/index.ts +0 -2
- package/src/builders/batch-get/types.ts +0 -28
- package/src/builders/batch-write/README.md +0 -204
- package/src/builders/batch-write/create-batch-write-builder.test.ts +0 -173
- package/src/builders/batch-write/create-batch-write-builder.ts +0 -49
- package/src/builders/batch-write/index.ts +0 -2
- package/src/builders/batch-write/types.ts +0 -33
- package/src/builders/delete/create-delete-builder.test.ts +0 -294
- package/src/builders/delete/create-delete-builder.ts +0 -100
- package/src/builders/delete/index.ts +0 -2
- package/src/builders/delete/types.ts +0 -17
- package/src/builders/get/create-get-builder.test.ts +0 -272
- package/src/builders/get/create-get-builder.ts +0 -117
- package/src/builders/get/index.ts +0 -2
- package/src/builders/get/types.ts +0 -39
- package/src/builders/index.ts +0 -14
- package/src/builders/put/create-put-builder.test.ts +0 -213
- package/src/builders/put/create-put-builder.ts +0 -160
- package/src/builders/put/index.ts +0 -2
- package/src/builders/put/types.ts +0 -24
- package/src/builders/query/create-query-builder.test.ts +0 -770
- package/src/builders/query/create-query-builder.ts +0 -455
- package/src/builders/query/index.ts +0 -2
- package/src/builders/query/types.ts +0 -109
- package/src/builders/scan/create-scan-builder.test.ts +0 -416
- package/src/builders/scan/create-scan-builder.ts +0 -265
- package/src/builders/scan/index.ts +0 -2
- package/src/builders/scan/types.ts +0 -88
- package/src/builders/shared/conditions.ts +0 -58
- package/src/builders/shared/index.ts +0 -4
- package/src/builders/shared/operators.test.ts +0 -270
- package/src/builders/shared/operators.ts +0 -208
- package/src/builders/shared/projection.ts +0 -28
- package/src/builders/shared/types.ts +0 -101
- package/src/builders/transact-get/README.md +0 -167
- package/src/builders/transact-get/create-transact-get-builder.test.ts +0 -239
- package/src/builders/transact-get/create-transact-get-builder.ts +0 -67
- package/src/builders/transact-get/index.ts +0 -2
- package/src/builders/transact-get/types.ts +0 -31
- package/src/builders/transact-write/README.md +0 -202
- package/src/builders/transact-write/create-transact-write-builder.test.ts +0 -288
- package/src/builders/transact-write/create-transact-write-builder.ts +0 -124
- package/src/builders/transact-write/index.ts +0 -2
- package/src/builders/transact-write/types.ts +0 -71
- package/src/builders/update/create-update-builder.test.ts +0 -573
- package/src/builders/update/create-update-builder.ts +0 -375
- package/src/builders/update/index.ts +0 -2
- package/src/builders/update/types.ts +0 -63
- package/src/core/types.test.ts +0 -641
- package/src/core/types.ts +0 -366
- package/src/entity/create-entity-api.ts +0 -224
- package/src/entity/index.ts +0 -19
- package/src/entity/middleware/factories.ts +0 -15
- package/src/entity/middleware/types.ts +0 -7
- package/src/entity/middleware/with-middleware.ts +0 -37
- package/src/entity/types.ts +0 -79
- package/src/entity/validation/key-validation.ts +0 -34
- package/src/index.ts +0 -23
- package/src/table.ts +0 -116
- package/src/utils/dynamodb-logger.test.ts +0 -246
- package/src/utils/dynamodb-logger.ts +0 -175
- package/src/utils/model-utils.test.ts +0 -452
- package/src/utils/model-utils.ts +0 -188
- package/src/utils/zod-utils.test.ts +0 -363
- package/src/utils/zod-utils.ts +0 -56
- package/tests/integration/instagram-clone.integration.test.ts +0 -1019
- package/tests/integration/pagination-timestamps.integration.test.ts +0 -374
- package/tests/integration/transactions.integration.test.ts +0 -528
- package/tsconfig.json +0 -12
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
+
## @ftschopp/dynatable-core [1.4.4](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.4.3...@ftschopp/dynatable-core@1.4.4) (2026-05-09)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **core:** detect SET-collisions on auto-recomputed index keys; stripInternalKeys recurses ([#38](https://github.com/ftschopp/dynatable/issues/38)) ([d609c5b](https://github.com/ftschopp/dynatable/commit/d609c5bbeb566145ec034ec9883246a6d77d7cf6)), closes [#17](https://github.com/ftschopp/dynatable/issues/17) [#GSI1](https://github.com/ftschopp/dynatable/issues/GSI1) [#GSI1](https://github.com/ftschopp/dynatable/issues/GSI1)
|
|
7
|
+
|
|
8
|
+
## @ftschopp/dynatable-core [1.4.3](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.4.2...@ftschopp/dynatable-core@1.4.3) (2026-05-09)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **query:** resolve index keys exactly, not by prefix match ([#34](https://github.com/ftschopp/dynatable/issues/34)) ([5ca8654](https://github.com/ftschopp/dynatable/commit/5ca865465454e84bee6b5e3e1510f5905a7027d9)), closes [#11](https://github.com/ftschopp/dynatable/issues/11)
|
|
14
|
+
|
|
1
15
|
## @ftschopp/dynatable-core [1.4.2](https://github.com/ftschopp/dynatable/compare/@ftschopp/dynatable-core@1.4.1...@ftschopp/dynatable-core@1.4.2) (2026-05-08)
|
|
2
16
|
|
|
3
17
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,
|
|
1
|
+
{"version":3,"file":"create-query-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/query/create-query-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAM1D,OAAO,EAAE,YAAY,EAA8B,MAAM,SAAS,CAAC;AACnE,OAAO,EAAiB,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAElE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AA8a7D;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EACtC,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,cAAc,EACtB,KAAK,CAAC,EAAE,eAAe,EACvB,MAAM,CAAC,EAAE,cAAc,EACvB,UAAU,CAAC,EAAE,MAAM,GAClB,YAAY,CAAC,KAAK,CAAC,CA6BrB"}
|
|
@@ -6,6 +6,23 @@ const operators_1 = require("../shared/operators");
|
|
|
6
6
|
const conditions_1 = require("../shared/conditions");
|
|
7
7
|
const projection_1 = require("../shared/projection");
|
|
8
8
|
const model_utils_1 = require("../../utils/model-utils");
|
|
9
|
+
/**
|
|
10
|
+
* Decide whether an entry in `model.index` belongs to the given index.
|
|
11
|
+
*
|
|
12
|
+
* 1. If the key declaration carries an explicit `indexName`, that wins —
|
|
13
|
+
* consumers with non-conventional names (e.g. index `BySpotifyId` with
|
|
14
|
+
* keys `lookupPK`/`lookupSK`) opt in by setting it.
|
|
15
|
+
* 2. Otherwise we fall back to the historical convention but with an EXACT
|
|
16
|
+
* `<indexName>PK` / `<indexName>SK` suffix check (not `startsWith`), so
|
|
17
|
+
* sibling indexes whose names share a prefix — `GSI1` vs `GSI10` — don't
|
|
18
|
+
* cross-pollute key resolution.
|
|
19
|
+
*/
|
|
20
|
+
function keyBelongsToIndex(keyName, keyDef, indexName) {
|
|
21
|
+
if (keyDef.indexName !== undefined) {
|
|
22
|
+
return keyDef.indexName === indexName;
|
|
23
|
+
}
|
|
24
|
+
return keyName === `${indexName}PK` || keyName === `${indexName}SK`;
|
|
25
|
+
}
|
|
9
26
|
/**
|
|
10
27
|
* Maps a model attribute name to its corresponding DynamoDB key name (PK/SK)
|
|
11
28
|
* Also returns the key template for value transformation
|
|
@@ -16,7 +33,7 @@ function getKeyNameForAttribute(fieldName, model, indexName) {
|
|
|
16
33
|
// When an index is specified, check model.index first for key templates
|
|
17
34
|
if (indexName && model.index) {
|
|
18
35
|
for (const [keyName, keyDef] of Object.entries(model.index)) {
|
|
19
|
-
if (!keyName
|
|
36
|
+
if (!keyBelongsToIndex(keyName, keyDef, indexName)) {
|
|
20
37
|
continue;
|
|
21
38
|
}
|
|
22
39
|
const templateVars = (0, model_utils_1.extractTemplateVars)(keyDef.value);
|
|
@@ -198,7 +215,7 @@ function createQueryExecutor(state) {
|
|
|
198
215
|
if (keyConditions.length === 0) {
|
|
199
216
|
const keyTemplates = state.indexName
|
|
200
217
|
? Object.entries(state.model?.index ?? {})
|
|
201
|
-
.filter(([keyName]) => keyName
|
|
218
|
+
.filter(([keyName, def]) => keyBelongsToIndex(keyName, def, state.indexName))
|
|
202
219
|
.map(([, def]) => def.value)
|
|
203
220
|
: Object.values(state.model?.key ?? {}).map((def) => def.value);
|
|
204
221
|
const templateVars = Array.from(new Set(keyTemplates.flatMap((tpl) => (0, model_utils_1.extractTemplateVars)(tpl))));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-update-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/update/create-update-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAgC,SAAS,EAA4B,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;
|
|
1
|
+
{"version":3,"file":"create-update-builder.d.ts","sourceRoot":"","sources":["../../../src/builders/update/create-update-builder.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAE1D,OAAO,EAAgC,SAAS,EAA4B,MAAM,WAAW,CAAC;AAC9F,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpE,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAa7D;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EACvC,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,OAAO,CAAC,KAAK,CAAC,EACnB,MAAM,EAAE,cAAc,EACtB,cAAc,GAAE,SAAS,EAAO,EAChC,aAAa,GAAE;IACb,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,GAAG,EAAE,YAAY,EAAE,CAAC;IACpB,MAAM,EAAE,YAAY,EAAE,CAAC;CACuB,EAChD,UAAU,GAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,GAAG,aAAsB,EACnF,YAAY,SAAI,EAChB,gBAAgB,UAAQ,EACxB,MAAM,CAAC,EAAE,cAAc,EACvB,YAAY,CAAC,EAAE,YAAY,EAC3B,SAAS,GAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAM,GAClC,aAAa,CAAC,KAAK,CAAC,CAsWtB"}
|
|
@@ -4,6 +4,15 @@ exports.createUpdateBuilder = createUpdateBuilder;
|
|
|
4
4
|
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
5
5
|
const shared_1 = require("../shared");
|
|
6
6
|
const model_utils_1 = require("../../utils/model-utils");
|
|
7
|
+
/**
|
|
8
|
+
* Pull the LHS attribute name out of a SET-action expression like
|
|
9
|
+
* `#GSI1PK = :v_0` → `'GSI1PK'`. Returns `undefined` for shapes the
|
|
10
|
+
* builder doesn't emit so callers can ignore them safely.
|
|
11
|
+
*/
|
|
12
|
+
function extractAttrName(action) {
|
|
13
|
+
const m = action.expression.match(/^\s*#([A-Za-z0-9_]+)\s*=/);
|
|
14
|
+
return m?.[1];
|
|
15
|
+
}
|
|
7
16
|
/**
|
|
8
17
|
* Creates an UpdateBuilder for an item key and table.
|
|
9
18
|
*
|
|
@@ -120,6 +129,22 @@ function createUpdateBuilder(tableName, key, client, prevConditions = [], update
|
|
|
120
129
|
`but the templates cannot be fully resolved from the update payload. ` +
|
|
121
130
|
`Include the missing fields in .set():\n${details}`);
|
|
122
131
|
}
|
|
132
|
+
// If the user's `.set()` already targets the same index-key
|
|
133
|
+
// attribute (e.g. `.set('GSI1PK', 'foo')`), refuse to silently
|
|
134
|
+
// emit a second SET against the same path. DynamoDB rejects
|
|
135
|
+
// `SET #GSI1PK = :a, #GSI1PK = :b` outright, and namespacing the
|
|
136
|
+
// placeholders only hides which one wins. Force the caller to
|
|
137
|
+
// resolve the conflict explicitly.
|
|
138
|
+
const userSetKeys = new Set(actionsToProcess.set.map(extractAttrName));
|
|
139
|
+
userSetKeys.delete(undefined);
|
|
140
|
+
const conflicts = Object.keys(idxActions).filter((k) => userSetKeys.has(k));
|
|
141
|
+
if (conflicts.length > 0) {
|
|
142
|
+
throw new Error(`Update would write the same secondary-index key twice: ` +
|
|
143
|
+
`[${conflicts.join(', ')}] is recomputed from the index template AND ` +
|
|
144
|
+
`set explicitly via .set(). Either remove the explicit .set() and let ` +
|
|
145
|
+
`the recomputation handle it, or include all template variables in ` +
|
|
146
|
+
`.set() so you take full control.`);
|
|
147
|
+
}
|
|
123
148
|
for (const [indexName, resolved] of Object.entries(idxActions)) {
|
|
124
149
|
const valueName = getUniqueValueName(indexName);
|
|
125
150
|
actionsToProcess.set.push({
|
package/dist/core/types.d.ts
CHANGED
|
@@ -83,10 +83,15 @@ export type AttributeDefinition = ScalarAttributeDefinition | ObjectAttributeDef
|
|
|
83
83
|
*
|
|
84
84
|
* @property type - Always String for DynamoDB keys
|
|
85
85
|
* @property value - Template string for key generation
|
|
86
|
+
* @property indexName - Optional explicit association of this key to a secondary
|
|
87
|
+
* index. Use this when index keys are not named with the conventional
|
|
88
|
+
* `<indexName>PK` / `<indexName>SK` pattern, or to disambiguate indexes
|
|
89
|
+
* whose names share a prefix (e.g. `GSI1` and `GSI10`).
|
|
86
90
|
*/
|
|
87
91
|
export type KeyDefinition = {
|
|
88
92
|
type: StringConstructor;
|
|
89
93
|
value: string;
|
|
94
|
+
indexName?: string;
|
|
90
95
|
};
|
|
91
96
|
/**
|
|
92
97
|
* Primary key definition - requires both PK and SK (uppercase)
|
package/dist/core/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAIH;;GAEG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,iBAAiB,GAAG,iBAAiB,GAAG,kBAAkB,GAAG,eAAe,CAAC;IACnF,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,yBAAyB,GAAG;IACtC,IAAI,EAAE,iBAAiB,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,CAAC;IAC5C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,MAAM,wBAAwB,GAAG;IACrC,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,mBAAmB,CAAC;IAC3B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,mBAAmB,GAC3B,yBAAyB,GACzB,yBAAyB,GACzB,wBAAwB,CAAC;AAE7B
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -50,9 +50,15 @@ export declare const applyPostDefaults: <M extends ModelDefinition>(model: M, va
|
|
|
50
50
|
timestamps?: boolean;
|
|
51
51
|
}) => InferModel<M>;
|
|
52
52
|
/**
|
|
53
|
-
* Removes internal DynamoDB keys from an item or array of items
|
|
54
|
-
*
|
|
55
|
-
*
|
|
53
|
+
* Removes internal DynamoDB keys from an item or array of items.
|
|
54
|
+
*
|
|
55
|
+
* Recurses into nested plain objects and arrays so an `_type` / `PK` / `SK`
|
|
56
|
+
* field embedded inside an attribute value (e.g. a denormalized snapshot
|
|
57
|
+
* of another entity) is also stripped. Class instances such as `Date` are
|
|
58
|
+
* passed through untouched — recursing into them would lose their type.
|
|
59
|
+
*
|
|
60
|
+
* @param data - Single item, array of items, or any nested value from DynamoDB
|
|
61
|
+
* @returns Data with internal keys removed at every level
|
|
56
62
|
*/
|
|
57
63
|
export declare const stripInternalKeys: <T>(data: T | T[] | undefined) => T | T[] | undefined;
|
|
58
64
|
//# sourceMappingURL=model-utils.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,eAAe,EAC3D,OAAO,CAAC,EACR,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC3B,mBAwBF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,CAiCd,CAAC;AAOF
|
|
1
|
+
{"version":3,"file":"model-utils.d.ts","sourceRoot":"","sources":["../../src/utils/model-utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,UAAU,EAAiB,eAAe,EAAE,MAAM,cAAc,CAAC;AAG1E;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,GAAI,UAAU,MAAM,KAAG,MAAM,EAG5D,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,eAAe,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAG,MAoB7E,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GAAI,CAAC,SAAS,eAAe,EACnD,OAAO,CAAC,EACR,OAAO,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC1B,OAAM,KAAK,GAAG,OAAO,GAAG,MAAc,KACrC,MAAM,CAAC,MAAM,EAAE,MAAM,CAWvB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,EAAE,CAAA;KAAE,EAAE,CAAC;CACnE,CAAC;AAEF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,mBAAmB,GAAI,CAAC,SAAS,eAAe,EAC3D,OAAO,CAAC,EACR,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC5B,SAAS,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAC3B,mBAwBF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,SAAS,eAAe,EACzD,OAAO,CAAC,EACR,eAAe,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAClC,UAAU;IAAE,QAAQ,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,KACrD,UAAU,CAAC,CAAC,CAiCd,CAAC;AAOF;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAAI,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,GAAG,SAAS,KAAG,CAAC,GAAG,CAAC,EAAE,GAAG,SAqB1E,CAAC"}
|
|
@@ -125,9 +125,15 @@ exports.applyPostDefaults = applyPostDefaults;
|
|
|
125
125
|
*/
|
|
126
126
|
const INTERNAL_KEYS = ['PK', 'SK', '_type'];
|
|
127
127
|
/**
|
|
128
|
-
* Removes internal DynamoDB keys from an item or array of items
|
|
129
|
-
*
|
|
130
|
-
*
|
|
128
|
+
* Removes internal DynamoDB keys from an item or array of items.
|
|
129
|
+
*
|
|
130
|
+
* Recurses into nested plain objects and arrays so an `_type` / `PK` / `SK`
|
|
131
|
+
* field embedded inside an attribute value (e.g. a denormalized snapshot
|
|
132
|
+
* of another entity) is also stripped. Class instances such as `Date` are
|
|
133
|
+
* passed through untouched — recursing into them would lose their type.
|
|
134
|
+
*
|
|
135
|
+
* @param data - Single item, array of items, or any nested value from DynamoDB
|
|
136
|
+
* @returns Data with internal keys removed at every level
|
|
131
137
|
*/
|
|
132
138
|
const stripInternalKeys = (data) => {
|
|
133
139
|
if (data === undefined || data === null) {
|
|
@@ -136,12 +142,14 @@ const stripInternalKeys = (data) => {
|
|
|
136
142
|
if (Array.isArray(data)) {
|
|
137
143
|
return data.map((item) => (0, exports.stripInternalKeys)(item));
|
|
138
144
|
}
|
|
139
|
-
|
|
145
|
+
// Only recurse into plain objects; preserve Date, Buffer, Set, Map,
|
|
146
|
+
// and anything else with a non-Object prototype.
|
|
147
|
+
if (typeof data === 'object' && Object.getPrototypeOf(data) === Object.prototype) {
|
|
140
148
|
const cleaned = {};
|
|
141
149
|
for (const [key, value] of Object.entries(data)) {
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
if (INTERNAL_KEYS.includes(key))
|
|
151
|
+
continue;
|
|
152
|
+
cleaned[key] = (0, exports.stripInternalKeys)(value);
|
|
145
153
|
}
|
|
146
154
|
return cleaned;
|
|
147
155
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ftschopp/dynatable-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
4
4
|
"description": "Core library for DynamoDB single table design",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"default": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"CHANGELOG.md"
|
|
18
|
+
],
|
|
7
19
|
"publishConfig": {
|
|
8
20
|
"access": "public"
|
|
9
21
|
},
|
|
10
22
|
"scripts": {
|
|
11
23
|
"build": "tsc && tsc-alias",
|
|
12
24
|
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf dist",
|
|
13
|
-
"
|
|
25
|
+
"lint": "eslint src",
|
|
26
|
+
"test": "jest",
|
|
27
|
+
"prepublishOnly": "yarn build"
|
|
14
28
|
},
|
|
15
29
|
"devDependencies": {
|
|
16
30
|
"@repo/eslint-config": "*",
|
|
@@ -33,5 +47,8 @@
|
|
|
33
47
|
"ramda": "^0.32.0",
|
|
34
48
|
"ulid": "^3.0.2",
|
|
35
49
|
"zod": "^4.3.5"
|
|
50
|
+
},
|
|
51
|
+
"engines": {
|
|
52
|
+
"node": ">=22"
|
|
36
53
|
}
|
|
37
54
|
}
|
package/eslint.config.mjs
DELETED
package/jest.config.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** @type {import('jest').Config} */
|
|
2
|
-
module.exports = {
|
|
3
|
-
preset: 'ts-jest',
|
|
4
|
-
testEnvironment: 'node',
|
|
5
|
-
testMatch: ['**/?(*.)+(test).[tj]s'],
|
|
6
|
-
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
|
7
|
-
roots: ['<rootDir>/src', '<rootDir>/tests'],
|
|
8
|
-
moduleNameMapper: {
|
|
9
|
-
'^@/(.*)$': '<rootDir>/src/$1',
|
|
10
|
-
},
|
|
11
|
-
};
|
package/src/builders/README.md
DELETED
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
# Builders Architecture
|
|
2
|
-
|
|
3
|
-
This directory contains the builder pattern implementation for DynamoDB operations.
|
|
4
|
-
|
|
5
|
-
## Structure
|
|
6
|
-
|
|
7
|
-
```
|
|
8
|
-
builders/
|
|
9
|
-
├── shared/ # Shared utilities and types
|
|
10
|
-
│ ├── types.ts # Base types (Condition, OpBuilder, etc.)
|
|
11
|
-
│ ├── operators.ts # Condition operators (eq, ne, lt, etc.)
|
|
12
|
-
│ └── conditions.ts # Condition expression builders
|
|
13
|
-
├── get/ # GET operation builder
|
|
14
|
-
│ ├── types.ts
|
|
15
|
-
│ ├── create-get-builder.ts
|
|
16
|
-
│ └── index.ts
|
|
17
|
-
├── put/ # PUT operation builder
|
|
18
|
-
│ ├── types.ts
|
|
19
|
-
│ ├── create-put-builder.ts
|
|
20
|
-
│ └── index.ts
|
|
21
|
-
└── query/ # QUERY operation builder
|
|
22
|
-
├── types.ts
|
|
23
|
-
├── create-query-builder.ts
|
|
24
|
-
└── index.ts
|
|
25
|
-
```
|
|
26
|
-
|
|
27
|
-
## Design Principles
|
|
28
|
-
|
|
29
|
-
1. **Modularity**: Each operation has its own directory with dedicated types and implementation
|
|
30
|
-
2. **Reusability**: Common utilities are centralized in `shared/`
|
|
31
|
-
3. **Scalability**: Easy to add new operations (query, scan, delete, etc.) by following the same pattern
|
|
32
|
-
4. **Type Safety**: Full TypeScript support with proper type inference
|
|
33
|
-
|
|
34
|
-
## Adding a New Builder
|
|
35
|
-
|
|
36
|
-
To add a new operation (e.g., `query`):
|
|
37
|
-
|
|
38
|
-
1. Create a new directory: `builders/query/`
|
|
39
|
-
2. Add three files:
|
|
40
|
-
- `types.ts` - Interface definition extending `OperationBuilder` or `ExecutableBuilder`
|
|
41
|
-
- `create-query-builder.ts` - Implementation with immutable builder pattern
|
|
42
|
-
- `index.ts` - Export all public APIs
|
|
43
|
-
3. Import shared utilities from `../shared`
|
|
44
|
-
4. Export from main `builders/index.ts`
|
|
45
|
-
|
|
46
|
-
## Usage Examples
|
|
47
|
-
|
|
48
|
-
### GET Operation
|
|
49
|
-
|
|
50
|
-
```typescript
|
|
51
|
-
import { createGetBuilder } from './builders';
|
|
52
|
-
|
|
53
|
-
const getBuilder = createGetBuilder(tableName, key, client)
|
|
54
|
-
.select(['name', 'email'])
|
|
55
|
-
.consistentRead();
|
|
56
|
-
|
|
57
|
-
const item = await getBuilder.execute();
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
### PUT Operation
|
|
61
|
-
|
|
62
|
-
```typescript
|
|
63
|
-
import { createPutBuilder } from './builders';
|
|
64
|
-
|
|
65
|
-
const putBuilder = createPutBuilder(tableName, item, client).ifNotExists().returning('ALL_NEW');
|
|
66
|
-
|
|
67
|
-
const result = await putBuilder.execute();
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
### QUERY Operation
|
|
71
|
-
|
|
72
|
-
```typescript
|
|
73
|
-
import { createQueryBuilder } from './builders';
|
|
74
|
-
|
|
75
|
-
// Basic query with partition key
|
|
76
|
-
const query1 = createQueryBuilder(tableName, client, model)
|
|
77
|
-
.where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
78
|
-
.execute();
|
|
79
|
-
|
|
80
|
-
// Query with AND filter
|
|
81
|
-
const query2 = createQueryBuilder(tableName, client, model)
|
|
82
|
-
.where((attr, op) => op.and(op.eq(attr.username, 'juanca'), op.gt(attr.likesCount, 10)))
|
|
83
|
-
.limit(10)
|
|
84
|
-
.scanIndexForward(false)
|
|
85
|
-
.select(['id', 'title', 'createdAt'])
|
|
86
|
-
.execute();
|
|
87
|
-
|
|
88
|
-
// Query with OR filter
|
|
89
|
-
const query3 = createQueryBuilder(tableName, client, model)
|
|
90
|
-
.where((attr, op) =>
|
|
91
|
-
op.and(
|
|
92
|
-
op.eq(attr.username, 'juanca'),
|
|
93
|
-
op.or(op.gt(attr.likesCount, 100), op.gt(attr.commentCount, 50))
|
|
94
|
-
)
|
|
95
|
-
)
|
|
96
|
-
.execute();
|
|
97
|
-
|
|
98
|
-
// Complex nested conditions
|
|
99
|
-
const query4 = createQueryBuilder(tableName, client, model)
|
|
100
|
-
.where((attr, op) =>
|
|
101
|
-
op.and(
|
|
102
|
-
op.eq(attr.username, 'juanca'),
|
|
103
|
-
op.or(
|
|
104
|
-
op.and(op.gt(attr.likesCount, 100), op.lt(attr.commentCount, 10)),
|
|
105
|
-
op.gt(attr.likesCount, 500)
|
|
106
|
-
)
|
|
107
|
-
)
|
|
108
|
-
)
|
|
109
|
-
.execute();
|
|
110
|
-
|
|
111
|
-
// Query using a secondary index
|
|
112
|
-
const query5 = createQueryBuilder(tableName, client, model)
|
|
113
|
-
.where((attr, op) => op.eq(attr.status, 'active'))
|
|
114
|
-
.useIndex('GSI1')
|
|
115
|
-
.execute();
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
## Type-Safe Queries
|
|
119
|
-
|
|
120
|
-
The Query Builder provides full TypeScript type safety with an intuitive API:
|
|
121
|
-
|
|
122
|
-
```typescript
|
|
123
|
-
// Your model type
|
|
124
|
-
type Photo = {
|
|
125
|
-
username: string; // Used in partition key template
|
|
126
|
-
photoId: string; // Used in sort key template
|
|
127
|
-
url: string;
|
|
128
|
-
likesCount: number;
|
|
129
|
-
commentCount: number;
|
|
130
|
-
};
|
|
131
|
-
|
|
132
|
-
// Type-safe query - TypeScript knows all fields and their types!
|
|
133
|
-
const photos = await createQueryBuilder<Photo>(tableName, client, model)
|
|
134
|
-
.where((attr, op) =>
|
|
135
|
-
op.and(
|
|
136
|
-
op.eq(attr.username, 'juanca'), // ✓ Key field - goes to KeyConditionExpression
|
|
137
|
-
op.gt(attr.likesCount, 10) // ✓ Non-key field - goes to FilterExpression
|
|
138
|
-
)
|
|
139
|
-
)
|
|
140
|
-
.select(['username', 'url', 'likesCount']) // ✓ Only Photo keys allowed
|
|
141
|
-
.execute();
|
|
142
|
-
|
|
143
|
-
// TypeScript will catch errors at compile time:
|
|
144
|
-
// .where((attr, op) => op.eq(attr.invalid, 'x')) // ✗ Property 'invalid' does not exist
|
|
145
|
-
// .where((attr, op) => op.eq(attr.username, 123)) // ✗ Argument of type 'number' not assignable
|
|
146
|
-
// .select(['invalidField']) // ✗ Type error!
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### Important: Automatic Separation of Key Conditions vs Filters
|
|
150
|
-
|
|
151
|
-
The query builder **automatically separates** your conditions:
|
|
152
|
-
|
|
153
|
-
**KeyConditionExpression** (efficient, uses indexes):
|
|
154
|
-
|
|
155
|
-
- Fields used in partition key (pk) or sort key (sk) templates
|
|
156
|
-
- Evaluated during the query at the index level
|
|
157
|
-
- Very efficient - only reads matching items
|
|
158
|
-
|
|
159
|
-
**FilterExpression** (less efficient, post-processing):
|
|
160
|
-
|
|
161
|
-
- All other fields not in key templates
|
|
162
|
-
- Applied AFTER items are retrieved
|
|
163
|
-
- OR operators automatically go here (DynamoDB requirement)
|
|
164
|
-
|
|
165
|
-
```typescript
|
|
166
|
-
// ✓ The builder handles this automatically!
|
|
167
|
-
.where((attr, op) => op.and(
|
|
168
|
-
op.eq(attr.username, 'juanca'), // → KeyConditionExpression (username is in pk template)
|
|
169
|
-
op.gt(attr.likesCount, 10) // → FilterExpression (likesCount is not a key)
|
|
170
|
-
))
|
|
171
|
-
|
|
172
|
-
// Generates:
|
|
173
|
-
// KeyConditionExpression: "#username = :username_0"
|
|
174
|
-
// FilterExpression: "#likesCount > :likesCount_1"
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
## Important: Operator Isolation
|
|
178
|
-
|
|
179
|
-
Each builder creates its own isolated instance of operators with independent counters. This prevents naming conflicts when building multiple queries concurrently or reusing builder code.
|
|
180
|
-
|
|
181
|
-
```typescript
|
|
182
|
-
// ✓ Each builder has isolated state
|
|
183
|
-
const query1 = createQueryBuilder(table, client).where((attr, op) => op.eq(attr.id, '1')); // Uses :id_0
|
|
184
|
-
|
|
185
|
-
const query2 = createQueryBuilder(table, client).where((attr, op) => op.eq(attr.id, '2')); // Also uses :id_0 (isolated counter)
|
|
186
|
-
|
|
187
|
-
// No conflicts! Each builder has its own counter starting at 0
|
|
188
|
-
```
|
|
189
|
-
|
|
190
|
-
For advanced use cases where you need to manually create operators, use `createOpBuilder()`:
|
|
191
|
-
|
|
192
|
-
```typescript
|
|
193
|
-
import { createOpBuilder } from './builders/shared';
|
|
194
|
-
|
|
195
|
-
const op = createOpBuilder(); // Creates isolated operator instance
|
|
196
|
-
const condition = op.eq(attr, value);
|
|
197
|
-
```
|
|
198
|
-
|
|
199
|
-
## Available Operators
|
|
200
|
-
|
|
201
|
-
The `op` parameter in `.where()` provides these operators:
|
|
202
|
-
|
|
203
|
-
**Comparison Operators:**
|
|
204
|
-
|
|
205
|
-
- `op.eq(attr, value)` - Equals (=)
|
|
206
|
-
- `op.ne(attr, value)` - Not equals (<>)
|
|
207
|
-
- `op.lt(attr, value)` - Less than (<)
|
|
208
|
-
- `op.lte(attr, value)` - Less than or equal (<=)
|
|
209
|
-
- `op.gt(attr, value)` - Greater than (>)
|
|
210
|
-
- `op.gte(attr, value)` - Greater than or equal (>=)
|
|
211
|
-
- `op.between(attr, low, high)` - Between two values
|
|
212
|
-
|
|
213
|
-
**String Operators:**
|
|
214
|
-
|
|
215
|
-
- `op.beginsWith(attr, prefix)` - Begins with a string prefix (for string/binary fields)
|
|
216
|
-
- `op.contains(attr, value)` - Contains a substring or value (works with strings, sets, and lists)
|
|
217
|
-
|
|
218
|
-
**Existence Operators:**
|
|
219
|
-
|
|
220
|
-
- `op.exists(attr)` - Attribute exists
|
|
221
|
-
- `op.notExists(attr)` - Attribute does not exist
|
|
222
|
-
|
|
223
|
-
**Type Checking:**
|
|
224
|
-
|
|
225
|
-
- `op.attributeType(attr, type)` - Check the attribute's type
|
|
226
|
-
- Valid types: `'S'` (String), `'N'` (Number), `'B'` (Binary), `'SS'` (String Set), `'NS'` (Number Set), `'BS'` (Binary Set), `'M'` (Map), `'L'` (List), `'NULL'`, `'BOOL'`
|
|
227
|
-
|
|
228
|
-
**Advanced Operators:**
|
|
229
|
-
|
|
230
|
-
- `op.in(attr, values[])` - Attribute value is in the provided array
|
|
231
|
-
- `op.size(attr)` - Get the size of an attribute (string length, number of elements in set/list, etc.)
|
|
232
|
-
- Returns a `SizeRef` object with comparison methods:
|
|
233
|
-
- `.eq(n)` - Size equals n
|
|
234
|
-
- `.ne(n)` - Size not equals n
|
|
235
|
-
- `.lt(n)` - Size less than n
|
|
236
|
-
- `.lte(n)` - Size less than or equal to n
|
|
237
|
-
- `.gt(n)` - Size greater than n
|
|
238
|
-
- `.gte(n)` - Size greater than or equal to n
|
|
239
|
-
|
|
240
|
-
**Logical Operators:**
|
|
241
|
-
|
|
242
|
-
- `op.and(...conditions)` - Combines conditions with AND
|
|
243
|
-
- `op.or(...conditions)` - Combines conditions with OR
|
|
244
|
-
- `op.not(condition)` - Negates a condition
|
|
245
|
-
|
|
246
|
-
### Example Usage
|
|
247
|
-
|
|
248
|
-
```typescript
|
|
249
|
-
// Basic equality
|
|
250
|
-
.where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
251
|
-
|
|
252
|
-
// Comparison operators
|
|
253
|
-
.where((attr, op) => op.and(
|
|
254
|
-
op.eq(attr.username, 'juanca'),
|
|
255
|
-
op.gt(attr.age, 18),
|
|
256
|
-
op.lt(attr.score, 100)
|
|
257
|
-
))
|
|
258
|
-
|
|
259
|
-
// String operations
|
|
260
|
-
.where((attr, op) => op.beginsWith(attr.email, 'admin@'))
|
|
261
|
-
|
|
262
|
-
// Between operator
|
|
263
|
-
.where((attr, op) => op.between(attr.createdAt, '2024-01-01', '2024-12-31'))
|
|
264
|
-
|
|
265
|
-
// OR conditions
|
|
266
|
-
.where((attr, op) => op.or(
|
|
267
|
-
op.eq(attr.status, 'active'),
|
|
268
|
-
op.eq(attr.status, 'pending')
|
|
269
|
-
))
|
|
270
|
-
|
|
271
|
-
// Complex nested conditions
|
|
272
|
-
.where((attr, op) => op.and(
|
|
273
|
-
op.eq(attr.org, 'acme'),
|
|
274
|
-
op.or(
|
|
275
|
-
op.and(
|
|
276
|
-
op.gt(attr.score, 80),
|
|
277
|
-
op.eq(attr.verified, true)
|
|
278
|
-
),
|
|
279
|
-
op.eq(attr.role, 'admin')
|
|
280
|
-
)
|
|
281
|
-
))
|
|
282
|
-
|
|
283
|
-
// NOT operator
|
|
284
|
-
.where((attr, op) => op.not(op.eq(attr.status, 'deleted')))
|
|
285
|
-
|
|
286
|
-
// Existence operators
|
|
287
|
-
.where((attr, op) => op.and(
|
|
288
|
-
op.eq(attr.username, 'alice'),
|
|
289
|
-
op.exists(attr.email) // Has email field
|
|
290
|
-
))
|
|
291
|
-
|
|
292
|
-
.where((attr, op) => op.notExists(attr.deletedAt)) // Not deleted
|
|
293
|
-
|
|
294
|
-
// Contains operator (for strings, sets, lists)
|
|
295
|
-
.where((attr, op) => op.and(
|
|
296
|
-
op.eq(attr.username, 'alice'),
|
|
297
|
-
op.contains(attr.bio, 'developer') // Bio contains "developer"
|
|
298
|
-
))
|
|
299
|
-
|
|
300
|
-
.where((attr, op) => op.contains(attr.tags, 'premium')) // Has "premium" in tags set/list
|
|
301
|
-
|
|
302
|
-
// IN operator
|
|
303
|
-
.where((attr, op) => op.in(attr.status, ['active', 'pending', 'verified']))
|
|
304
|
-
|
|
305
|
-
// Size operator
|
|
306
|
-
.where((attr, op) => op.size(attr.tags).gte(3)) // At least 3 tags
|
|
307
|
-
.where((attr, op) => op.size(attr.username).lt(20)) // Username shorter than 20 chars
|
|
308
|
-
.where((attr, op) => op.size(attr.comments).eq(0)) // No comments
|
|
309
|
-
|
|
310
|
-
// Attribute type checking
|
|
311
|
-
.where((attr, op) => op.attributeType(attr.metadata, 'M')) // Is a Map
|
|
312
|
-
.where((attr, op) => op.attributeType(attr.items, 'L')) // Is a List
|
|
313
|
-
|
|
314
|
-
// Complex example with new operators
|
|
315
|
-
await table.entities.User.query()
|
|
316
|
-
.where((attr, op) => op.and(
|
|
317
|
-
op.eq(attr.status, 'active'),
|
|
318
|
-
op.exists(attr.email),
|
|
319
|
-
op.size(attr.followers).gte(10),
|
|
320
|
-
op.or(
|
|
321
|
-
op.contains(attr.tags, 'premium'),
|
|
322
|
-
op.contains(attr.tags, 'verified')
|
|
323
|
-
)
|
|
324
|
-
))
|
|
325
|
-
.execute();
|
|
326
|
-
|
|
327
|
-
// Complete example with all features
|
|
328
|
-
await table.entities.Photo.query()
|
|
329
|
-
.where((attr, op) => op.and(
|
|
330
|
-
op.eq(attr.username, 'juanca'), // Key field
|
|
331
|
-
op.or(
|
|
332
|
-
op.gt(attr.likesCount, 100), // Filter field
|
|
333
|
-
op.gt(attr.commentCount, 50) // Filter field
|
|
334
|
-
)
|
|
335
|
-
))
|
|
336
|
-
.limit(50)
|
|
337
|
-
.scanIndexForward(false)
|
|
338
|
-
.execute();
|
|
339
|
-
```
|