effect-dynamodb 0.1.0 → 1.1.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.
Files changed (66) hide show
  1. package/dist/DynamoClient.d.ts +3 -3
  2. package/dist/DynamoClient.d.ts.map +1 -1
  3. package/dist/DynamoClient.js +1 -3
  4. package/dist/DynamoClient.js.map +1 -1
  5. package/dist/Entity.d.ts +69 -10
  6. package/dist/Entity.d.ts.map +1 -1
  7. package/dist/Entity.js +383 -2
  8. package/dist/Entity.js.map +1 -1
  9. package/dist/KeyComposer.d.ts +14 -0
  10. package/dist/KeyComposer.d.ts.map +1 -1
  11. package/dist/KeyComposer.js +22 -1
  12. package/dist/KeyComposer.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/internal/EntityConfig.d.ts +33 -0
  17. package/dist/internal/EntityConfig.d.ts.map +1 -1
  18. package/dist/internal/EntitySchemas.d.ts +14 -3
  19. package/dist/internal/EntitySchemas.d.ts.map +1 -1
  20. package/dist/internal/EntitySchemas.js +41 -2
  21. package/dist/internal/EntitySchemas.js.map +1 -1
  22. package/dist/internal/EntityTypes.d.ts +35 -3
  23. package/dist/internal/EntityTypes.d.ts.map +1 -1
  24. package/dist/internal/TransactableOps.d.ts +1 -1
  25. package/dist/internal/TransactableOps.d.ts.map +1 -1
  26. package/package.json +1 -1
  27. package/dist/_AggregateCursor.d.ts +0 -44
  28. package/dist/_AggregateCursor.d.ts.map +0 -1
  29. package/dist/_AggregateCursor.js +0 -18
  30. package/dist/_AggregateCursor.js.map +0 -1
  31. package/dist/_AggregateEdges.d.ts +0 -77
  32. package/dist/_AggregateEdges.d.ts.map +0 -1
  33. package/dist/_AggregateEdges.js +0 -41
  34. package/dist/_AggregateEdges.js.map +0 -1
  35. package/dist/_AggregateSchemas.d.ts +0 -69
  36. package/dist/_AggregateSchemas.d.ts.map +0 -1
  37. package/dist/_AggregateSchemas.js +0 -194
  38. package/dist/_AggregateSchemas.js.map +0 -1
  39. package/dist/_AggregateTypes.d.ts +0 -94
  40. package/dist/_AggregateTypes.d.ts.map +0 -1
  41. package/dist/_AggregateTypes.js +0 -7
  42. package/dist/_AggregateTypes.js.map +0 -1
  43. package/dist/_EntityCombinators.d.ts +0 -155
  44. package/dist/_EntityCombinators.d.ts.map +0 -1
  45. package/dist/_EntityCombinators.js +0 -158
  46. package/dist/_EntityCombinators.js.map +0 -1
  47. package/dist/_EntityConfig.d.ts +0 -100
  48. package/dist/_EntityConfig.d.ts.map +0 -1
  49. package/dist/_EntityConfig.js +0 -7
  50. package/dist/_EntityConfig.js.map +0 -1
  51. package/dist/_EntityOps.d.ts +0 -205
  52. package/dist/_EntityOps.d.ts.map +0 -1
  53. package/dist/_EntityOps.js +0 -137
  54. package/dist/_EntityOps.js.map +0 -1
  55. package/dist/_EntitySchemas.d.ts +0 -57
  56. package/dist/_EntitySchemas.d.ts.map +0 -1
  57. package/dist/_EntitySchemas.js +0 -298
  58. package/dist/_EntitySchemas.js.map +0 -1
  59. package/dist/_EntityTypes.d.ts +0 -81
  60. package/dist/_EntityTypes.d.ts.map +0 -1
  61. package/dist/_EntityTypes.js +0 -7
  62. package/dist/_EntityTypes.js.map +0 -1
  63. package/dist/_sparse_check.d.ts +0 -2
  64. package/dist/_sparse_check.d.ts.map +0 -1
  65. package/dist/_sparse_check.js +0 -193
  66. package/dist/_sparse_check.js.map +0 -1
