Skip to content

Commit

Permalink
Merge 8fc2c04 into fca73d4
Browse files Browse the repository at this point in the history
  • Loading branch information
tywalch committed Nov 5, 2022
2 parents fca73d4 + 8fc2c04 commit 534567d
Show file tree
Hide file tree
Showing 8 changed files with 453 additions and 142 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,12 @@ All notable changes to this project will be documented in this file. Breaking ch

## [2.2.2] - 2022-11-04
### Added
- the return type from an update/patch call now returns an Entity item when `all_new` or `all_old` response options are passed
- the return type from an update/patch call now returns an Entity item when `all_new` or `all_old` response options are passed

## [2.2.3] = 2022-11-05
### Removed
- Backed out the response typing change added in `2.2.2`. The type of a record coming back from an update is more complex than one might expect. Because update operations can result in a record insert, the response type is not necessarily a TableItem. I am backing out this change for now until I can be be more sure of an appropriate typing.
### Added
- New function to help with Custom Types: CustomAttributeType. This replaces `createCustomAttribute` (now depreciated) because of the unfortunate widening caused by the initial implementation. [[read more](https://github.com/tywalch/electrodb/blob/master/README.md#custom-attributes))]
### Deprecated
- The function `createCustomAttribute` is now deprecated. The function still operates as it did, though any changes related to Custom Attribute types will see development focused on `CustomAttributeType` rather than this function.
67 changes: 61 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5635,12 +5635,16 @@ let stores = await StoreLocations.malls({mallId}).query({buildingId, storeId}).g
ElectroDB using advanced dynamic typing techniques to automatically create types based on the configurations in your model. Changes to your model will automatically change the types returned by ElectroDB.

## Custom Attributes
If you have a need for a custom attribute type (beyond those supported by ElectroDB) you can use the the export function `createCustomAttribute`. This function takes an attribute definition and allows you to specify a custom typed attribute with ElectroDB:
If you have a need for a custom attribute type (beyond those supported by ElectroDB) you can use the the export function `CustomAttributeType` or `OpaquePrimitiveType`. These functions can be passed a generic and that allow you to specify a custom attribute with ElectroDB:

> _NOTE: creating a custom type, ElectroDB will enforce attribute constraints based on the attribute definition provided, but will yield typing control to the user. This may result in some mismatches between your typing and the constraints enforced by ElectroDB._
### CustomAttributeType
This function allows for a narrowing of ElectroDB's `any` type, which does not enforce runtime type checks. This can be useful for expressing complex attribute types.

The function `CustomAttributeType` takes one argument, which is the "base" type of the attribute. For complex objects and arrays, the base object would be "any" but you can also use a base type like "string", "number", or "boolean" to accomplish (Opaque Keys)[#opaque-keys] which can be used as Composite Attributes.

In this example we accomplish a complex union type:
```typescript
import { Entity, createCustomAttribute } from 'electrodb';
import { Entity, CustomAttributeType } from 'electrodb';

const table = 'workplace_table';

Expand All @@ -5654,7 +5658,6 @@ type PersonnelRole = {
contractEndDate: number;
};


const person = new Entity({
model: {
entity: 'personnel',
Expand All @@ -5665,9 +5668,10 @@ const person = new Entity({
id: {
type: 'string'
},
role: createCustomAttribute<PersonnelRole>({
role: {
type: CustomAttributeType<PersonnelRole>('any'),
required: true,
}),
},
},
indexes: {
record: {
Expand All @@ -5684,6 +5688,57 @@ const person = new Entity({
}, { table });
```

### Opaque Keys
If you use Opaque Keys for identifiers or other primitive types, you can use the function `CustomAttributeType` and pass it the primitive base type of your key ('string', 'number', 'boolean'). This can be useful to gain more precise control over which properties can be used as entity identifiers, create unique unit types, etc.

```
import { Entity, CustomAttributeType } from 'electrodb';
const UniqueKeySymbol: unique symbol = Symbol();
type EmployeeID = string & {[UniqueKeySymbol]: any};
const UniqueAgeSymbol: unique symbol = Symbol();
type Month = number & {[UniqueAgeSymbol]: any};
const table = 'workplace_table';
const person = new Entity({
model: {
entity: 'personnel',
service: 'workplace',
version: '1'
},
attributes: {
employeeId: {
type: CustomAttributeType<EmployeeID>('string')
},
firstName: {
type: 'string',
required: true,
},
lastName: {
type: 'string',
required: true,
},
ageInMonths: {
type: CustomAttributeType<Month>('number')
}
},
indexes: {
record: {
pk: {
field: 'pk',
composite: ['employeeId']
},
sk: {
field: 'sk',
composite: [],
}
}
}
}, { table });
```

## Exported Types

The following types are exported for easier use while using ElectroDB with TypeScript. The naming convention for the types include three different kinds:
Expand Down
83 changes: 52 additions & 31 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -987,16 +987,7 @@ export type ServiceQueryRecordsGo<ResponseType, Options = QueryOptions> = <T = R

export type QueryRecordsGo<ResponseType, Options = QueryOptions> = <T = ResponseType>(options?: Options) => Promise<{ data: T, cursor: string | null }>;

export type UpdateRecordGo<ResponseType> = <T = ResponseType, Options extends UpdateQueryOptions = UpdateQueryOptions>(options?: Options) =>
Options extends infer O
? 'response' extends keyof O
? O['response'] extends 'all_new'
? Promise<{data: T}>
: O['response'] extends 'all_old'
? Promise<{data: T}>
: Promise<{data: Partial<T>}>
: Promise<{data: Partial<T>}>
: never;
export type UpdateRecordGo<ResponseType> = <T = ResponseType, Options extends UpdateQueryOptions = UpdateQueryOptions>(options?: Options) => Promise<{data: Partial<T>}>

export type PutRecordGo<ResponseType, Options = QueryOptions> = <T = ResponseType>(options?: Options) => Promise<{ data: T }>;

Expand Down Expand Up @@ -1454,16 +1445,23 @@ type StaticAttribute = {
readonly field?: string;
}

type CustomAttribute<T extends any = any> = {
readonly type: "custom";
readonly [CustomAttributeSymbol]: T;
type CustomAttributeTypeName<T> = { readonly [CustomAttributeSymbol]: T };

type OpaquePrimitiveTypeName<T extends string | number | boolean> =
T extends string ? 'string' & { readonly [OpaquePrimitiveSymbol]: T }
: T extends number ? 'number' & { readonly [OpaquePrimitiveSymbol]: T }
: T extends boolean ? 'boolean' & { readonly [OpaquePrimitiveSymbol]: T }
: never;

type CustomAttribute = {
readonly type: CustomAttributeTypeName<any> | OpaquePrimitiveTypeName<any>;
readonly required?: boolean;
readonly hidden?: boolean;
readonly readOnly?: boolean;
readonly get?: (val: T, item: any) => T | undefined | void;
readonly set?: (val?: T, item?: any) => T | undefined | void;
readonly default?: T | (() => T);
readonly validate?: ((val: T) => boolean) | ((val: T) => void) | ((val: T) => string | void);
readonly get?: (val: any, item: any) => any | undefined | void;
readonly set?: (val?: any, item?: any) => any | undefined | void;
readonly default?: any | (() => any);
readonly validate?: ((val: any) => boolean) | ((val: any) => void) | ((val: any) => string | void);
readonly field?: string;
readonly watch?: ReadonlyArray<string> | "*";
};
Expand Down Expand Up @@ -1621,7 +1619,9 @@ type PartialDefinedKeys<T> = {
}

export type ItemAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A["type"] extends infer R
? R extends "string" ? string
Expand Down Expand Up @@ -1661,8 +1661,10 @@ export type ItemAttribute<A extends Attribute> =
: never

export type ReturnedAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
? T
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A["type"] extends infer R
? R extends "static" ? never
: R extends "string" ? string
Expand Down Expand Up @@ -1716,7 +1718,9 @@ export type ReturnedAttribute<A extends Attribute> =
: never

export type CreatedAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A["type"] extends infer R
? R extends "static" ? never
Expand Down Expand Up @@ -1779,8 +1783,10 @@ export type CreatedItem<A extends string, F extends string, C extends string, S
}

export type EditableItemAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
? T
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A extends ReadOnlyAttribute
? never
: A["type"] extends infer R
Expand Down Expand Up @@ -1826,7 +1832,9 @@ export type EditableItemAttribute<A extends Attribute> =
: never

export type UpdatableItemAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A extends ReadOnlyAttribute
? never
Expand Down Expand Up @@ -1888,7 +1896,9 @@ export type UpdatableItemAttribute<A extends Attribute> =
: never

export type RemovableItemAttribute<A extends Attribute> =
A extends CustomAttribute<infer T>
A['type'] extends OpaquePrimitiveTypeName<infer T>
? T
: A['type'] extends CustomAttributeTypeName<infer T>
? T
: A extends ReadOnlyAttribute | RequiredAttribute
? never
Expand Down Expand Up @@ -2072,7 +2082,7 @@ export type RemoveItem<A extends string, F extends string, C extends string, S e
export type AppendItem<A extends string, F extends string, C extends string, S extends Schema<A,F,C>> =
{
[
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'list' | 'any' | 'custom'
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'list' | 'any' | 'custom' | CustomAttributeTypeName<any>
? P
: never
]?: P extends keyof SetItem<A,F,C,S>
Expand All @@ -2083,7 +2093,7 @@ export type AppendItem<A extends string, F extends string, C extends string, S e
export type AddItem<A extends string, F extends string, C extends string, S extends Schema<A,F,C>> =
{
[
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'number' | 'any' | 'set' | 'custom'
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'number' | 'any' | 'set' | 'custom' | CustomAttributeTypeName<any> | OpaquePrimitiveTypeName<number>
? P
: never
]?: P extends keyof SetItem<A,F,C,S>
Expand All @@ -2094,7 +2104,7 @@ export type AddItem<A extends string, F extends string, C extends string, S exte
export type SubtractItem<A extends string, F extends string, C extends string, S extends Schema<A,F,C>> =
{
[
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'number' | 'any' | 'custom'
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'number' | 'any' | 'custom' | OpaquePrimitiveTypeName<number>
? P
: never
]?: P extends keyof SetItem<A,F,C,S>
Expand All @@ -2105,7 +2115,7 @@ export type SubtractItem<A extends string, F extends string, C extends string, S
export type DeleteItem<A extends string, F extends string, C extends string, S extends Schema<A,F,C>> =
{
[
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'any' | 'set' | 'custom'
P in keyof ItemTypeDescription<A,F,C,S> as ItemTypeDescription<A,F,C,S>[P] extends 'any' | 'set' | 'custom' | CustomAttributeTypeName<any>
? P
: never
]?: P extends keyof SetItem<A,F,C,S>
Expand All @@ -2116,6 +2126,7 @@ export type DeleteItem<A extends string, F extends string, C extends string, S e
export declare const WhereSymbol: unique symbol;
export declare const UpdateDataSymbol: unique symbol;
export declare const CustomAttributeSymbol: unique symbol;
export declare const OpaquePrimitiveSymbol: unique symbol;

export type WhereAttributeSymbol<T extends any> =
{ [WhereSymbol]: void }
Expand Down Expand Up @@ -2317,6 +2328,16 @@ type CustomAttributeDefinition<T> = {
readonly validate?: ((val: T) => boolean) | ((val: T) => void) | ((val: T) => string | void);
readonly field?: string;
readonly watch?: ReadonlyArray<string> | "*";
}
};

declare function createCustomAttribute<T>(definition?: CustomAttributeDefinition<T>): CustomAttribute<T>;
/** @depricated use 'CustomAttributeType' or 'OpaquePrimitiveType' instead */
declare function createCustomAttribute<T, A extends Readonly<CustomAttributeDefinition<T>> = Readonly<CustomAttributeDefinition<T>>>(definition?: A): A & { type: CustomAttributeTypeName<T> };

declare function CustomAttributeType<T>(
base: T extends string ? 'string'
: T extends number ? 'number'
: T extends boolean ? 'boolean'
: 'any'
): T extends string | number | boolean
? OpaquePrimitiveTypeName<T>
: CustomAttributeTypeName<T>;
5 changes: 3 additions & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
const { Entity } = require("./src/entity");
const { Service } = require("./src/service");
const { createCustomAttribute } = require('./src/schema');
const { createCustomAttribute, CustomAttributeType } = require('./src/schema');
const { ElectroError, ElectroValidationError, ElectroUserValidationError, ElectroAttributeValidationError } = require('./src/errors');

module.exports = {
Entity,
Service,
createCustomAttribute,
ElectroError,
CustomAttributeType,
createCustomAttribute,
ElectroValidationError,
};
59 changes: 1 addition & 58 deletions index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3986,61 +3986,4 @@ type AvailableParsingOptions = Parameters<typeof casingEntity.parse>[1]
expectAssignable<AvailableParsingOptions>(undefined);
expectAssignable<AvailableParsingOptions>({});
expectAssignable<AvailableParsingOptions>({ignoreOwnership: true});
expectAssignable<AvailableParsingOptions>({ignoreOwnership: false});

normalEntity2
.update({
prop1: 'abc',
prop2: 'def',
prop5: 123
})
.set({attr6: 456})
.go()
.then(results => {
expectType<{
prop1?: string | undefined;
prop2?: string | undefined;
prop3?: string | undefined;
prop5?: number | undefined;
attr6?: number | undefined;
attr9?: number | undefined;
}>(magnify(results.data));
});

normalEntity2
.update({
prop1: 'abc',
prop2: 'def',
prop5: 123
})
.set({attr6: 456})
.go({response: 'updated_new'})
.then(results => {
expectType<{
prop1?: string | undefined;
prop2?: string | undefined;
prop3?: string | undefined;
prop5?: number | undefined;
attr6?: number | undefined;
attr9?: number | undefined;
}>(magnify(results.data));
});

normalEntity2
.update({
prop1: 'abc',
prop2: 'def',
prop5: 123
})
.set({attr6: 456})
.go({response: 'all_new'})
.then(results => {
expectType<{
prop1: string;
prop2: string;
prop3: string;
prop5: number;
attr6?: number | undefined;
attr9?: number | undefined;
}>(magnify(results.data));
});
expectAssignable<AvailableParsingOptions>({ignoreOwnership: false});
9 changes: 9 additions & 0 deletions src/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -1464,10 +1464,19 @@ function createCustomAttribute(definition = {}) {
};
}

function CustomAttributeType(base) {
const supported = ['string', 'number', 'boolean', 'any'];
if (!supported.includes(base)) {
throw new Error(`OpaquePrimitiveType only supports base types: ${u.commaSeparatedString(supported)}`);
}
return base;
}

module.exports = {
Schema,
Attribute,
SetAttribute,
CastTypes,
CustomAttributeType,
createCustomAttribute,
};

0 comments on commit 534567d

Please sign in to comment.