Skip to content

Commit 66f5631

Browse files
committed
feat(attributes): added type support for keys defined on the model
1 parent 7cf3ca6 commit 66f5631

File tree

8 files changed

+139
-74
lines changed

8 files changed

+139
-74
lines changed

docs/calliope/readme.md

+18-6
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Model
22

33
The model is at the hearth of this package. It boasts a lot of features, so they have been broken down into the following sections:
4-
- [Attributes](./attributes.md)
5-
- [Api calls](./api-calls.md)
6-
- [Query Building](./query-building.md)
7-
- [Relationships](./relationships.md)
8-
- [Timestamps](./timestamps.md)
9-
- [Additional Methods](#additional-methods)
4+
- [Attributes](./attributes.md)
5+
- [Api calls](./api-calls.md)
6+
- [Query Building](./query-building.md)
7+
- [Relationships](./relationships.md)
8+
- [Timestamps](./timestamps.md)
9+
- [Additional Methods](#additional-methods)
1010

1111
## Creating Models
1212

@@ -18,6 +18,18 @@ import { Model } from '@upfrontjs/framework';
1818
export default class User extends Model {}
1919
```
2020

21+
::: tip TIP (Typescript)
22+
Typescript users may benefit from better typing support if they defined keys and their types on the models
23+
```ts
24+
export default class User extends Model {
25+
public is_admin?: boolean;
26+
public age?: number;
27+
public name?: string;
28+
}
29+
```
30+
This will typehint keys on the model when accessing the above keys like `user.age` and will get type hinted in various methods such as [getAttribute](./attributes.md#getattribute) where both the key, the default value and the return value will be type hinted.
31+
:::
32+
2133
Then you can call your model in various way, for example
2234
```js
2335
// myScript.js

src/Calliope/Concerns/CallsApi.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,7 @@ export default class CallsApi extends BuildsQuery {
324324
}
325325

326326
Object.defineProperty(this, key, {
327-
get: () => to,
327+
get: () => to, // todo cast toDateTime?
328328
configurable: true,
329329
enumerable: true
330330
});

src/Calliope/Concerns/CastsAttributes.ts

+25-20
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Collection from '../../Support/Collection';
33
import { cloneDeep, merge } from 'lodash';
44
import type AttributeCaster from '../../Contracts/AttributeCaster';
55
import GlobalConfig from '../../Support/GlobalConfig';
6-
import type { Attributes } from './HasAttributes';
6+
import type { Attributes, AttributeKeys } from './HasAttributes';
77
import InvalidArgumentException from '../../Exceptions/InvalidArgumentException';
88
import { isConstructableUserClass, isObjectLiteral } from '../../Support/function';
99
import type Model from '../Model';
@@ -19,7 +19,7 @@ export default class CastsAttributes {
1919
*
2020
* @type {object}
2121
*/
22-
protected attributeCasts: Record<string, CastType> = this.casts;
22+
protected attributeCasts = this.casts as Record<AttributeKeys<this> | string, CastType>;
2323

2424
/**
2525
* The attributes that should be cast.
@@ -39,7 +39,7 @@ export default class CastsAttributes {
3939
*
4040
* @return {this}
4141
*/
42-
public mergeCasts(casts: Record<string, CastType>): this {
42+
public mergeCasts(casts: Record<AttributeKeys<this> | string, CastType>): this {
4343
this.attributeCasts = merge(this.attributeCasts, casts);
4444

4545
return this;
@@ -52,7 +52,7 @@ export default class CastsAttributes {
5252
*
5353
* @return {this}
5454
*/
55-
public setCasts(casts: Record<string, CastType>): this {
55+
public setCasts(casts: Record<AttributeKeys<this> | string, CastType>): this {
5656
this.attributeCasts = casts;
5757

5858
return this;
@@ -65,7 +65,7 @@ export default class CastsAttributes {
6565
*
6666
* @return {boolean}
6767
*/
68-
public hasCast(key: string): key is BuiltInCastType | 'object' {
68+
public hasCast(key: AttributeKeys<this> | string): key is BuiltInCastType | 'object' {
6969
const cast = this.getCastType(key);
7070

7171
if (!cast) return false;
@@ -82,7 +82,7 @@ export default class CastsAttributes {
8282
*
8383
* @return {string|undefined}
8484
*/
85-
protected getCastType(key: string): BuiltInCastType | 'object' | undefined {
85+
protected getCastType(key: AttributeKeys<this> | string): BuiltInCastType | 'object' | undefined {
8686
const caster = this.attributeCasts[key];
8787

8888
if (!caster) {
@@ -108,12 +108,12 @@ export default class CastsAttributes {
108108
*
109109
* @return {any}
110110
*/
111-
protected castAttribute(
112-
key: string,
111+
protected castAttribute<T>(
112+
key: AttributeKeys<this> | string,
113113
value: any,
114-
attributes?: Attributes,
114+
attributes?: Attributes, // todo - replace this with this.getRawAttributes
115115
method: keyof AttributeCaster = 'get'
116-
): unknown {
116+
): T {
117117
value = cloneDeep(value);
118118

119119
if (!this.hasCast(key)) {
@@ -122,44 +122,49 @@ export default class CastsAttributes {
122122

123123
switch (this.getCastType(key)) {
124124
case 'boolean':
125-
return this.castToBoolean(key, value);
125+
value = this.castToBoolean(key, value);
126+
break;
126127
case 'string':
127-
return this.castToString(key, value);
128+
value = this.castToString(key, value);
129+
break;
128130
case 'number':
129-
return this.castToNumber(key, value);
131+
value = this.castToNumber(key, value);
132+
break;
130133
case 'object':
131-
return this.castWithObject(
134+
value = this.castWithObject(
132135
key,
133136
value,
134137
attributes ?? (this as unknown as Model).getRawAttributes(),
135138
method
136139
);
140+
break;
137141
case 'collection':
138142
if (method === 'set') {
139143
if (Collection.isCollection(value)) {
140144
// we don't want to wrap collection in a collection on every get
141-
return value.toArray();
145+
value = value.toArray();
142146
}
143-
144-
return value;
145147
} else {
146-
return new Collection(value);
148+
value = new Collection(value);
147149
}
150+
break;
148151
case 'datetime':
149152
if (method === 'set') {
150153
// check if it throws
151154
this.getDateTimeLibInstance(value);
152-
return value;
153155
} else {
154-
return this.castToDateTime(key, value);
156+
value = this.castToDateTime(key, value);
155157
}
158+
break;
156159
default:
157160
// either or both hasCast() and getCastType() has been overridden and hasCast()
158161
// returns true while getCastType() returns a value that lands in this default case
159162
throw new LogicException(
160163
'Impossible logic path reached. getCastType() returned unexpected value.'
161164
);
162165
}
166+
167+
return value;
163168
}
164169

165170
/**

src/Calliope/Concerns/GuardsAttributes.ts

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import CastsAttributes from './CastsAttributes';
2-
import type { Attributes } from './HasAttributes';
2+
import type { Attributes, AttributeKeys } from './HasAttributes';
33

44
export default class GuardsAttributes extends CastsAttributes {
55
/**
@@ -9,7 +9,7 @@ export default class GuardsAttributes extends CastsAttributes {
99
*
1010
* @type {string[]}
1111
*/
12-
protected fillableAttributes: string[] = this.fillable;
12+
protected fillableAttributes: (AttributeKeys<this> | string)[] = this.fillable;
1313

1414
/**
1515
* The attributes that aren't mass assignable
@@ -18,14 +18,14 @@ export default class GuardsAttributes extends CastsAttributes {
1818
*
1919
* @type {string[]}
2020
*/
21-
protected guardedAttributes: string[] = this.guarded;
21+
protected guardedAttributes: (AttributeKeys<this> | string)[] = this.guarded;
2222

2323
/**
2424
* The attributes that are mass assignable.
2525
*
2626
* @type {string[]}
2727
*/
28-
public get fillable(): string[] {
28+
public get fillable(): (AttributeKeys<this> | string)[] {
2929
return [];
3030
}
3131

@@ -34,7 +34,7 @@ export default class GuardsAttributes extends CastsAttributes {
3434
*
3535
* @type {string[]}
3636
*/
37-
public get guarded(): string[] {
37+
public get guarded(): (AttributeKeys<this> | string)[] {
3838
return ['*'];
3939
}
4040

@@ -43,7 +43,7 @@ export default class GuardsAttributes extends CastsAttributes {
4343
*
4444
* @return {string[]}
4545
*/
46-
public getGuarded(): string[] {
46+
public getGuarded(): (AttributeKeys<this> | string)[] {
4747
return this.guardedAttributes;
4848
}
4949

@@ -52,7 +52,7 @@ export default class GuardsAttributes extends CastsAttributes {
5252
*
5353
* @return {string[]}
5454
*/
55-
public getFillable(): string[] {
55+
public getFillable(): (AttributeKeys<this> | string)[] {
5656
return this.fillableAttributes;
5757
}
5858

@@ -63,7 +63,7 @@ export default class GuardsAttributes extends CastsAttributes {
6363
*
6464
* @return {this}
6565
*/
66-
public mergeFillable(fillable: string[]): this {
66+
public mergeFillable(fillable: (AttributeKeys<this> | string)[]): this {
6767
this.fillableAttributes = [...this.getFillable(), ...fillable];
6868

6969
return this;
@@ -76,7 +76,7 @@ export default class GuardsAttributes extends CastsAttributes {
7676
*
7777
* @return {this}
7878
*/
79-
public mergeGuarded(guarded: string[]): this {
79+
public mergeGuarded(guarded: (AttributeKeys<this> | string)[]): this {
8080
this.guardedAttributes = [...this.getGuarded(), ...guarded];
8181

8282
return this;
@@ -89,7 +89,7 @@ export default class GuardsAttributes extends CastsAttributes {
8989
*
9090
* @return {this}
9191
*/
92-
public setFillable(fillable: string[]): this {
92+
public setFillable(fillable: (AttributeKeys<this> | string)[]): this {
9393
this.fillableAttributes = fillable;
9494

9595
return this;
@@ -102,7 +102,7 @@ export default class GuardsAttributes extends CastsAttributes {
102102
*
103103
* @return {this}
104104
*/
105-
public setGuarded(guarded: string[]): this {
105+
public setGuarded(guarded: (AttributeKeys<this> | string)[]): this {
106106
this.guardedAttributes = guarded;
107107

108108
return this;
@@ -113,7 +113,7 @@ export default class GuardsAttributes extends CastsAttributes {
113113
*
114114
* @param {string} key
115115
*/
116-
public isFillable(key: string): boolean {
116+
public isFillable(key: AttributeKeys<this> | string): boolean {
117117
return this.getFillable().includes(key) || this.getFillable().includes('*');
118118
}
119119

@@ -122,7 +122,7 @@ export default class GuardsAttributes extends CastsAttributes {
122122
*
123123
* @param {string} key
124124
*/
125-
public isGuarded(key: string): boolean {
125+
public isGuarded(key: AttributeKeys<this> | string): boolean {
126126
// if key is defined in both guarded and fillable, then fillable takes priority.
127127
return (this.getGuarded().includes(key) || this.getGuarded().includes('*')) && !this.isFillable(key);
128128
}

0 commit comments

Comments
 (0)