Skip to content

Commit 622b22f

Browse files
authored
feat: upstream release/0.x into main (#182)
## Docs * docs: updated pagination recipe * docs: corrected pagination example * The exists method has taken the `_lastSyncedAt` property into account and here it was not set before * docs: adjust wording in doc blocks * docs(services): noted `handleError` is now async * docs(query-builder): added documentation for `resetQueryParameters` * docs: fixed grammar mistakes * docs(factory): updated examples to be in-line with mocks * docs(api-calls): updated methods to preferred uppercase * docs: updated recipe to use the new typing * docs(timestamps): updated attribute name getter method names * Linked: 114176e * docs: fixed typo in test name * docs: updated pagination recipe to handle model constructors * docs(api-calls): updated to uppercase method names * Updated all method argument to `call` to uppercase for consistency * docs: fixed examples * docs(api-calls): updated method name to uppercase * change missed from 03486f1 ## Perf * perf(query-builder): simplified withs deduplication ## Feature * feat(services): made handleError async on ApiResponseHandler * feat(api-calls): added default type argument * While this is invalid, from the outside TS still figures the current `this` correctly * feat(query-builder): changed `resetQueryParameters` access to public * feat(api-calls): export `Method` type * feat(services): widened request type on ApiResponse * feat(services): added HEAD request handling * feat(services): improve typings of services call and handle method * feat(attributes): improved `Attributes` type inference * Resolves #154 * feat(attributes): improved `only` method's typing * feat(attributes): improved `except` typing * feat(attributes): improved typings on attribute management methods * Following have been updated: `getOriginal` `getRawOriginal` `getChanges` `getDeletedAttributes` `getNewAttributes` * feat: updated static methods returning models to infer type from `this` * feat: updated some static inference to not use error ignoring * Updates related to: 63ed0be * feat(model): added `create` method * This allows to constructing a model more fluently ## Fix * fix(model): updated missed distinctBy in the clone method * fix(attributes): fixed casting methods typing to accept mixed values * fix(api-calls): pass the response attributes to the constructor * This also cleans up the code a little * fix(services): normalised fetch methods * `patch` could failed with lowercase method name * fix(api-calls): updated typing to be inclusive of each other on `get` * fix(relations): fixed `addRelation` typing to accept subclass of a model * fix: fixed outstanding eslint and typing errors * fix(model-collection): fixed wrong typing of `toJSON` * Issue introduced in b4d6db4 * fix(**BREAKING CHANGE**): renamed timestamp name methods * Renamed the following: `getDeletedAtColumn` -> `getDeletedAtName` `getCreatedAtColumn` -> `getCreatedAtName` `getUpdatedAtColumn` -> `getUpdatedAtName` ## Test * test(timestamps): tested `restore` accepting server deletedAt value * test: added initial baseEndpoint in testing * test(services): added missing test updates * Updates missed from a07b22a * test(attributes): fixed failing guarding tests * test(services): added missing test updates * Updates missed from a07b22a * test(relations): updated testing to use the new typings * test(model): updated to use new typings * test(api-calls): added missing data unwrap test * test(api-calls): added missing endpoint getter test ## Refactor * refactor(services): simplified `handleError` in response handler * refactor(attributes): simplified `getAttribute` override typing * refactor(factory): update return types of mock factories * refactor(model-collection): updated `toJSON` typing * updated `toJSON` typing to track the model's `toJSON` ## Chore * chore: incremented package version * chore: added new eslint rule * chore: moved todos into github * chore(deps): updated non-breaking dependencies * @commitlint/prompt-cli * @types/uuid * @typescript-eslint/eslint-plugin * @typescript-eslint/parser * commitlint * eslint * eslint-plugin-jest * lint-staged * qs * rollup * typedoc * typescript * chore(deps-dev): updated breaking change dependencies * semantic-release ## Style * style: fixed eslint issues ## Continuous Integration * ci: added matrix values explanation
1 parent 8620cf1 commit 622b22f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+26342
-26761
lines changed

.eslintrc.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = {
4040
"max-len": ["warn", 120],
4141
"eqeqeq": "error",
4242
"no-restricted-imports": "off",
43+
"lines-between-class-members": "off",
4344

4445
// https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules
4546
'@typescript-eslint/object-curly-spacing': ['warn', 'always'],
@@ -87,6 +88,7 @@ module.exports = {
8788
"@typescript-eslint/no-confusing-void-expression": "off",
8889
"@typescript-eslint/array-type": "warn",
8990
"@typescript-eslint/prefer-for-of": "off",
90-
"@typescript-eslint/no-restricted-imports": "off"
91+
"@typescript-eslint/no-restricted-imports": "off",
92+
"@typescript-eslint/lines-between-class-members": ["error"],
9193
}
9294
}

.github/workflows/test.yml

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ jobs:
2020
runs-on: ubuntu-latest
2121
strategy:
2222
matrix:
23+
# current and active LTS
2324
node: [ 16, 17 ]
2425
steps:
2526
- uses: actions/checkout@v2

TODO

-3
This file was deleted.

docs/calliope/api-calls.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -91,14 +91,14 @@ user.delete({ attribute: 1 });
9191
<Badge text="async" type="warning"/>
9292
<Badge text="advanced" type="warning"/>
9393

94-
The `call` method is what powers the rest of the api calls on the model. It takes one argument and two optional arguments. The first argument is the method name which is one of `'delete' | 'get' | 'patch' | 'post' | 'put'`. The second is the data to send along with the request. and the third is any custom headers in an object format to be sent along. After the request the [resetEndpoint](#resetendpoint) will be called and all [query parameters](./query-building.md) will be reset.
94+
The `call` method is what powers the rest of the api calls on the model. It takes one argument and two optional arguments. The first argument is the method name which is one of `'DELETE' | 'GET' | 'PATCH' | 'POST' | 'PUT' | 'HEAD'`. The second is the data to send along with the request. and the third is any custom headers in an object format to be sent along. After the request the [resetEndpoint](#resetendpoint) will be called and all [query parameters](./query-building.md) will be reset.
9595

9696
```ts
9797
import User from '@Models/User';
9898

9999
const user = new User();
100100

101-
await user.call('get', { query: 'value' }); // GET your-api.com/users?query=value
101+
await user.call('GET', { query: 'value' }); // GET your-api.com/users?query=value
102102
```
103103

104104
### Endpoint manipulation

docs/calliope/attributes.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export default class User extends Model {
5050
**The following cast types are available:**
5151

5252
::: warning
53-
If the values cannot be casted, an error will be thrown.
53+
If the values cannot be cast, an error will be thrown.
5454
:::
5555
#### `'boolean'`
5656
Casts the values to boolean. It does not evaluate values as truthy or falsy, it expects the numeric `0, 1`, booleans or `'true', 'false'` in any casing. This is useful to parse the data from the back-end.
@@ -409,7 +409,7 @@ user.name; // 'Jane Doe'
409409

410410
When setting an attribute following priority will apply to value:
411411
- If exists use the [mutator](#mutatorsaccessors) to set the attribute.
412-
- If [cast](#casting) defined, use the casted value to set the attribute.
412+
- If [cast](#casting) defined, use the cast value to set the attribute.
413413
- If the `key` argument is a defined relation and the `value` argument is a valid relation value, set the relation value.
414414
- Otherwise, just set the attribute on the model.
415415

@@ -428,7 +428,7 @@ Optionally the method takes a second argument which is returned if the key canno
428428
This method is internally used when accessing attributes on the model like in the above example. The model will resolve the value in the following order:
429429

430430
- If the attribute exists and has an [accessor](#mutatorsaccessors), return the accessor's value.
431-
- If the attribute exists, and a cast has been defined, return the casted value.
431+
- If the attribute exists, and a cast has been defined, return the cast value.
432432
- If the given key is a relation's name, and the relation [has been loaded](./relationships.md#relationloaded), return the relation.
433433
- If the key is a property of the model, and it's a function, return the default value.
434434
- If the key is a property of the model, return its value.

docs/calliope/query-building.md

+14-2
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,7 @@ User.orderByDesc('column');
348348

349349
#### latest
350350

351-
The `latest` method is an alias of the [orderBy](#orderby) method using a descending order. You may optionally specify which column to order by with the default being the result of the [getCreatedAtColumn](./timestamps.md#getcreatedatcolumn) method.
351+
The `latest` method is an alias of the [orderBy](#orderby) method using a descending order. You may optionally specify which column to order by with the default being the result of the [getCreatedAtName](./timestamps.md#getcreatedatname) method.
352352

353353
```js
354354
import User from '@Models/User';
@@ -359,7 +359,7 @@ User.latest('signed_up_at');
359359

360360
#### oldest
361361

362-
The `oldest` method is an alias of the [orderBy](#orderby) method using an ascending order. You may optionally specify which column to order by with the default being the result of the [getCreatedAtColumn](./timestamps.md#getcreatedatcolumn) method.
362+
The `oldest` method is an alias of the [orderBy](#orderby) method using an ascending order. You may optionally specify which column to order by with the default being the result of the [getCreatedAtName](./timestamps.md#getcreatedatname) method.
363363

364364
```js
365365
import User from '@Models/User';
@@ -399,6 +399,18 @@ import User from '@Models/User';
399399
User.newQuery(); // The query builder
400400
```
401401

402+
#### resetQueryParameters
403+
404+
The `resetQueryParameters` is a method that clears all previously set query parameters.
405+
406+
```js
407+
import User from '@Models/User';
408+
409+
const user = new User();
410+
void user.where('columns', 'value').get(); // would send the where query
411+
void user.where('columns', 'value').resetQueryParameters().get(); // would NOT send any query
412+
```
413+
402414
## Properties
403415

404416
#### withRelations

docs/calliope/readme.md

+3-3
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ This will typehint keys on the model when accessing the above keys like `user.ag
7171

7272
#### primaryKey
7373

74-
The `primaryKey` is a getter of the column name which is used to identify your model. The default value is `'id'`.
74+
The `primaryKey` is a getter of the attribute name which is used to identify your model. The default value is `'id'`.
7575

7676
```js
7777
// User.js
@@ -147,12 +147,12 @@ import User from '@Models/User';
147147
const user = User.factory().create();
148148
user.getKey(); // 1
149149
user.name; // 'the name'
150-
user.getAttribute(user.getCreatedAtColumn()); // Date instance
150+
user.getAttribute(user.getCreatedAtName()); // Date instance
151151

152152
const userCopy = user.replicate();
153153
userCopy.getKey(); // undefined
154154
userCopy.name; // 'the name'
155-
userCopy.getAttribute(userCopy.getCreatedAtColumn()); // undefined
155+
userCopy.getAttribute(userCopy.getCreatedAtName()); // undefined
156156
```
157157

158158
#### clone

docs/calliope/relationships.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ const userComments = await user.$comments().get();
114114
```
115115

116116
::: tip
117-
[hasMany](#hasmany) and [hasOne](#hasone) methods also allows us to create related resources while automatically setting the related column value.
117+
[hasMany](#hasmany) and [hasOne](#hasone) methods also allows us to create related resources while automatically setting the related attribute value.
118118

119119
```ts
120120
// User.ts

docs/calliope/timestamps.md

+14-14
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,26 @@ Timestamps are a feature of the model used for tracking changes on your entities
99
#### createdAt
1010

1111
The `createdAt` is a static property on the model. The default value is `'createdAt'`. You may over ride this if the expected timestamp attribute is named differently.
12-
The letter casing is no concern here as [getCreatedAtColumn](#getcreatedatcolumn) will update it to the correct casing.
12+
The letter casing is no concern here as [getCreatedAtName](#getcreatedatname) will update it to the correct casing.
1313

1414
#### updatedAt
1515

1616
The `updatedAt` is a static property on the model. The default value is `'updatedAt'`. You may over ride this if the expected timestamp attribute is named differently.
17-
The letter casing is no concern here as [getUpdatedAtColumn](#getupdatedatcolumn) will update it to the correct casing.
17+
The letter casing is no concern here as [getUpdatedAtName](#getupdatedatname) will update it to the correct casing.
1818

1919
#### timestamps
2020

2121
The `timestamps` is a read only attribute that signifies whether the model uses timestamps or not. The default value is `true`;
2222

2323
### Methods
2424

25-
#### getCreatedAtColumn
25+
#### getCreatedAtName
2626

27-
The `getCreatedAtColumn` method returns the value of the static [createdAt](#createdat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
27+
The `getCreatedAtName` method returns the value of the static [createdAt](#createdat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
2828

29-
#### getUpdatedAtColumn
29+
#### getUpdatedAtName
3030

31-
The `getUpdatedAtColumn` method returns the value of the static [updatedAt](#updatedat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
31+
The `getUpdatedAtName` method returns the value of the static [updatedAt](#updatedat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
3232

3333
#### usesTimestamps
3434

@@ -37,7 +37,7 @@ The `usesTimestamps` method returns the value of the [timestamps](#timestamps-3)
3737
#### touch
3838
<Badge text="async" type="warning"/>
3939

40-
The `touch` method sends a `PATCH` request with the new [updatedAt](#getupdatedatcolumn) attribute value. It updates the attribute from the response data.
40+
The `touch` method sends a `PATCH` request with the new [updatedAt](#getupdatedatname) attribute value. It updates the attribute from the response data.
4141

4242
::: tip
4343
Your backend should probably not trust this input, but generate its own timestamp.
@@ -46,7 +46,7 @@ Your backend should probably not trust this input, but generate its own timestam
4646
#### freshTimestamps
4747
<Badge text="async" type="warning"/>
4848

49-
The `freshTimestamps` method sends `GET` request [selecting](./query-building.md#select) only the [createdAt](#getcreatedatcolumn) and [updatedAt](#getupdatedatcolumn) attributes, which are updated from the response on success.
49+
The `freshTimestamps` method sends `GET` request [selecting](./query-building.md#select) only the [createdAt](#getcreatedatname) and [updatedAt](#getupdatedatname) attributes, which are updated from the response on success.
5050

5151
## Soft Deletes
5252

@@ -55,21 +55,21 @@ The `freshTimestamps` method sends `GET` request [selecting](./query-building.md
5555
#### deletedAt
5656

5757
The `deletedAt` is a static property on the model. The default value is `'deletedAt'`. You may over ride this if the expected timestamp attribute is named differently.
58-
The letter casing is no concern here as [getDeletedAtColumn](#getdeletedatcolumn) will update it to the correct casing.
58+
The letter casing is no concern here as [getDeletedAtName](#getdeletedatname) will update it to the correct casing.
5959

6060
#### softDeletes
6161

6262
The `softDeletes` is a read only attribute that signifies whether the model uses soft deleting or not. The default value is `true`;
6363

6464
#### trashed
6565

66-
The `trashed` is a getter property that returns a boolean depending on whether the model has the [deletedAt](#getdeletedatcolumn) set to a truthy value.
66+
The `trashed` is a getter property that returns a boolean depending on whether the model has the [deletedAt](#getdeletedatname) set to a truthy value.
6767

6868
### Methods
6969

70-
#### getDeletedAtColumn
70+
#### getDeletedAtName
7171

72-
The `getDeletedAtColumn` method returns the value of the static [deletedAt](#deletedat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
72+
The `getDeletedAtName` method returns the value of the static [deletedAt](#deletedat) value with the letter casing set to the given [attributeCasing](./attributes.md#attributecasing).
7373

7474
#### usesSoftDeletes
7575

@@ -80,7 +80,7 @@ The `usesSoftDeletes` method returns the value of the [softDeletes](#softdeletes
8080

8181
The `delete` method is an extension of the api calling method [delete](./api-calls.md#delete). If the model is not [using softDeletes](#usessoftdeletes) the logic will fall back to the original [delete](./api-calls.md#delete) method's logic therefore, method accepts an optional object argument which will be sent along on the request in the body.
8282

83-
This method sends a `DELETE` request with the new [deletedAt](#getdeletedatcolumn) attribute value. It updates the attribute from the response data.
83+
This method sends a `DELETE` request with the new [deletedAt](#getdeletedatname) attribute value. It updates the attribute from the response data.
8484

8585
::: tip
8686
Your backend should probably not trust this input, but generate its own timestamp.
@@ -89,6 +89,6 @@ Your backend should probably not trust this input, but generate its own timestam
8989
#### restore
9090
<Badge text="async" type="warning"/>
9191

92-
The `restore` methods sends a `PATCH` request with the nullified [deletedAt](#getdeletedatcolumn) attribute value. It updates the attribute to `null` on successful request.
92+
The `restore` methods sends a `PATCH` request with the nullified [deletedAt](#getdeletedatname) attribute value. It updates the attribute to `null` on successful request.
9393

9494

docs/cookbook.md

+73-25
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ const form = new FormData;
129129
const response = await handler.handle(
130130
api.call(
131131
config.get('baseEndPoint').finish('/') + 'form',
132-
'post',
132+
'POST',
133133
form,
134134
{ 'X-Requested-With': 'Upfront' },
135135
{ query: { parameters: 'to encode' } }
@@ -175,7 +175,7 @@ const myFirstUser = await User.where('name', 'like', '%me%').first()
175175
Extending/overwriting the model should not be a daunting task. If we wanted we could add an extra method to send data to the server. In this example we add a new field on the sent data which is called `appends` and we're expecting the server to append additional information on the model response data.
176176

177177
```ts
178-
import type { FormatsQueryParameters, QueryParams } from '@upfrontjs/framework';
178+
import type { FormatsQueryParameters, QueryParams, StaticToThis } from '@upfrontjs/framework';
179179
import { Model as BaseModel } from '@upfrontjs/framework';
180180

181181
export default class Model extends BaseModel implements FormatsQueryParameters {
@@ -185,9 +185,9 @@ export default class Model extends BaseModel implements FormatsQueryParameters {
185185
this.appends.push(name);
186186
return this;
187187
}
188-
189-
public static append<T extends Model>(name: string): T {
190-
this.newQuery<T>().append(name);
188+
189+
public static append<T extends StaticToThis<Model>>(this: T, name: string): T['prototype'] {
190+
this.newQuery().append(name);
191191
}
192192

193193
public withoutAppend(name: string): this {
@@ -217,50 +217,98 @@ Now if our other models extend our own model they will have the option to set th
217217
While it's nice to be able to [paginate locally](./helpers/pagination.md) it might not be desired to get too much data upfront. In this case a pagination can be implemented that will only get the pages in question on an explicit request. Of course, you might change the typings and the implementation to fit your needs.
218218

219219
```ts
220-
// utils.ts
221-
import type { Model, ModelCollection } from '@upfrontjs/framework';
222-
import User from '@models/User';
220+
// paginator.ts
221+
import type { Attributes, Model } from '@upfrontjs/framework';
222+
import { ModelCollection } from '@upfrontjs/framework';
223223

224-
interface MyJsonApiResponse {
225-
data: Attributes[];
224+
interface PaginatedApiResponse<T = Attributes> {
225+
data: T[];
226226
links: {
227227
first: string;
228228
last: string;
229-
next: string;
229+
prev: string | null;
230+
next: string | null;
230231
};
231232
meta: {
232233
current_page: number;
234+
/**
235+
* From all the existing records this is where the current items start from.
236+
*/
233237
from: number;
238+
/**
239+
* From all the existing records this is where the current items go to.
240+
*/
234241
to: number;
235242
last_page: number;
243+
/**
244+
* String representation of a number.
245+
*/
246+
per_page: string;
247+
links: {
248+
url: string | null;
249+
label: string;
250+
active: boolean;
251+
}[];
252+
/**
253+
* Total number of records.
254+
*/
236255
total: number;
237256
path: string;
238257
};
239258
}
240259

241-
interface PaginatedModels<T extends Model> {
260+
export interface PaginatedModels<T extends Model> {
242261
data: ModelCollection<T>;
243-
next: () => Promise<PaginatedModels<T>>;
244-
previous: () => Promise<PaginatedModels<T>>;
245-
page: (page: number) => Promise<PaginatedModels<T>>;
246-
meta: MyJsonApiResponse['meta'];
262+
next: () => Promise<PaginatedModels<T> | undefined>;
263+
previous: () => Promise<PaginatedModels<T> | undefined>;
264+
page: (page: number) => Promise<PaginatedModels<T> | undefined>;
265+
hasNext: boolean;
266+
hasPrevious: boolean;
267+
from: PaginatedApiResponse['meta']['from'];
268+
to: PaginatedApiResponse['meta']['to'];
269+
total: PaginatedApiResponse['meta']['total'];
247270
}
248271

249-
export async function paginateModels<T extends Model>(builder: T, page = 1, limit = 25): Promise<PaginatedModels<T>> {
250-
const response = await builder.clone().limit(limit).page(page).call<MyJsonApiResponse>('get');
251-
const modelCollection = new ModelCollection<T>(response!.data.map(attributes => builder.new(attributes)));
252-
// or some other custom logic where the response `meta` and `links` or other keys (if any) are taken into account
272+
async function paginatedModels<T extends Model>(
273+
builder: T | (new() => T),
274+
page = 1,
275+
limit = 25
276+
): Promise<PaginatedModels<T>> {
277+
const instance = !(builder instanceof Model) ? new builder() : builder.clone();
278+
279+
const response = (await instance.limit(limit).page(page).call<PaginatedApiResponse<Attributes<T>>>('GET'))!;
280+
const modelCollection = new ModelCollection<T>(response.data.map(attributes => {
281+
return instance
282+
.new(attributes)
283+
// @ts-expect-error - Protected internal method required for correct .exists detection
284+
.setLastSyncedAt();
285+
}));
253286

254287
return {
255288
data: modelCollection,
256-
next: async () => paginateModels(builder, page + 1, limit),
257-
previous: async () => paginateModels(builder, page - 1, limit),
258-
page: async (pageNumber: number) => paginateModels(builder, pageNumber, limit),
259-
meta: response!.meta
260-
// any other keys here like that you want to have access to
289+
next: async () => {
290+
if (!response.links.next) return;
291+
return paginatedModels(instance, page + 1, limit);
292+
},
293+
previous: async () => {
294+
if (!response.links.prev) return;
295+
return paginatedModels(instance, page - 1, limit);
296+
},
297+
page: async (pageNumber: number) => {
298+
if (pageNumber > response.meta.last_page || pageNumber < 0) return;
299+
return paginatedModels(instance, pageNumber, limit);
300+
},
301+
from: response.meta.from,
302+
to: response.meta.to,
303+
total: response.meta.total,
304+
hasNext: !!response.links.next,
305+
hasPrevious: !!response.links.prev
261306
};
262307
}
263308

309+
export default paginator;
310+
311+
// script.ts
264312
// paginate users where column has the value of 1
265313
const firstPage = await paginateModels(User.where('column', 1));
266314
const secondPage = await firstPage.next();

docs/services/api-response-handler.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ The `handleSuccess` method attempts to parse the [`ApiResponse`](./readme.md#api
1212

1313
#### handleError
1414

15-
The `handleError` method throws the captured error from [handleSuccess](#handlesuccess).
15+
The `handleError` is an async method that throws the captured error from [handleSuccess](#handlesuccess).
1616

1717
#### handleFinally
1818

0 commit comments

Comments
 (0)