Skip to content

Commit

Permalink
feat: 0.9.0 (#196)
Browse files Browse the repository at this point in the history
## Refactor
* refactor(internal): renamed memorised ids name  in factory
  * This will reduce the likelihood of clashing with user's code

## Performance
* perf(internal): removed repeated calls in factory builder `getKey` method

## Fix
* fix(internal): prevent losing error name when mangling code
* fix(factory): ensured states are only called once
* fix(model)!: `exists` now accepts any string id as valid
    * So long the string has length
* fix(factory)!: factory now respects the model's key type
* fix(attributes)!: recursively transform keys on mass-assignment
    * On outgoing object keys were recursively set back to `serverAttributeCasing`.
    * Now the incoming object keys behave the same way and are set to `attributeCasing`.
    * Bringing it the behaviour in line with expectations.
* fix(attributes): added SimpleAttributes type
    * This type is same as Attributes except it does not include Models and ModelCollections
      Resolves #183
* fix(timestamps): added missing deletedAt value in FormData
* fix(attributes)!: updated attributeCasing modifier type to protected
    * just like `serverAttributeCasing` this isn't expected to be used outside of the class.
* fix(model-collection): fixed `toJSON` return type
    * Type information got lost when using return type of model type argument's `toJSON`

## Feature
* feat(factory): added factory type argument to factory builder
* feat(model): added keyType getter
    * This change allows for custom string ids that are not uuids and the factory to set keys as string
* feat(model): improved `getKey` typing
    * It now returns the correct type based on the type argument or `keyType` return value
* feat(model-collection): improved `modelKeys` return value
    * Changes missed from 6c54090
* feat(api-calls): improved `call` method signature with overrides
* feat(attributes): added `RawAttributes` type
    * This utility type helps describe the raw json version of the model.
* feat(factory): improved raw method typings
* feat(factory): added `attributes` method
* feat(collection): added missing array methods
* feat(factory): added missing type export
    * Missed in 5c191a2
* feat: added `PartialSome` utility type

## Documentation
* docs(factory): give more helpful error messages in case of mangling
* docs(factory): added tip for factory files and improved interface doc
* docs(model): updated examples to use `create`
    * Missed updating when moved to the `create` method
* docs(model): updated documentation around mass assignment
* docs: simplified custom collection extending recipe
* docs(api-calls): fixed wording
* docs(attributes): formatted inline comment
* docs(timestamps): added comment

## Testing
* test(factory): added test for states application order
* test: improved test helpers
* test(model-collection): fixed typing error in test
* test(factory): reset state after testing
* test(attributes): corrected test block name

## Chore
* chore: incremented version
* chore: updated test tsconfig
* chore: removed unused type import
* chore(deps-dev): updated dependencies
    * @commitlint/config-conventional
    * @commitlint/prompt-cli
    * @commitlint/types
    * @typescript-eslint/eslint-plugin
    * @typescript-eslint/parser
    * commitlint
    * eslint
    * lint-staged
    * rollup

## Continuous Integration
* ci: ensure `.nojekyll` exists

## Refactor
* refactor(api-calls): `customHeaders` type DRY-ed by adding a type

## Style
* style: fixed code style issues
  • Loading branch information
nandi95 authored Feb 14, 2022
1 parent 8828051 commit fa29d20
Show file tree
Hide file tree
Showing 41 changed files with 1,413 additions and 893 deletions.
3 changes: 2 additions & 1 deletion .github/workflows/deploy-api-docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ jobs:
git add -f assets
git add -f classes
git add -f interfaces
git add -f '.nojekyll'
touch .nojekyll
git add -f .nojekyll
git add -f index.html
git add -f modules.html
git commit -m "Updates from ${{ github.ref }}" --no-verify
Expand Down
4 changes: 2 additions & 2 deletions docs/calliope/api-calls.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ The `loading` property indicates whether there is an ongoing request on the mode

#### serverAttributeCasing

The `serverAttributeCasing` is a getter which similarly to [attributeCasing](./attributes.md#attributecasing) casts the request keys to the given casing on outgoing requests. The valid values are `'snake'` or `'camel'` with `'snake'` being the default value.
The `serverAttributeCasing` is a getter which similarly to [attributeCasing](./attributes.md#attributecasing) casts the request keys recursively to the given casing on outgoing requests. The valid values are `'snake'` or `'camel'` with `'snake'` being the default value.

#### _lastSyncedAt

The `_lastSyncedAt` or `_last_synced_at` (naming subject to [attributeCasing](./attributes.md#attributecasing)) attribute is a getter attribute that is set only when the model data has been fetched, [saved](./readme.md#save) or [refreshed](./readme.md#refresh). It is type subject to the [datetime](./attributes.md#datetime) setting, with the value of when was the last time the data has been loaded from the backend.
The `_lastSyncedAt` or `_last_synced_at` (naming subject to [attributeCasing](./attributes.md#attributecasing)) attribute is a getter attribute that is set only when the model data has been fetched, [saved](./readme.md#save) or [refreshed](./readme.md#refresh). Its type subject to the [datetime](./attributes.md#datetime) setting, with the value of when was the last time the data has been loaded from the backend.

## Methods

Expand Down
56 changes: 22 additions & 34 deletions docs/calliope/attributes.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ This in action will look like:
```js
import User from '@Models/User';

const user = new User({ someAttribute: 1, name: 'name' });
const user = User.create({ someAttribute: 1, name: 'name' });
user.getAttributes(); // { name: 'name' }
```

Expand Down Expand Up @@ -318,32 +318,19 @@ After you have defined your accessors and mutators they'll be automatically call
```js
import User from '@Models/User';

const user = new User({ title: 'Dr.', fullName: 'John Doe' });
const user = User.create({ title: 'Dr.', fullName: 'John Doe' });
user.fullName; // 'Dr. John Doe'
```

## Attribute management

Models can be constructed with the `new` keyword. This will mass assign attributes to the model while respecting the [guarding](#guarding) settings.

::: warning
When constructing an instance and passing in another instance of the model:
```js
import User from '@Models/User';
import Shift from '@Models/Shift';

const user = new User({ name: 'John Doe' });
const newUser = new User(user);
```
It will clone the [raw attributes](#getrawattributes) and the [relationships](./relationships.md) of the model.
:::

#### attributeCasing

While some prefer to name their variables and object keys as [camelCase](../helpers/readme.md#camel) others will prefer [snake_case](../helpers/readme.md#snake) or perhaps there are different conventions between the front and back end. To accommodate such preferences you can set the `attributeCasing` getter to return either `'camel'` or `'snake'` like so:

<code-group>
<code-block title="Javascript">

```js
// User.js
import { Model } from '@upfrontjs/framework';
Expand All @@ -357,6 +344,7 @@ export default class User extends Model {
</code-block>

<code-block title="Typescript">

```ts
// User.ts
import { Model } from '@upfrontjs/framework';
Expand All @@ -370,7 +358,7 @@ export default class User extends Model {
</code-block>
</code-group>

When using mass-assignment like the [constructor](#attribute-management) and the [fill](#fill) methods, all keys will automatically transform to its respective casing. `user.fill({ some_value: 1 }).someValue; // 1`
When using mass-assignment like the [create](./readme.md#create) and the [fill](#fill) methods, all keys of the given arguments will automatically and recursively transform to the set casing. e.g.: `user.fill({ some_value: 1 }).someValue; // 1`
The default value is `'camel'`. This can be counteracted by the [serverAttributeCasing](./api-calls.md#serverattributecasing) getter method when sending data to the server.

---
Expand All @@ -385,7 +373,7 @@ Just like object literals, models are also iterable using a for of loop. This lo
import User from '@Models/User';
import Shift from '@Models/Shift';

const user = new User({ title: 'Dr.', shifts: [new Shift] });
const user = User.create({ title: 'Dr.', shifts: [new Shift] });

for (const [item, key] of user) {
// ...
Expand Down Expand Up @@ -419,7 +407,7 @@ The `getAttribute` method is what's used for getting attributes from the model.
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe' });
const user = User.create({ name: 'John Doe' });
user.name; // 'John Doe'
user.getAttribute('name'); // 'John Doe'
```
Expand All @@ -441,7 +429,7 @@ The `getAttributes` method returns all the attributes that has been set in an ob
```js
import User from '@Models/User';

const user = new User({ firstName: 'John', lastName: 'Doe' });
const user = User.create({ firstName: 'John', lastName: 'Doe' });
user.getAttributes(); // { firstName: 'John', lastName: 'Doe' }
```

Expand All @@ -456,7 +444,7 @@ The `getAttributeKeys` method returns all the attribute keys on the model curren
```js
import User from '@Models/User';

const user = new User({ firstName: 'John', lastName: 'Doe' });
const user = User.create({ firstName: 'John', lastName: 'Doe' });
user.getAttributeKeys(); // ['firstName', 'lastName']
```

Expand All @@ -478,7 +466,7 @@ export default class User extends Model {
import User from '@Models/User';
import Shift from '@Models/Shift';

const user = new User({ firstName: 'John', lastName: 'Doe' });
const user = User.create({ firstName: 'John', lastName: 'Doe' });
user.property = 1;
user.addRelation('shifts', [new Shift]);

Expand All @@ -495,7 +483,7 @@ The `fill` method merges in the given attributes onto the model that are conside
```js
import User from '@Models/User';

const user = new User({ firstName: 'John', lastName: 'Doe' });
const user = User.create({ firstName: 'John', lastName: 'Doe' });
user.fill({ fistName: 'Jane', title: 'Dr.' }).getAttributes(); // { firstName: 'Jane', lastName: 'Doe', title: 'Dr. }
```

Expand All @@ -510,7 +498,7 @@ The `only` method returns only the attributes that match the given key(s).
```js
import User from '@Models/User';

const user = new User({ firstName: 'John', lastName: 'Doe' });
const user = User.create({ firstName: 'John', lastName: 'Doe' });
user.only('fistName'); // { firstName: 'John' }
```
#### except
Expand All @@ -520,7 +508,7 @@ The `except` method returns only the attributes that match does not the given ke
```js
import User from '@Models/User';

const user = new User({ firstName: 'John', lastName: 'Doe', title: 'Dr.' });
const user = User.create({ firstName: 'John', lastName: 'Doe', title: 'Dr.' });
user.except(['fistName', 'title']); // { lastName: 'Doe' }
```

Expand All @@ -531,7 +519,7 @@ The `toJSON` method returns the json representation of the model's attributes an
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe' });
const user = User.create({ name: 'John Doe' });
user.addRelation('shifts', new Shift({ shiftAttr: 1 })).toJSON(); // {"name":"John Doe","shifts":[{"shiftAttr":1}]}
```

Expand All @@ -545,7 +533,7 @@ The `syncOriginal` method set's the original state of the data to the current st
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe' });
const user = User.create({ name: 'John Doe' });
user.name = 'Jane Doe';
user.getOriginal('name'); // 'John Doe'
user.syncOriginal().getOriginal('name'); // 'Jane Doe'
Expand All @@ -558,7 +546,7 @@ The `reset` method will set the attributes to the original values, discarding an
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe' });
const user = User.create({ name: 'John Doe' });
user.name = 'new name';
user.getChanges(); // { name: 'new name' }
user.reset().getChanges(); // {}
Expand All @@ -570,7 +558,7 @@ The `getOriginal` method returns the original value in a resolved format. Meanin
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe' });
const user = User.create({ name: 'John Doe' });
user.name = 'Jane Doe';
user.getOriginal('name'); // 'John Doe'
user.getOriginal('title', 'Mr.'); // 'Mr.'
Expand All @@ -586,7 +574,7 @@ The `getChanges` method returns only the changed data since the model was constr
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe', title: 'Mr.' });
const user = User.create({ name: 'John Doe', title: 'Mr.' });
user.getChanges(); // {}
user.name = 'Jane Doe';
user.getChanges(); // { name: 'Jane Doe' }
Expand All @@ -601,7 +589,7 @@ The `getDeletedAttributes` method returns only the deleted attributes since the
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe', title: 'Mr.' });
const user = User.create({ name: 'John Doe', title: 'Mr.' });
user.getDeletedAttributes(); // {}
user.deleteAttribute('name').getDeletedAttributes(); // { name: 'John Doe' }
user.deleteAttribute('title').getDeletedAttributes('name'); // { name: 'John Doe' }
Expand All @@ -614,7 +602,7 @@ The `getNewAttributes` method returns only the newly added attributes since the
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe', title: 'Mr.' });
const user = User.create({ name: 'John Doe', title: 'Mr.' });
user.getNewAttributes(); // {}
user.setAttribute('attr', 1).getNewAttributes(); // { attr: 1 }
user.setAttribute('attr2', 2).getNewAttributes('attr'); // { attr: 1 }
Expand All @@ -626,7 +614,7 @@ The `hasChanges` method determines whether any changes have occurred since const
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe', title: 'Mr.' });
const user = User.create({ name: 'John Doe', title: 'Mr.' });
user.hasChanges(); // false
user.name = 'Jane Doe';
user.hasChanges(); // true
Expand All @@ -647,7 +635,7 @@ The `isClean` method determines whether the attributes matches with the original
```js
import User from '@Models/User';

const user = new User({ name: 'John Doe', title: 'Mr.' });
const user = User.create({ name: 'John Doe', title: 'Mr.' });
user.isClean(); // true
user.name = 'Jane Doe';
user.isClean(); // false
Expand Down
38 changes: 37 additions & 1 deletion docs/calliope/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,36 @@ The `primaryKey` is a getter of the attribute name which is used to identify you
import { Model } from '@upfrontjs/framework';

export default class User extends Model {
getName() {
return 'User';
}

get primaryKey() {
return 'id';
}
}
```

#### keyType

The `keyType` is a getter that identifies what the type is of your [primaryKey](#primarykey). Its value has to be either `'string'` or `'number'` with `'number'` being the default value.
You should update this value to `'string'` if you're using a uuid or some custom string for the primary key as this is used when in the [Factory](../testing.md#factories) and [exists](#exists) logic.

```js
// User.ts
import { Model } from '@upfrontjs/framework';

export default class User extends Model {
public override getName(): string {
return 'User';
}

public get primaryKey(): 'string' {
return 'string';
}
}
```

#### exists

The `exists` property is a getter on the model that returns a boolean which can be used to assert that the model has been persisted. It takes the [primary key](#getkey), [timestamps](./timestamps.md#timestamps) and [soft deletes](./timestamps.md#soft-deletes) into account.
Expand Down Expand Up @@ -146,7 +170,7 @@ Bundlers/minifier options examples:
#### create
<Badge text="static" type="warning"/>

The `create` method instantiates your model while setting up attributes and relations.
The `create` method instantiates your model while setting up attributes and relations. This will mass assign attributes to the model while respecting the [guarding](./attributes#guarding) settings.

```ts
import User from '@Models/User';
Expand All @@ -157,6 +181,18 @@ const user = User.create({ name: 'User Name' }); // User
Constructing a new class like `new User({...})` is **not** acceptable. This will not overwrite your class fields with default values if the same key has been passed in due to how JavaScript first constructs the parent class and only then the subclasses. However, you can still use it to call instance methods. Furthermore, it will not cause unexpected results if using it with the [setAttribute](./attributes.md#setattribute) method or call methods that under the hood uses the [setAttribute](./attributes.md#setattribute).
:::

::: tip
When creating an instance and passing in another instance of the model:
```js
import User from '@Models/User';
import Shift from '@Models/Shift';

const user = User.create({ name: 'John Doe' });
const newUser = User.create(user);
```
It will clone the [raw attributes](./attributes#getrawattributes) and the [relationships](./relationships.md#getrelations) of the model.
:::

#### replicate

The `replicate` method copies the instance into a non-existent instance. Meaning primary key and the timestamps won't be copied.
Expand Down
24 changes: 15 additions & 9 deletions docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Don't be afraid of changing and overriding methods if that solves your problem.
<code-group>

<code-block title="Javascript">

```js
// UserCollection.js
import User from '@models/User';
Expand All @@ -32,16 +33,18 @@ export default class UserCollection extends ModelCollection {
}

async markAsReady() {
return User.whereKey(this.modelKeys()).update({ ready: true });
return User.whereKey(this.modelKeys()).patch({ ready: true });
}
}

// my-file.js
import User from '@models/User';
import UserCollection from '@/UserCollection';

const modelCollection = await User.latest().limit(10).get();
let users = new UserCollection(modelCollection.toArray());
const users = await User.latest()
.limit(10)
.get()
.then(users => new UserCollection(users.toArray()));

if (users.areAwake().length === modelCollection.length) {
await users.markAsReady();
Expand All @@ -52,27 +55,30 @@ if (users.areAwake().length === modelCollection.length) {
</code-block>

<code-block title="Typescript">

```ts
// UserCollection.ts
import User from '@models/User';
import { ModelCollection } from '@upfrontjs/framework';

export default class UserCollection<T extends User> extends ModelCollection<T> {
areAwake(): ModelCollection<T> {
export default class UserCollection<T extends User = User> extends ModelCollection<T> {
public areAwake(): ModelCollection<T> {
return this.filter(user => user.wokeUp && user.hadBeverage('hot'));
}

async markAsReady(): ModelCollection<User> {
return User.whereKey(this.modelKeys()).update({ ready: true });
public async markAsReady(): ModelCollection<T> {
return User.whereKey(this.modelKeys()).patch({ ready: true });
}
}

// my-file.ts
import User from '@models/User';
import UserCollection from '@/UserCollection';

const modelCollection = await User.latest().limit(10).get();
let users = new UserCollection(modelCollection.toArray());
const users = await User.latest()
.limit(10)
.get()
.then(users => new UserCollection(users.toArray()));

if (users.areAwake().length === modelCollection.length) {
await users.markAsReady();
Expand Down
2 changes: 2 additions & 0 deletions docs/helpers/collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

The collection is an object an implementing `ArrayLike` and [Iterable](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols) meaning it can be iterated with `for... of` or generators, has a `.length` property, and it can be indexed by numbers `collection[0]`. It implements all method available on the [array](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array) in a compatible fashion. The collection is immutable unless the methods that are expected to mutate the collection: `push`, `pop`, `unshift`, `shift`, `fill`.

_* Despite feature parity, the type of the collection isn't interchangeable with the array given some methods are returning a collection instead of array, for example `map`._

Instantiating a new collection is easy as:
```js
import { Collection, collect } from '@upfrontjs/framework'
Expand Down
Loading

0 comments on commit fa29d20

Please sign in to comment.