dyna-record 0.5.3 → 0.6.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Dyna-Record
2
2
 
3
- [API Documentation](https://dyna-record.com/)
3
+ [API Documentation](https://docs.dyna-record.com/)
4
4
 
5
5
  [Medium Article](https://medium.com/@drewdavis888/unlock-relational-data-modeling-in-dynamodb-with-dyna-record-5b9cce27c3ce)
6
6
 
@@ -21,6 +21,7 @@ Note: ACID compliant according to DynamoDB [limitations](https://docs.aws.amazon
21
21
  - [FindById](#findbyid)
22
22
  - [Query](#query)
23
23
  - [Filtering on Object Attributes](#filtering-on-object-attributes)
24
+ - [Typed Query Filters](#typed-query-filters)
24
25
  - [Update](#update)
25
26
  - [Updating Object Attributes](#updating-object-attributes)
26
27
  - [Delete](#delete)
@@ -50,9 +51,9 @@ Entities in Dyna-Record represent your DynamoDB table structure and relationship
50
51
 
51
52
  ### Table
52
53
 
53
- [Docs](https://dyna-record.com/functions/Table.html)
54
+ [Docs](https://docs.dyna-record.com/functions/Table.html)
54
55
 
55
- Create a table class that extends [DynaRecord base class](https://dyna-record.com/classes/default.html) and is decorated with the [Table decorator](https://dyna-record.com/functions/Table.html). At a minimum, the table class must define the [PartitionKeyAttribute](https://dyna-record.com/functions/PartitionKeyAttribute.html) and [SortKeyAttribute](https://dyna-record.com/functions/SortKeyAttribute.html).
56
+ Create a table class that extends [DynaRecord base class](https://docs.dyna-record.com/classes/default.html) and is decorated with the [Table decorator](https://docs.dyna-record.com/functions/Table.html). At a minimum, the table class must define the [PartitionKeyAttribute](https://docs.dyna-record.com/functions/PartitionKeyAttribute.html) and [SortKeyAttribute](https://docs.dyna-record.com/functions/SortKeyAttribute.html).
56
57
 
57
58
  #### Basic usage
58
59
 
@@ -107,56 +108,64 @@ abstract class MyTable extends DynaRecord {
107
108
 
108
109
  ### Entity
109
110
 
110
- [Docs](https://dyna-record.com/functions/Entity.html)
111
+ [Docs](https://docs.dyna-record.com/functions/Entity.html)
111
112
 
112
113
  Each entity must extend the Table class. To support single table design patterns, they must extend the same tables class.
113
114
 
114
- By default, each entity will have [default attributes](https://dyna-record.com/types/_internal_.DefaultFields.html)
115
+ Each entity **must** declare its `type` property as a string literal matching the class name. This enables compile-time type safety for query filters and return types. Omitting this declaration will produce a compile error at the `@Entity` decorator.
116
+
117
+ By default, each entity will have [default attributes](https://docs.dyna-record.com/types/_internal_.DefaultFields.html)
115
118
 
116
119
  - The partition key defined on the [table](#table) class
117
120
  - The sort key defined on the [table](#table) class
118
- - [id](https://dyna-record.com/classes/default.html#id) - The id for the model. This will be an autogenerated uuid unless [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) is set on a non-nullable entity attribute.
119
- - [type](https://dyna-record.com/classes/default.html#type) - The type of the entity. Value is the entity class name
120
- - [createdAt](https://dyna-record.com/classes/default.html#updatedAt) - The timestamp of when the entity was created
121
- - [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) - Timestamp of when the entity was updated last
121
+ - [id](https://docs.dyna-record.com/classes/default.html#id) - The id for the model. This will be an autogenerated uuid unless [IdAttribute](<(https://docs.dyna-record.com/functions/IdAttribute.html)>) is set on a non-nullable entity attribute.
122
+ - [type](https://docs.dyna-record.com/classes/default.html#type) - The type of the entity. Value is the entity class name. Must be declared as a string literal via `declare readonly type: "ClassName"`.
123
+ - [createdAt](https://docs.dyna-record.com/classes/default.html#createdAt) - The timestamp of when the entity was created
124
+ - [updatedAt](https://docs.dyna-record.com/classes/default.html#updatedAt) - Timestamp of when the entity was updated last
122
125
 
123
126
  ```typescript
124
127
  import { Entity } from "dyna-record";
125
128
 
126
129
  @Entity
127
130
  class Student extends MyTable {
131
+ declare readonly type: "Student";
128
132
  // ...
129
133
  }
130
134
 
131
135
  @Entity
132
136
  class Course extends MyTable {
133
- /// ...
137
+ declare readonly type: "Course";
138
+ // ...
134
139
  }
135
140
  ```
136
141
 
142
+ > **Note:** `declare readonly type` is a pure TypeScript type annotation with zero runtime impact. The ORM sets `type` to the class name automatically. The declaration simply tells TypeScript the exact literal type, enabling typed query filters and return type narrowing.
143
+
137
144
  ### Attributes
138
145
 
139
146
  Use the attribute decorators below to define attributes on a model. The decorator maps class properties to DynamoDB table attributes.
140
147
 
141
148
  - Attribute decorators
142
149
 
143
- - [@StringAttribute](https://dyna-record.com/functions/StringAttribute.html)
144
- - [@NumberAttribute](https://dyna-record.com/functions/NumberAttribute.html)
145
- - [@BooleanAttribute](https://dyna-record.com/functions/BooleanAttribute.html)
146
- - [@DateAttribute](https://dyna-record.com/functions/DateAttribute.html)
147
- - [@EnumAttribute](https://dyna-record.com/functions/EnumAttribute.html)
148
- - [@IdAttribute](https://dyna-record.com/functions/IdAttribute.html)
149
- - [@ObjectAttribute](https://dyna-record.com/functions/ObjectAttribute.html)
150
+ - [@StringAttribute](https://docs.dyna-record.com/functions/StringAttribute.html)
151
+ - [@NumberAttribute](https://docs.dyna-record.com/functions/NumberAttribute.html)
152
+ - [@BooleanAttribute](https://docs.dyna-record.com/functions/BooleanAttribute.html)
153
+ - [@DateAttribute](https://docs.dyna-record.com/functions/DateAttribute.html)
154
+ - [@EnumAttribute](https://docs.dyna-record.com/functions/EnumAttribute.html)
155
+ - [@IdAttribute](https://docs.dyna-record.com/functions/IdAttribute.html)
156
+ - [@ObjectAttribute](https://docs.dyna-record.com/functions/ObjectAttribute.html)
150
157
 
151
- - The [alias](https://dyna-record.com/interfaces/AttributeOptions.html#alias) option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
158
+ - The [alias](https://docs.dyna-record.com/interfaces/AttributeOptions.html#alias) option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
152
159
  - Set nullable attributes as optional for optimal type safety
153
- - Attempting to remove a non-nullable attribute will result in a [NullConstrainViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)
160
+ - Attempting to remove a non-nullable attribute will result in a [NullConstrainViolationError](https://docs.dyna-record.com/classes/NullConstraintViolationError.html)
154
161
 
155
162
  ```typescript
156
163
  import { Entity, Attribute } from "dyna-record";
157
164
 
158
165
  @Entity
159
166
  class Student extends MyTable {
167
+ declare readonly type: "Student";
168
+
160
169
  @StringAttribute({ alias: "Username" }) // Sets alias if field in Dynamo is different then on the model
161
170
  public username: string;
162
171
 
@@ -196,6 +205,8 @@ const addressSchema = {
196
205
 
197
206
  @Entity
198
207
  class Store extends MyTable {
208
+ declare readonly type: "Store";
209
+
199
210
  @ObjectAttribute({ alias: "Address", schema: addressSchema })
200
211
  public readonly address: InferObjectSchema<typeof addressSchema>;
201
212
  }
@@ -240,11 +251,11 @@ The schema must be declared with `as const satisfies ObjectSchema` so TypeScript
240
251
 
241
252
  ### Foreign Keys
242
253
 
243
- Define foreign keys in order to support [@BelongsTo](https://dyna-record.com/functions/BelongsTo.html) relationships. A foreign key is required for [@HasOne](https://dyna-record.com/functions/HasOne.html) and [@HasMany](https://dyna-record.com/functions/HasMany.html) relationships.
254
+ Define foreign keys in order to support [@BelongsTo](https://docs.dyna-record.com/functions/BelongsTo.html) relationships. A foreign key is required for [@HasOne](https://docs.dyna-record.com/functions/HasOne.html) and [@HasMany](https://docs.dyna-record.com/functions/HasMany.html) relationships.
244
255
 
245
- - The [alias](https://dyna-record.com/interfaces/AttributeOptions.html#alias) option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
256
+ - The [alias](https://docs.dyna-record.com/interfaces/AttributeOptions.html#alias) option allows you to specify the attribute name as it appears in the DynamoDB table, different from your class property name.
246
257
  - Set nullable foreign key attributes as optional for optimal type safety
247
- - Attempting to remove an entity from a non-nullable foreign key will result in a [NullConstrainViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)
258
+ - Attempting to remove an entity from a non-nullable foreign key will result in a [NullConstrainViolationError](https://docs.dyna-record.com/classes/NullConstraintViolationError.html)
248
259
  - Always provide the referenced entity class to `@ForeignKeyAttribute` (for example `@ForeignKeyAttribute(() => Customer)`); this allows DynaRecord to enforce referential integrity even when no relationship decorator is defined.
249
260
  - `Create` and `Update` automatically add DynamoDB condition checks for standalone foreign keys (those without a relationship decorator) to ensure the referenced entity exists, enabling referential integrity even when no denormalised access pattern is required.
250
261
 
@@ -259,6 +270,8 @@ import {
259
270
 
260
271
  @Entity
261
272
  class Assignment extends MyTable {
273
+ declare readonly type: "Assignment";
274
+
262
275
  @ForeignKeyAttribute(() => Course)
263
276
  public readonly courseId: ForeignKey<Course>;
264
277
 
@@ -268,6 +281,8 @@ class Assignment extends MyTable {
268
281
 
269
282
  @Entity
270
283
  class Course extends MyTable {
284
+ declare readonly type: "Course";
285
+
271
286
  @ForeignKeyAttribute(() => Teacher, { nullable: true })
272
287
  public readonly teacherId?: NullableForeignKey<Teacher>; // Set as optional
273
288
 
@@ -278,16 +293,16 @@ class Course extends MyTable {
278
293
 
279
294
  ### Relationships
280
295
 
281
- Dyna-Record supports defining relationships between entities such as [@HasOne](https://dyna-record.com/functions/HasOne.html), [@HasMany](https://dyna-record.com/functions/HasMany.html), [@BelongsTo](https://dyna-record.com/functions/BelongsTo.html) and [@HasAndBelongsToMany](https://dyna-record.com/functions/HasAndBelongsToMany.html). It does this by de-normalizing records to each of its related entities partitions.
296
+ Dyna-Record supports defining relationships between entities such as [@HasOne](https://docs.dyna-record.com/functions/HasOne.html), [@HasMany](https://docs.dyna-record.com/functions/HasMany.html), [@BelongsTo](https://docs.dyna-record.com/functions/BelongsTo.html) and [@HasAndBelongsToMany](https://docs.dyna-record.com/functions/HasAndBelongsToMany.html). It does this by de-normalizing records to each of its related entities partitions.
282
297
 
283
- A relationship can be defined as nullable or non-nullable. Non-nullable relationships will be enforced via transactions and violations will result in [NullConstraintViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)
298
+ A relationship can be defined as nullable or non-nullable. Non-nullable relationships will be enforced via transactions and violations will result in [NullConstraintViolationError](https://docs.dyna-record.com/classes/NullConstraintViolationError.html)
284
299
 
285
- - [@ForeignKeyAttribute](https://dyna-record.com/functions/ForeignKeyAttribute.html) is used to define a foreign key that links to another entity
286
- - Relationship decorators ([@HasOne](#hasone), [@HasMany](#hasmany), [@BelongsTo](https://dyna-record.com/functions/BelongsTo.html), [@HasAndBelongsToMany](#hasandbelongstomany)) define how entities relate to each other.
300
+ - [@ForeignKeyAttribute](https://docs.dyna-record.com/functions/ForeignKeyAttribute.html) is used to define a foreign key that links to another entity
301
+ - Relationship decorators ([@HasOne](#hasone), [@HasMany](#hasmany), [@BelongsTo](https://docs.dyna-record.com/functions/BelongsTo.html), [@HasAndBelongsToMany](#hasandbelongstomany)) define how entities relate to each other.
287
302
 
288
303
  #### HasOne
289
304
 
290
- [Docs](https://dyna-record.com/functions/HasOne.html)
305
+ [Docs](https://docs.dyna-record.com/functions/HasOne.html)
291
306
 
292
307
  ```typescript
293
308
  import {
@@ -300,6 +315,8 @@ import {
300
315
 
301
316
  @Entity
302
317
  class Assignment extends MyTable {
318
+ declare readonly type: "Assignment";
319
+
303
320
  // 'assignmentId' must be defined on associated model
304
321
  @HasOne(() => Grade, { foreignKey: "assignmentId" })
305
322
  public readonly grade: Grade;
@@ -307,6 +324,8 @@ class Assignment extends MyTable {
307
324
 
308
325
  @Entity
309
326
  class Grade extends MyTable {
327
+ declare readonly type: "Grade";
328
+
310
329
  @ForeignKeyAttribute(() => Assignment)
311
330
  public readonly assignmentId: ForeignKey<Assignment>;
312
331
 
@@ -318,13 +337,15 @@ class Grade extends MyTable {
318
337
 
319
338
  ### HasMany
320
339
 
321
- [Docs](https://dyna-record.com/functions/HasMany.html)
340
+ [Docs](https://docs.dyna-record.com/functions/HasMany.html)
322
341
 
323
342
  ```typescript
324
343
  import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";
325
344
 
326
345
  @Entity
327
346
  class Teacher extends MyTable {
347
+ declare readonly type: "Teacher";
348
+
328
349
  // 'teacherId' must be defined on associated model
329
350
  @HasMany(() => Course, { foreignKey: "teacherId" })
330
351
  public readonly courses: Course[];
@@ -332,6 +353,8 @@ class Teacher extends MyTable {
332
353
 
333
354
  @Entity
334
355
  class Course extends MyTable {
356
+ declare readonly type: "Course";
357
+
335
358
  @ForeignKeyAttribute(() => Teacher, { nullable: true })
336
359
  public readonly teacherId?: NullableForeignKey<Teacher>; // Mark as optional
337
360
 
@@ -350,6 +373,8 @@ import { Entity, NullableForeignKey, BelongsTo, HasMany } from "dyna-record";
350
373
 
351
374
  @Entity
352
375
  class Teacher extends MyTable {
376
+ declare readonly type: "Teacher";
377
+
353
378
  // 'teacherId' must be defined on associated model
354
379
  @HasMany(() => Course, { foreignKey: "teacherId", uniDirectional: true })
355
380
  public readonly courses: Course[];
@@ -357,6 +382,8 @@ class Teacher extends MyTable {
357
382
 
358
383
  @Entity
359
384
  class Course extends MyTable {
385
+ declare readonly type: "Course";
386
+
360
387
  @ForeignKeyAttribute(() => Teacher, { nullable: true })
361
388
  public readonly teacherId?: NullableForeignKey<Teacher>; // Mark as optional
362
389
  }
@@ -364,9 +391,9 @@ class Course extends MyTable {
364
391
 
365
392
  ### HasAndBelongsToMany
366
393
 
367
- [Docs](https://dyna-record.com/functions/HasAndBelongsToMany.html)
394
+ [Docs](https://docs.dyna-record.com/functions/HasAndBelongsToMany.html)
368
395
 
369
- HasAndBelongsToMany relationships require a [JoinTable](https://dyna-record.com/classes/JoinTable.html) class. This represents a virtual table to support the relationship
396
+ HasAndBelongsToMany relationships require a [JoinTable](https://docs.dyna-record.com/classes/JoinTable.html) class. This represents a virtual table to support the relationship
370
397
 
371
398
  ```typescript
372
399
  import {
@@ -383,6 +410,8 @@ class StudentCourse extends JoinTable<Student, Course> {
383
410
 
384
411
  @Entity
385
412
  class Course extends MyTable {
413
+ declare readonly type: "Course";
414
+
386
415
  @HasAndBelongsToMany(() => Student, {
387
416
  targetKey: "courses",
388
417
  through: () => ({ joinTable: StudentCourse, foreignKey: "courseId" })
@@ -392,6 +421,8 @@ class Course extends MyTable {
392
421
 
393
422
  @Entity
394
423
  class Student extends OtherTable {
424
+ declare readonly type: "Student";
425
+
395
426
  @HasAndBelongsToMany(() => Course, {
396
427
  targetKey: "students",
397
428
  through: () => ({ joinTable: StudentCourse, foreignKey: "studentId" })
@@ -404,9 +435,9 @@ class Student extends OtherTable {
404
435
 
405
436
  ### Create
406
437
 
407
- [Docs](https://dyna-record.com/classes/default.html#create)
438
+ [Docs](https://docs.dyna-record.com/classes/default.html#create)
408
439
 
409
- The create method is used to insert a new record into a DynamoDB table. This method automatically handles key generation (using UUIDs or custom id field if [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) is set), timestamps for [createdAt](https://dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) fields, and the management of relationships between entities. It leverages AWS SDK's [TransactWriteCommand](https://www.google.com/search?q=aws+transact+write+command&oq=aws+transact+write+command&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQRRg7MgYIAhBFGDvSAQgzMjAzajBqN6gCALACAA&sourceid=chrome&ie=UTF-8) for transactional integrity, ensuring either complete success or rollback in case of any failure. The method handles conditional checks to ensure data integrity and consistency during creation. If a foreignKey is set on create, dyna-record will de-normalize the data required in order to support the relationship
440
+ The create method is used to insert a new record into a DynamoDB table. This method automatically handles key generation (using UUIDs or custom id field if [IdAttribute](<(https://docs.dyna-record.com/functions/IdAttribute.html)>) is set), timestamps for [createdAt](https://docs.dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://docs.dyna-record.com/classes/default.html#updatedAt) fields, and the management of relationships between entities. It leverages AWS SDK's [TransactWriteCommand](https://www.google.com/search?q=aws+transact+write+command&oq=aws+transact+write+command&gs_lcrp=EgZjaHJvbWUyBggAEEUYOTIGCAEQRRg7MgYIAhBFGDvSAQgzMjAzajBqN6gCALACAA&sourceid=chrome&ie=UTF-8) for transactional integrity, ensuring either complete success or rollback in case of any failure. The method handles conditional checks to ensure data integrity and consistency during creation. If a foreignKey is set on create, dyna-record will de-normalize the data required in order to support the relationship
410
441
 
411
442
  To use the create method, call it on the model class you wish to create a new record for. Pass the properties of the new record as an object argument to the method. Only attributes defined on the model can be configured, and will be enforced via types and runtime schema validation.
412
443
 
@@ -451,20 +482,20 @@ const grade: Grade = await Grade.create(
451
482
 
452
483
  #### Error handling
453
484
 
454
- The method is designed to throw errors under various conditions, such as transaction cancellation due to failed conditional checks. For instance, if you attempt to create a `Grade` for an `Assignment` that already has one, the method throws a [TransactionWriteFailedError](https://dyna-record.com/classes/TransactionWriteFailedError.html).
485
+ The method is designed to throw errors under various conditions, such as transaction cancellation due to failed conditional checks. For instance, if you attempt to create a `Grade` for an `Assignment` that already has one, the method throws a [TransactionWriteFailedError](https://docs.dyna-record.com/classes/TransactionWriteFailedError.html).
455
486
 
456
487
  #### Notes
457
488
 
458
- - Automatic Timestamp Management: The [createdAt](https://dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://dyna-record.com/classes/default.html#updatedAt) fields are managed automatically and reflect the time of creation and the last update, respectively.
459
- - Automatic ID Generation: Each entity created gets a unique [id](https://dyna-record.com/classes/default.html#id) generated by the uuidv4 method.
460
- - This can be customized [IdAttribute](<(https://dyna-record.com/functions/IdAttribute.html)>) to support custom id attributes
489
+ - Automatic Timestamp Management: The [createdAt](https://docs.dyna-record.com/classes/default.html#createdAt) and [updatedAt](https://docs.dyna-record.com/classes/default.html#updatedAt) fields are managed automatically and reflect the time of creation and the last update, respectively.
490
+ - Automatic ID Generation: Each entity created gets a unique [id](https://docs.dyna-record.com/classes/default.html#id) generated by the uuidv4 method.
491
+ - This can be customized [IdAttribute](<(https://docs.dyna-record.com/functions/IdAttribute.html)>) to support custom id attributes
461
492
  - Relationship Management: The ORM manages entity relationships through DynamoDB's single-table design patterns, creating and maintaining the necessary links between related entities.
462
493
  - Conditional Checks: To ensure data integrity, the create method performs various conditional checks, such as verifying the existence of entities that new records relate to.
463
494
  - Error Handling: Errors during the creation process are handled gracefully, with specific errors thrown for different failure scenarios, such as conditional check failures or transaction cancellations.
464
495
 
465
496
  ### FindById
466
497
 
467
- [Docs](https://dyna-record.com/classes/default.html#findById)
498
+ [Docs](https://docs.dyna-record.com/classes/default.html#findById)
468
499
 
469
500
  Retrieve a single record by its primary key.
470
501
 
@@ -498,7 +529,7 @@ const course = await Course.findById("123", {
498
529
 
499
530
  ### Query
500
531
 
501
- [Docs](https://dyna-record.com/classes/default.html#query)
532
+ [Docs](https://docs.dyna-record.com/classes/default.html#query)
502
533
 
503
534
  The query method is a versatile tool for querying data from DynamoDB tables using primary key conditions and various optional filters. This method enables fetching multiple items that match specific criteria, making it ideal for situations where more than one item needs to be retrieved based on attributes of the primary key (partition key and sort key).
504
535
 
@@ -528,7 +559,7 @@ const result = await Customer.query("123", {
528
559
 
529
560
  ##### Query by primary key
530
561
 
531
- To be more precise to the underlying data, you can specify the partition key and sort key directly. The keys here will be the partition and sort keys defined on the [table](#table) class.
562
+ To be more precise to the underlying data, you can specify the partition key and sort key directly. The keys here will be the partition and sort keys defined on the [table](#table) class. The `sk` value is typed to only accept valid entity names from the partition.
532
563
 
533
564
  ```typescript
534
565
  const orders = await Customer.query({
@@ -556,9 +587,8 @@ const result = await Course.query(
556
587
  updatedAt: { $beginsWith: "2023-02-15" }
557
588
  },
558
589
  {
559
- type: ["science", "math"],
560
- createdAt: { $beginsWith: "2021-09-15T" },
561
- type: "Assignment"
590
+ type: "Assignment",
591
+ createdAt: { $beginsWith: "2021-09-15T" }
562
592
  },
563
593
  {
564
594
  id: "123"
@@ -632,6 +662,163 @@ const result = await Store.query("123", {
632
662
  });
633
663
  ```
634
664
 
665
+ #### Typed Query Filters
666
+
667
+ Query filters are strongly typed based on the entities in the queried partition. A partition includes the entity itself plus all entities reachable through its declared relationships (`@HasMany`, `@HasOne`, `@BelongsTo`, `@HasAndBelongsToMany`). For example, if `Customer` has `@HasMany(() => Order)` and `@HasOne(() => ContactInformation)`, then Customer's partition entities are `Customer`, `Order`, and `ContactInformation`.
668
+
669
+ The type system validates:
670
+
671
+ - **Filter attribute keys**: Only attributes that exist on the entity or its related entities are accepted. Relationship property names, partition keys, and sort keys are excluded.
672
+ - **`type` field values**: The `type` field only accepts entity names from the partition — the entity itself and its declared relationships. Entities from other tables or unrelated entities on the same table are rejected.
673
+ - **Sort key values**: Both `skCondition` and the `sk` property in key conditions only accept entity names from the partition. This matches dyna-record's single-table design where sort key values always start with an entity class name.
674
+ - **`type` narrowing in `$or`**: Each `$or` element is independently narrowed. When an `$or` block specifies `type: "Order"`, only Order's attributes are allowed in that block.
675
+ - **Dot-path keys**: Nested `@ObjectAttribute` fields are available as typed filter keys using dot notation (e.g., `"address.city"`).
676
+
677
+ ##### Filter key validation
678
+
679
+ ```typescript
680
+ // Valid: 'name' exists on Customer, 'lastFour' on PaymentMethod
681
+ await Customer.query("123", {
682
+ filter: { name: "John", lastFour: "1234" }
683
+ });
684
+
685
+ // Error: 'nonExistent' is not an attribute on any entity in Customer's partition
686
+ await Customer.query("123", {
687
+ filter: { nonExistent: "value" } // Compile error
688
+ });
689
+
690
+ // Error: 'orders' is a relationship property, not a filterable attribute
691
+ await Customer.query("123", {
692
+ filter: { orders: "value" } // Compile error
693
+ });
694
+ ```
695
+
696
+ ##### Type field narrowing
697
+
698
+ ```typescript
699
+ // Valid entity names only
700
+ await Customer.query("123", {
701
+ filter: { type: "Order" } // OK: "Order" is in Customer's partition
702
+ });
703
+
704
+ await Customer.query("123", {
705
+ filter: { type: "NonExistent" } // Compile error
706
+ });
707
+
708
+ // Array form (IN operator) accepts valid entity names
709
+ await Customer.query("123", {
710
+ filter: { type: ["Order", "PaymentMethod"] }
711
+ });
712
+ ```
713
+
714
+ ##### $or element narrowing
715
+
716
+ Each `$or` element narrows independently based on its own `type` value:
717
+
718
+ ```typescript
719
+ await Customer.query("123", {
720
+ filter: {
721
+ $or: [
722
+ { type: "Order", orderDate: "2023" }, // OK: orderDate is on Order
723
+ { type: "PaymentMethod", lastFour: "1234" } // OK: lastFour is on PaymentMethod
724
+ ]
725
+ }
726
+ });
727
+
728
+ // Error in $or: lastFour is not an attribute on Order
729
+ await Customer.query("123", {
730
+ filter: {
731
+ $or: [
732
+ { type: "Order", lastFour: "1234" } // Compile error
733
+ ]
734
+ }
735
+ });
736
+ ```
737
+
738
+ ##### Return type narrowing
739
+
740
+ When querying a partition with no filter or sort key condition, the return type is a union of the entity itself and all its related entities:
741
+
742
+ ```typescript
743
+ // Return type: Array<EntityAttributesInstance<Customer> | EntityAttributesInstance<Order>
744
+ // | EntityAttributesInstance<PaymentMethod> | EntityAttributesInstance<ContactInformation>>
745
+ const results = await Customer.query("123");
746
+ ```
747
+
748
+ When the filter specifies a `type` value, the return type automatically narrows to only the matching entities:
749
+
750
+ ```typescript
751
+ // Return type: Array<EntityAttributesInstance<Order>>
752
+ const orders = await Customer.query("123", {
753
+ filter: { type: "Order" }
754
+ });
755
+
756
+ orders[0]?.orderDate; // OK: orderDate is accessible
757
+
758
+ // Return type: Array<EntityAttributesInstance<Order> | EntityAttributesInstance<PaymentMethod>>
759
+ const mixed = await Customer.query("123", {
760
+ filter: { type: ["Order", "PaymentMethod"] }
761
+ });
762
+ ```
763
+
764
+ ##### Sort key validation and narrowing
765
+
766
+ Sort key values are typed to only accept valid entity names from the partition, matching dyna-record's single-table design where SK values always start with an entity class name. This applies to both the `skCondition` option and the `sk` property in key conditions:
767
+
768
+ ```typescript
769
+ // Both forms validate sort key values against partition entity names
770
+
771
+ // skCondition option (string form)
772
+ await Customer.query("123", { skCondition: "Order" }); // OK
773
+ await Customer.query("123", { skCondition: "Order#123" }); // OK
774
+ await Customer.query("123", { skCondition: { $beginsWith: "Order" } }); // OK
775
+ await Customer.query("123", { skCondition: "NonExistent" }); // Compile error
776
+
777
+ // sk property in key conditions (object form)
778
+ await Customer.query({ pk: "Customer#123", sk: "Order" }); // OK
779
+ await Customer.query({ pk: "Customer#123", sk: "Order#001" }); // OK
780
+ await Customer.query({ pk: "Customer#123", sk: { $beginsWith: "Order" } }); // OK
781
+ await Customer.query({ pk: "Customer#123", sk: "NonExistent" }); // Compile error
782
+ ```
783
+
784
+ **Return type narrowing** works with `skCondition` when the value is an exact entity name or `$beginsWith` with an entity name:
785
+
786
+ ```typescript
787
+ // skCondition narrows the return type
788
+ const orders = await Customer.query("123", { skCondition: "Order" });
789
+ // orders is Array<EntityAttributesInstance<Order>>
790
+
791
+ const orders2 = await Customer.query("123", {
792
+ skCondition: { $beginsWith: "Order" }
793
+ });
794
+ // orders2 is Array<EntityAttributesInstance<Order>>
795
+
796
+ // Suffix prevents narrowing (delimiter is configurable)
797
+ const specific = await Customer.query("123", { skCondition: "Order#123" });
798
+ // specific is QueryResults<Customer> (full union)
799
+ ```
800
+
801
+ When using the object key form (`{ pk: "...", sk: "..." }`), sort key values are **validated** but the return type is **not narrowed**. Use `filter: { type: "Order" }` alongside key conditions for return type narrowing:
802
+
803
+ ```typescript
804
+ // sk is validated but does NOT narrow the return type
805
+ const results = await Customer.query({ pk: "Customer#123", sk: "Order" });
806
+ // results is QueryResults<Customer> (full union)
807
+
808
+ // Combine with filter type for return type narrowing
809
+ const orders = await Customer.query(
810
+ { pk: "Customer#123", sk: { $beginsWith: "Order" } },
811
+ { filter: { type: "Order" } }
812
+ );
813
+ // orders is Array<EntityAttributesInstance<Order>>
814
+ ```
815
+
816
+ > **Note:** Return type narrowing applies to the top-level `type` filter field, `type` values within `$or` elements, and to the `skCondition` option. When `$or` elements specify `type` values, the return type narrows to the union of those entity types. The `sk` property in key conditions validates values but does not narrow return types. Index queries (`{ indexName: "..." }`) use untyped filters.
817
+ >
818
+ > **Filter key narrowing:** When no `type` is specified, the return type automatically narrows based on which entities have the filtered attributes. For example, `filter: { orderDate: "2023" }` narrows to `Order` if only `Order` has `orderDate`. In `$or` blocks, each element narrows independently — by `type` if present, or by filter keys otherwise — and the return type is the union across all blocks.
819
+ >
820
+ > **AND intersection:** Since DynamoDB ANDs top-level filter conditions with `$or` blocks, the return type reflects this. When both top-level conditions and `$or` blocks independently narrow to specific entity sets, the return type is their intersection. If no entity satisfies both (e.g., `{ orderDate: "2023", $or: [{ lastFour: "1234" }] }` where `orderDate` is on `Order` and `lastFour` is on `PaymentMethod`), the return type is `never[]` — correctly indicating that no records can match.
821
+
635
822
  ### Querying on an index
636
823
 
637
824
  For querying based on secondary indexes, you can specify the index name in the options.
@@ -648,7 +835,7 @@ const result = await Customer.query(
648
835
 
649
836
  ### Update
650
837
 
651
- [Docs](https://dyna-record.com/classes/default.html#update)
838
+ [Docs](https://docs.dyna-record.com/classes/default.html#update)
652
839
 
653
840
  The update method enables modifications to existing items in a DynamoDB table. It supports updating simple attributes, handling nullable fields, and managing relationships between entities, including updating and removing foreign keys. Only attributes defined on the model can be updated, and will be enforced via types and runtime schema validation.
654
841
 
@@ -663,7 +850,7 @@ await Customer.update("123", {
663
850
 
664
851
  #### Removing attributes
665
852
 
666
- Note: Attempting to remove a non nullable attribute will result in a [NullConstraintViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)
853
+ Note: Attempting to remove a non nullable attribute will result in a [NullConstraintViolationError](https://docs.dyna-record.com/classes/NullConstraintViolationError.html)
667
854
 
668
855
  ```typescript
669
856
  await ContactInformation.update("123", {
@@ -686,7 +873,7 @@ await PaymentMethod.update("123", {
686
873
 
687
874
  Nullable foreign key references can be removed by setting them to null
688
875
 
689
- Note: Attempting to remove a non nullable foreign key will result in a [NullConstraintViolationError](https://dyna-record.com/classes/NullConstraintViolationError.html)
876
+ Note: Attempting to remove a non nullable foreign key will result in a [NullConstraintViolationError](https://docs.dyna-record.com/classes/NullConstraintViolationError.html)
690
877
 
691
878
  ```typescript
692
879
  await Pet.update("123", {
@@ -780,7 +967,7 @@ const updatedInstance = await paymentMethodInstance.update(
780
967
 
781
968
  ### Delete
782
969
 
783
- [Docs](https://dyna-record.com/classes/default.html#delete)
970
+ [Docs](https://docs.dyna-record.com/classes/default.html#delete)
784
971
 
785
972
  The delete method is used to remove an entity from a DynamoDB table, along with handling the deletion of associated items in relationships (like HasMany, HasOne, BelongsTo) to maintain the integrity of the database schema.
786
973
 
@@ -821,9 +1008,13 @@ If deleting an entity or its relationships fails due to database constraints or
821
1008
 
822
1009
  Dyna-Record integrates type safety into your DynamoDB interactions, reducing runtime errors and enhancing code quality.
823
1010
 
1011
+ - **Entity Type Declaration**: The `@Entity` decorator enforces that each entity declares `readonly type` as a string literal matching the class name (`declare readonly type: "MyEntity"`). This is required for compile-time query type safety.
824
1012
  - **Attribute Type Enforcement**: Ensures that the data types of attributes match their definitions in your entities.
825
1013
  - **Method Parameter Checking**: Validates method parameters against entity definitions, preventing invalid operations.
826
1014
  - **Relationship Integrity**: Automatically manages the consistency of relationships between entities, ensuring data integrity.
1015
+ - **Typed Query Filters**: Query filter keys are validated against the attributes of entities in the partition. Invalid keys, relationship property names, and non-existent attributes produce compile errors. The `type` field only accepts valid entity class names.
1016
+ - **Return Type Narrowing**: When a query filter specifies a `type` value, the return type is automatically narrowed to only the matching entity types instead of the full partition union.
1017
+ - **`$or` Element Narrowing**: Each element in a `$or` filter array is independently type-checked based on its own `type` field, preventing attribute mismatches.
827
1018
 
828
1019
  ## Best Practices
829
1020