package/dist/Entity.js CHANGED
@@ -100,7 +100,7 @@ const makeImpl = (config) => {
100
100
  const isSchemaClass = typeof rawModel === "function";
101
101
  const modelFields = getFields(rawModel);
102
102
  const hasHiddenFields = Object.values(modelFields).some(isHidden);
103
- const systemFields = resolveSystemFields(config.timestamps, config.versioned);
103
+ const systemFields = resolveSystemFields(config.timestamps, config.versioned, config.timeSeries);
104
104
  // ---------------------------------------------------------------------------
105
105
  // Validate indexes
106
106
  // ---------------------------------------------------------------------------
@@ -123,6 +123,82 @@ const makeImpl = (config) => {
123
123
  }
124
124
  }
125
125
  }
126
+ // ---------------------------------------------------------------------------
127
+ // Validate timeSeries config (EDD-9010..9016)
128
+ // ---------------------------------------------------------------------------
129
+ if (config.timeSeries !== undefined && config.timeSeries !== null) {
130
+ const ts = config.timeSeries;
131
+ const orderBy = ts.orderBy;
132
+ // EDD-9010: orderBy must name a model field
133
+ if (!(orderBy in modelFields)) {
134
+ throw new Error(`[EDD-9010] Entity "${config.entityType}": timeSeries.orderBy "${orderBy}" does not name a model field. ` +
135
+ `Valid model fields: ${Object.keys(modelFields).sort().join(", ")}`);
136
+ }
137
+ // EDD-9011: orderBy must not be a primary-key composite (PK or SK)
138
+ const primary = config.indexes.primary;
139
+ const pkSkComposites = new Set([...primary.pk.composite, ...primary.sk.composite]);
140
+ if (pkSkComposites.has(orderBy)) {
141
+ throw new Error(`[EDD-9011] Entity "${config.entityType}": timeSeries.orderBy "${orderBy}" must not appear ` +
142
+ `in the primary key pk or sk composite — it shadows the #e# event-SK infix.`);
143
+ }
144
+ // EDD-9012: mutually exclusive with versioned
145
+ if (config.versioned !== undefined && config.versioned !== null && config.versioned !== false) {
146
+ throw new Error(`[EDD-9012] Entity "${config.entityType}": timeSeries and versioned are mutually exclusive. ` +
147
+ `Pick one consistency model per entity.`);
148
+ }
149
+ // EDD-9015: mutually exclusive with softDelete
150
+ if (config.softDelete !== undefined &&
151
+ config.softDelete !== null &&
152
+ config.softDelete !== false) {
153
+ throw new Error(`[EDD-9015] Entity "${config.entityType}": timeSeries and softDelete are mutually exclusive. ` +
154
+ `Append on a soft-deleted item would land on a new empty row — not a sound resurrection model.`);
155
+ }
156
+ // EDD-9016: appendInput is required
157
+ if (ts.appendInput === undefined || ts.appendInput === null) {
158
+ throw new Error(`[EDD-9016] Entity "${config.entityType}": timeSeries.appendInput is required. ` +
159
+ `Define a Schema.Struct whose fields are the subset of the model allowed in .append() input. ` +
160
+ `Fields outside appendInput are preserved on the current item — this is the enrichment-preservation guarantee. ` +
161
+ `To opt out (dangerous), pass the full model schema explicitly.`);
162
+ }
163
+ const appendInputFields = (() => {
164
+ const ai = ts.appendInput;
165
+ if ("fields" in ai && typeof ai.fields === "object") {
166
+ return Object.keys(ai.fields);
167
+ }
168
+ throw new Error(`[EDD-9016] Entity "${config.entityType}": timeSeries.appendInput must be a Schema.Struct or Schema.Class (.fields required).`);
169
+ })();
170
+ const appendInputFieldSet = new Set(appendInputFields);
171
+ // EDD-9013: appendInput must include orderBy + all PK/SK composites
172
+ if (!appendInputFieldSet.has(orderBy)) {
173
+ throw new Error(`[EDD-9013] Entity "${config.entityType}": timeSeries.appendInput must include orderBy "${orderBy}". ` +
174
+ `Without it .append() cannot evaluate the CAS condition.`);
175
+ }
176
+ for (const composite of pkSkComposites) {
177
+ if (!appendInputFieldSet.has(composite)) {
178
+ throw new Error(`[EDD-9013] Entity "${config.entityType}": timeSeries.appendInput missing primary-key composite "${composite}". ` +
179
+ `Every PK/SK composite must appear in appendInput so the event can be addressed.`);
180
+ }
181
+ }
182
+ // EDD-9014: orderBy must not name a ref field or ref-derived ${name}Id field
183
+ for (const fieldName of Object.keys(modelFields)) {
184
+ if (isRefField(fieldName, config.model)) {
185
+ if (orderBy === fieldName || orderBy === `${fieldName}Id`) {
186
+ throw new Error(`[EDD-9014] Entity "${config.entityType}": timeSeries.orderBy "${orderBy}" names a ref ` +
187
+ `or ref-derived id field. Refs are create-time denormalisations and cannot serve as the event clock.`);
188
+ }
189
+ }
190
+ }
191
+ // Also: appendInput must not declare any ref-derived ${name}Id field —
192
+ // ref changes go through .update(), not .append() (§4.6).
193
+ for (const fieldName of Object.keys(modelFields)) {
194
+ if (isRefField(fieldName, config.model)) {
195
+ if (appendInputFieldSet.has(`${fieldName}Id`)) {
196
+ throw new Error(`[EDD-9014] Entity "${config.entityType}": timeSeries.appendInput must not include ref-derived ` +
197
+ `"${fieldName}Id" — refs cannot be reassigned via .append(). Use .update() to change a ref.`);
198
+ }
199
+ }
200
+ }
201
+ }
126
202
  const resolvedRefs = config.refs
127
203
  ? Object.entries(config.refs).map(([fieldName, refValue]) => {
128
204
  // Extract entity from shorthand or expanded form
@@ -182,7 +258,7 @@ const makeImpl = (config) => {
182
258
  }
183
259
  // Resolve identifier field name from model annotation
184
260
  const resolvedIdentifier = getIdentifierField(config.model)?.name;
185
- const schemas = buildDerivedSchemas(modelFields, allIndexes, systemFields, resolvedRefs, immutableFields, resolvedIdentifier);
261
+ const schemas = buildDerivedSchemas(modelFields, allIndexes, systemFields, resolvedRefs, immutableFields, resolvedIdentifier, config.timeSeries);
186
262
  // schema and tableTag are injected via _configure() when the entity is registered
187
263
  // on a Table and bound through DynamoClient.make(). They are captured by operation
188
264
  // closures and resolved at runtime (inside Effects), not at definition time.
@@ -1727,6 +1803,261 @@ const makeImpl = (config) => {
1727
1803
  return yield* decodeAs(raw, result.Attributes, mode);
1728
1804
  }), self, input);
1729
1805
  // ---------------------------------------------------------------------------
1806
+ // append operation — time-series primitive (only when `timeSeries` configured)
1807
+ //
1808
+ // Two-item `TransactWriteItems`:
1809
+ // - UpdateItem on current: scoped SET (appendInput fields only) + CAS on
1810
+ // `orderBy`, with GSI keys recomposed when any appendInput field is a
1811
+ // GSI composite. `createdAt` uses if_not_exists on first append.
1812
+ // - Put of event: full decoded input + __edd_e__ + _ttl (if configured),
1813
+ // GSI keys stripped, SK replaced with `<currentSk>#e#<orderByValue>`.
1814
+ //
1815
+ // On TransactionCancelled or ConditionalCheckFailed, we issue a follow-up
1816
+ // GetItem on the primary key and return `{ applied: false, reason: "stale",
1817
+ // current }`. On success, same follow-up GetItem yields the post-append
1818
+ // current. See `docs/designs/timeseries.md` §4.
1819
+ // ---------------------------------------------------------------------------
1820
+ const timeSeriesConfig = config.timeSeries;
1821
+ const append = (input, userCondition) => Effect.gen(function* () {
1822
+ if (!timeSeriesConfig) {
1823
+ return yield* new ValidationError({
1824
+ entityType,
1825
+ operation: "append",
1826
+ cause: "Entity is not configured with timeSeries. .append() requires timeSeries config.",
1827
+ });
1828
+ }
1829
+ const client = yield* DynamoClient;
1830
+ const { name: tableName } = yield* tableTag;
1831
+ const orderByField = timeSeriesConfig.orderBy;
1832
+ const ttlDuration = timeSeriesConfig.ttl;
1833
+ const appendInputSchema = schemas.appendInputSchema;
1834
+ // Decode input via appendInputSchema
1835
+ const decodedInput = yield* Schema.decodeUnknownEffect(appendInputSchema)(input).pipe(Effect.mapError((cause) => new ValidationError({
1836
+ entityType,
1837
+ operation: "append.decode",
1838
+ cause,
1839
+ })));
1840
+ const decoded = decodedInput;
1841
+ // Compose current-item primary key (pk + sk derived from PK/SK composites in decoded)
1842
+ const primary = config.indexes.primary;
1843
+ const pkValue = KeyComposer.composePk(schema, entityType, primary, decoded);
1844
+ const currentSk = KeyComposer.composeSk(schema, entityType, entityVersion, primary, decoded);
1845
+ const marshalledKey = toAttributeMap({
1846
+ [primary.pk.field]: pkValue,
1847
+ [primary.sk.field]: currentSk,
1848
+ });
1849
+ // Prepare domain-serialised values for the item build.
1850
+ // Build a shallow copy and apply date serialization (per fieldEncodings)
1851
+ // before marshalling. This mirrors the put path.
1852
+ const serialisedInput = { ...decoded };
1853
+ serializeDateFields(serialisedInput);
1854
+ // ---------- Build UpdateItem (scoped SET + CAS) ----------
1855
+ const setClauses = [];
1856
+ const names = {};
1857
+ const values = {};
1858
+ let counter = 0;
1859
+ // Only fields named in appendInput (the serialisedInput object). This is
1860
+ // the enrichment-preservation contract — fields outside appendInput are
1861
+ // never touched. PK composites ARE included: they're stored as regular
1862
+ // attributes on the item (mirrors `.put()`), and though their values
1863
+ // never change, writing them on every append makes the first append
1864
+ // (where the row doesn't yet exist) materialise the row correctly.
1865
+ const pkCompositeSet = new Set(primaryKeyComposites(config.indexes));
1866
+ for (const [attr, val] of Object.entries(serialisedInput)) {
1867
+ if (val === undefined)
1868
+ continue;
1869
+ const nameKey = `#a${counter}`;
1870
+ const valKey = `:a${counter}`;
1871
+ names[nameKey] = resolveDbName(attr);
1872
+ values[valKey] = toAttributeValue(val);
1873
+ setClauses.push(`${nameKey} = ${valKey}`);
1874
+ counter++;
1875
+ }
1876
+ // GSI key recomposition: any GSI whose composites are touched by input.
1877
+ // Reuses the same machinery as `.update()`. PK composites are excluded
1878
+ // from the update payload: they're already in the key and never change
1879
+ // during an append. Non-PK composites that overlap between appendInput
1880
+ // and a GSI trigger recomposition, which requires the full GSI composite
1881
+ // set (partial presence raises `PartialGsiCompositeError`).
1882
+ const nonPkAppendFields = {};
1883
+ for (const [attr, val] of Object.entries(decoded)) {
1884
+ if (!pkCompositeSet.has(attr)) {
1885
+ nonPkAppendFields[attr] = val;
1886
+ }
1887
+ }
1888
+ let gsiKeys;
1889
+ try {
1890
+ gsiKeys = KeyComposer.composeGsiKeysForUpdate(schema, entityType, entityVersion, allIndexes, nonPkAppendFields, decoded);
1891
+ }
1892
+ catch (e) {
1893
+ return yield* new ValidationError({
1894
+ entityType,
1895
+ operation: "append.gsiComposites",
1896
+ cause: e instanceof Error ? e.message : e,
1897
+ });
1898
+ }
1899
+ for (const [field, value] of Object.entries(gsiKeys)) {
1900
+ const nameKey = `#a${counter}`;
1901
+ const valKey = `:a${counter}`;
1902
+ names[nameKey] = field;
1903
+ values[valKey] = toAttributeValue(value);
1904
+ setClauses.push(`${nameKey} = ${valKey}`);
1905
+ counter++;
1906
+ }
1907
+ // Entity type discriminator — idempotent; ensures existing items
1908
+ // without __edd_e__ still get tagged on first append.
1909
+ {
1910
+ const nameKey = `#a${counter}`;
1911
+ const valKey = `:a${counter}`;
1912
+ names[nameKey] = "__edd_e__";
1913
+ values[valKey] = toAttributeValue(entityType);
1914
+ setClauses.push(`${nameKey} = ${valKey}`);
1915
+ counter++;
1916
+ }
1917
+ // createdAt (if configured) — if_not_exists so subsequent appends leave it alone
1918
+ if (systemFields.createdAt) {
1919
+ const nameKey = `#a${counter}`;
1920
+ const valKey = `:a${counter}`;
1921
+ names[nameKey] = systemFields.createdAt;
1922
+ values[valKey] = toAttributeValue(generateTimestamp(systemFields.createdAtEncoding));
1923
+ setClauses.push(`${nameKey} = if_not_exists(${nameKey}, ${valKey})`);
1924
+ counter++;
1925
+ }
1926
+ // CAS condition: attribute_not_exists(#pk) OR #ob < :newOb
1927
+ names["#_tspk"] = primary.pk.field;
1928
+ names["#_tsob"] = resolveDbName(orderByField);
1929
+ // The comparison uses the stored domain-value representation — for
1930
+ // DateTime.Utc this is ISO (lexicographic == chronological), for numbers
1931
+ // it's numeric, for strings it's lexicographic. Matches how the current
1932
+ // item stores `orderByField`.
1933
+ const newObValue = serialisedInput[orderByField];
1934
+ values[":_tsNewOb"] = toAttributeValue(newObValue);
1935
+ const casCondition = "attribute_not_exists(#_tspk) OR #_tsob < :_tsNewOb";
1936
+ let finalCondition = casCondition;
1937
+ if (userCondition !== undefined) {
1938
+ const uc = compileCondition(userCondition, resolveDbName);
1939
+ finalCondition = `(${casCondition}) AND (${uc.expression})`;
1940
+ Object.assign(names, uc.names);
1941
+ Object.assign(values, uc.values);
1942
+ }
1943
+ const updateExpression = `SET ${setClauses.join(", ")}`;
1944
+ // ---------- Build Put of event item ----------
1945
+ const eventSk = KeyComposer.composeEventSk(currentSk, newObValue, schema.casing);
1946
+ const eventItem = { ...decoded };
1947
+ // Serialise date fields to storage format for the Put
1948
+ serializeDateFields(eventItem);
1949
+ // Rename domain → DB names for the Put (matches put path)
1950
+ renameToDynamo(eventItem);
1951
+ // pk + sk (event)
1952
+ eventItem[primary.pk.field] = pkValue;
1953
+ eventItem[primary.sk.field] = eventSk;
1954
+ // __edd_e__
1955
+ eventItem.__edd_e__ = entityType;
1956
+ // _ttl
1957
+ if (ttlDuration) {
1958
+ eventItem._ttl = Math.floor(Date.now() / 1000) + Duration.toSeconds(ttlDuration);
1959
+ }
1960
+ // Events never participate in indexes: strip any GSI key fields that the
1961
+ // naive spread above may have carried over. gsiKeys weren't written into
1962
+ // eventItem (only decoded fields are), but defensively clear them.
1963
+ for (const field of gsiKeyFields()) {
1964
+ delete eventItem[field];
1965
+ }
1966
+ const marshalledEventItem = toAttributeMap(eventItem);
1967
+ // ---------- Execute TransactWriteItems ----------
1968
+ const transactResult = yield* client
1969
+ .transactWriteItems({
1970
+ TransactItems: [
1971
+ {
1972
+ Update: {
1973
+ TableName: tableName,
1974
+ Key: marshalledKey,
1975
+ UpdateExpression: updateExpression,
1976
+ ConditionExpression: finalCondition,
1977
+ ExpressionAttributeNames: names,
1978
+ ExpressionAttributeValues: values,
1979
+ },
1980
+ },
1981
+ {
1982
+ Put: {
1983
+ TableName: tableName,
1984
+ Item: marshalledEventItem,
1985
+ },
1986
+ },
1987
+ ],
1988
+ })
1989
+ .pipe(Effect.matchEffect({
1990
+ onSuccess: () => Effect.succeed({ stale: false }),
1991
+ onFailure: (err) => {
1992
+ // TransactionCancelled or ConditionalCheckFailed at the transaction
1993
+ // level both map to "stale" (can't distinguish CAS from user-cond
1994
+ // in v1 — see §12 decision 5).
1995
+ if (isAwsTransactionCancelled(err.cause) || isAwsConditionalCheckFailed(err.cause)) {
1996
+ return Effect.succeed({ stale: true });
1997
+ }
1998
+ return Effect.fail(err);
1999
+ },
2000
+ }));
2001
+ // Follow-up GetItem to read the current (post-append on success, winning
2002
+ // state on stale). One read is required either way — the contract
2003
+ // returns `Model` which demands the full row including enrichment.
2004
+ const followUp = yield* client.getItem({
2005
+ TableName: tableName,
2006
+ Key: marshalledKey,
2007
+ });
2008
+ if (!followUp.Item) {
2009
+ // TTL race or out-of-band delete — surface as a DynamoClientError
2010
+ // (the previous state vanished under us). This is NOT a stale result.
2011
+ return yield* Effect.fail(new ValidationError({
2012
+ entityType,
2013
+ operation: "append.followUp",
2014
+ cause: "Current item not found after append — possible TTL race or concurrent delete.",
2015
+ }));
2016
+ }
2017
+ const rawCurrent = fromAttributeMap(followUp.Item);
2018
+ const currentModel = yield* decodeAs(rawCurrent, followUp.Item, "model");
2019
+ if (transactResult.stale) {
2020
+ return {
2021
+ applied: false,
2022
+ reason: "stale",
2023
+ current: currentModel,
2024
+ };
2025
+ }
2026
+ return { applied: true, current: currentModel };
2027
+ });
2028
+ // ---------------------------------------------------------------------------
2029
+ // history operation — BoundQuery for event items only
2030
+ // ---------------------------------------------------------------------------
2031
+ const history = (key) => {
2032
+ if (!timeSeriesConfig) {
2033
+ throw new Error(`[EDD-9010] Entity "${entityType}": .history() requires timeSeries config on the entity.`);
2034
+ }
2035
+ const decodedKey = Schema.decodeUnknownSync(schemas.keySchema)(key);
2036
+ const primary = config.indexes.primary;
2037
+ const pkValue = KeyComposer.composePk(schema, entityType, primary, decodedKey);
2038
+ const currentSk = KeyComposer.composeSk(schema, entityType, entityVersion, primary, decodedKey);
2039
+ const prefix = KeyComposer.composeEventSkPrefix(currentSk, schema.casing);
2040
+ const decodeHistory = (raw) => {
2041
+ renameFromDynamo(raw);
2042
+ deserializeDateFields(raw);
2043
+ return Schema.decodeUnknownEffect(schemas.historyRecordSchema)(raw).pipe(Effect.map(attachPrototype), Effect.mapError((cause) => new ValidationError({
2044
+ entityType,
2045
+ operation: "history.decode",
2046
+ cause,
2047
+ })));
2048
+ };
2049
+ return Query.make({
2050
+ tableName: "",
2051
+ indexName: undefined,
2052
+ pkField: primary.pk.field,
2053
+ pkValue,
2054
+ skField: primary.sk.field,
2055
+ entityTypes: [entityType],
2056
+ decoder: (raw) => decodeHistory(raw),
2057
+ resolveTableName: tableTag.useSync((tc) => tc.name),
2058
+ }).pipe(Query.where({ beginsWith: prefix }));
2059
+ };
2060
+ // ---------------------------------------------------------------------------
1730
2061
  // query namespace
1731
2062
  // ---------------------------------------------------------------------------
1732
2063
  const queryNamespace = {};
@@ -2170,6 +2501,7 @@ const makeImpl = (config) => {
2170
2501
  softDelete: config.softDelete,
2171
2502
  unique: config.unique,
2172
2503
  identifier: resolvedIdentifier,
2504
+ timeSeries: config.timeSeries,
2173
2505
  _resolvedRefs: resolvedRefs,
2174
2506
  /** @internal Full decode pipeline: rename + date deser + schema decode. Used by Batch/Aggregate. */
2175
2507
  _decodeRecord: decodeRecord,
@@ -2206,6 +2538,8 @@ const makeImpl = (config) => {
2206
2538
  restore,
2207
2539
  purge,
2208
2540
  deleted: { get: deletedGet, list: deletedList },
2541
+ append,
2542
+ history,
2209
2543
  set,
2210
2544
  expectedVersion,
2211
2545
  query: queryNamespace,
@@ -2297,6 +2631,53 @@ export const bind = (entity) => Effect.gen(function* () {
2297
2631
  get: (key) => provide(entity.deleted.get(key)._run("record")),
2298
2632
  list: (key) => wrapAsBoundQuery(entity.deleted.list(key)),
2299
2633
  },
2634
+ // Time-series (no-ops when entity is not configured; runtime check in
2635
+ // the entity-level `append` returns a ValidationError).
2636
+ append: (input, condition) => provide(entity.append(input, condition)),
2637
+ history: (key) => {
2638
+ const q = entity.history(key);
2639
+ const pathBuilder = createPathBuilder();
2640
+ const conditionOps = createConditionOps();
2641
+ const ts = entity.timeSeries;
2642
+ const orderBy = ts?.orderBy;
2643
+ const entityInternals = entity;
2644
+ const schemaRef = entityInternals._schema;
2645
+ const entityTypeRef = entityInternals.entityType;
2646
+ // composeSkCondition: rewrite user's .where() values by prefixing
2647
+ // with `<currentSk>#e#` and applying serialisation + casing to the
2648
+ // user-supplied orderBy value.
2649
+ const primary = entity.indexes.primary;
2650
+ const composeSkCondition = (cond) => {
2651
+ // We need the `currentSk` + prefix. The user's `key` lets us derive
2652
+ // `currentSk` via the same primary SK composer used by `history()`.
2653
+ const currentSk = KeyComposer.composeSk(schemaRef, entityTypeRef, 1, primary, key);
2654
+ const prefix = KeyComposer.composeEventSkPrefix(currentSk, schemaRef.casing);
2655
+ const rewrite = (v) => `${prefix}${DynamoSchema.applyCasing(KeyComposer.serializeValue(v), schemaRef.casing)}`;
2656
+ if ("eq" in cond)
2657
+ return { eq: rewrite(cond.eq) };
2658
+ if ("lt" in cond)
2659
+ return { lt: rewrite(cond.lt) };
2660
+ if ("lte" in cond)
2661
+ return { lte: rewrite(cond.lte) };
2662
+ if ("gt" in cond)
2663
+ return { gt: rewrite(cond.gt) };
2664
+ if ("gte" in cond)
2665
+ return { gte: rewrite(cond.gte) };
2666
+ if ("between" in cond)
2667
+ return { between: [rewrite(cond.between[0]), rewrite(cond.between[1])] };
2668
+ if ("beginsWith" in cond)
2669
+ return { beginsWith: rewrite(cond.beginsWith) };
2670
+ return cond;
2671
+ };
2672
+ const bqConfig = {
2673
+ pathBuilder,
2674
+ conditionOps,
2675
+ provide,
2676
+ skFields: orderBy ? [orderBy] : [],
2677
+ composeSkCondition,
2678
+ };
2679
+ return new BoundQueryImpl(q, bqConfig);
2680
+ },
2300
2681
  // Query execution
2301
2682
  paginate: (q, ...combinators) => {
2302
2683
  const final = applyQuery(q, combinators);