From 4a5837b85bbbb5ca4fcab553cbbf42f0b0d53b0c Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Tue, 31 Jan 2023 02:13:03 +0100 Subject: [PATCH 1/8] Start to tests --- tests/integration/batch.test.ts | 38 +++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/integration/batch.test.ts diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts new file mode 100644 index 0000000..5053052 --- /dev/null +++ b/tests/integration/batch.test.ts @@ -0,0 +1,38 @@ +import makeServer from './drivers/default/server'; +import Post from '../stubs/models/post'; +import { Batch } from '../../src/batch'; + +let server: any; + +beforeEach(() => { + server = makeServer(); +}); + +afterEach(() => { + server.shutdown(); +}); + +describe('Batch tests', () => { + test('saving a couple of models', async () => { + await Batch.store( + [ + new Post({title: "item1", id: 10, created_at: "", updated_at: ""}), + new Post({title: "item2", id: 12, created_at: "", updated_at: ""}), + ] + ) + }); + + test('updating a couple of models', async () => { + const post1 = await Post.$query().find(1); + const post2 = await Post.$query().find(2); + + post1.$attributes.title = "new title"; + post2.$attributes.title = "newer title"; + + await Batch.update( + [ + post1, post2 + ] + ) + }); +}); From 40535981caba2bddb116506cdfd7c81d6bc46f5f Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Tue, 31 Jan 2023 02:13:56 +0100 Subject: [PATCH 2/8] Added batch start --- src/batch.ts | 110 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 src/batch.ts diff --git a/src/batch.ts b/src/batch.ts new file mode 100644 index 0000000..fd88cbd --- /dev/null +++ b/src/batch.ts @@ -0,0 +1,110 @@ +import { HttpMethod } from "./drivers/default/enums/httpMethod"; +import { Model } from "./model"; +import { Orion } from "./orion"; + +export class Batch +{ + private static $httpClient() { + const httpClient = Orion.makeHttpClient(Orion.getBaseUrl()); + + return httpClient; + } + + public static async store(items: M[]) + { + if (!items.length) + return; + + const client = this.$httpClient(); + const url = items[0].$resource(); + + const data = { + resources: items.map(x => x.$attributes) + }; + + return client.request( + `${url}/batch`, + HttpMethod.POST, + null, + data + ) + } + + public static async update(items: M[]) + { + if (!items.length) + return; + + const client = this.$httpClient(); + const url = items[0].$resource(); + + const data = { + resources: {} + }; + items.forEach((v, i) => data.resources[v.$getKey()] = v.$attributes); + + return client.request( + `${url}/batch`, + HttpMethod.PATCH, + null, + data + ) + } + + public static async delete(items: number[] | M[], baseUrl?: string) + { + const {ids, url} = this.getBatchIds(items); + const data = { + resources: ids + }; + const finalUrl = url || baseUrl; + const client = this.$httpClient(); + + + if (!finalUrl) + throw "BuilderError: url not found" + return client.request( + `${finalUrl}/batch`, + HttpMethod.DELETE, + data + ) + } + + + public static async restore(items: number[] | M[], baseUrl?: string) + { + const {ids, url} = this.getBatchIds(items); + const data = { + resources: ids + }; + const finalUrl = url || baseUrl; + const client = this.$httpClient(); + + + if (!finalUrl) + throw "BuilderError: url not found" + return client.request( + `${finalUrl}/batch/restore`, + HttpMethod.POST, + data + ) + } + + private static getBatchIds(items: number[] | M[]): {ids: unknown[], url: string | undefined} { + let foundUrl: string | undefined = undefined; + + let ids = items.map((x: number | M) => { + // also find the url while we're at it + if (typeof (x) == 'object' && x.$resource() && foundUrl == undefined) { + foundUrl = x.$resource(); + } + + return typeof(x) == 'number' ? x : x.$attributes[x.$getKeyName()] + }) + + return { + ids, + url: foundUrl, + } + } +} \ No newline at end of file From fcc2f17e6e4a34c00846835284f8fdccbaf494be Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Mon, 6 Feb 2023 21:59:58 +0100 Subject: [PATCH 3/8] Added tests for batch --- tests/integration/batch.test.ts | 105 +++++++++++++++++--- tests/integration/drivers/default/server.ts | 68 +++++++++++++ 2 files changed, 158 insertions(+), 15 deletions(-) diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts index 5053052..1c02528 100644 --- a/tests/integration/batch.test.ts +++ b/tests/integration/batch.test.ts @@ -1,11 +1,13 @@ import makeServer from './drivers/default/server'; import Post from '../stubs/models/post'; import { Batch } from '../../src/batch'; +import { Orion } from '../../src/orion'; let server: any; beforeEach(() => { server = makeServer(); + Orion.setBaseUrl("https://api-mock.test/api") }); afterEach(() => { @@ -14,25 +16,98 @@ afterEach(() => { describe('Batch tests', () => { test('saving a couple of models', async () => { - await Batch.store( - [ - new Post({title: "item1", id: 10, created_at: "", updated_at: ""}), - new Post({title: "item2", id: 12, created_at: "", updated_at: ""}), - ] - ) + const posts = [ + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + + const res = await Batch.store(posts); + + expect(server.schema.posts.all()).toHaveLength(2); + expect(server.schema.posts.find('1').attrs.title).toBe("First") + expect(server.schema.posts.find('2').attrs.title).toBe("Second") + expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) + expect(server.schema.posts.find('1').attrs.created_at).toEqual(res[0].$attributes.created_at) + expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) + expect(server.schema.posts.find('2').attrs.created_at).toEqual(res[1].$attributes.created_at) }); test('updating a couple of models', async () => { - const post1 = await Post.$query().find(1); - const post2 = await Post.$query().find(2); + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + res[0].$attributes.title = "NewFirst"; + res[1].$attributes.title = "NewSecond"; + + res = await Batch.update([res[0],res[1]]); + + expect(res).toHaveLength(2); + expect(server.schema.posts.find('1').attrs.title).toBe("NewFirst") + expect(server.schema.posts.find('2').attrs.title).toBe("NewSecond") + expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) + expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) + expect(server.schema.posts.find('3').attrs.title).toEqual("Third"); + + }); + + test('deleting a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + let ModelDelete = await Batch.delete([res[1]]); + let idDelete = await Batch.delete([3], new Post); + + expect(server.schema.posts.find('1').attrs.deleted_at).toBeUndefined(); + expect(server.schema.posts.find('2').attrs.deleted_at).toBeDefined(); + expect(server.schema.posts.find('3').attrs.deleted_at).toBeDefined(); + expect(server.schema.posts.find('2').attrs.title).toEqual(ModelDelete[0].$attributes.title) + expect(server.schema.posts.find('3').attrs.title).toEqual(idDelete[0].$attributes.title) + + + }); + + test('restoring a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + // delete ID 2 & 3 + let ModelDelete = await Batch.delete([res[1]]); + let idDelete = await Batch.delete([3], new Post); + + res = await Batch.restore([...ModelDelete, ...idDelete]); + + expect(server.schema.posts.find('1').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('2').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('3').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('2').attrs.title).toEqual(res[0].$attributes.title); + expect(server.schema.posts.find('3').attrs.title).toEqual(res[1].$attributes.title); - post1.$attributes.title = "new title"; - post2.$attributes.title = "newer title"; - await Batch.update( - [ - post1, post2 - ] - ) }); }); diff --git a/tests/integration/drivers/default/server.ts b/tests/integration/drivers/default/server.ts index 9e562c5..3480279 100644 --- a/tests/integration/drivers/default/server.ts +++ b/tests/integration/drivers/default/server.ts @@ -129,6 +129,74 @@ export default function makeServer() { updated: [request.params.tag_id], }; }); + + this.post('/api/posts/batch', (schema: any, request) => { + const body: { + resources: any[] + } = JSON.parse(request.requestBody); + + const rval: any[] = []; + for (let i = 0; i < body.resources.length; i++) { + rval.push(schema.posts.create(body.resources[i])); + } + + return {data: rval}; + }) + + this.patch('/api/posts/batch', (schema: any, request) => { + const body: { + resources: object + } = JSON.parse(request.requestBody); + + const rval: any[] = []; + for (const key in body.resources) { + const attrs = body.resources[key]; + + const post = schema.posts.find(key); + + + rval.push(post.update(attrs)); + } + + return {data: rval}; + }) + + this.delete('/api/posts/batch', (schema: any, request) => { + const body: { + resources: number[] + } = JSON.parse(request.requestBody); + + const rval: any[] = []; + for (let i = 0; i < body.resources.length; i++) { + const id = body.resources[i]; + const post = schema.posts.find(id); + + post.update({ deleted_at: '2021-01-01' }); + + rval.push(post); + } + + return {data: rval}; + }) + + this.post('/api/posts/batch/restore', (schema: any, request) => { + const body: { + resources: number[] + } = JSON.parse(request.requestBody); + + const rval: any[] = []; + + for (let i = 0; i < body.resources.length; i++) { + const id = body.resources[i]; + const post = schema.posts.find(id); + + post.update({ deleted_at: null }); + + rval.push(post); + } + + return {data: rval}; + }) }, }); } From a30bbddc6158a61945179e7647448092439cd1bc Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Mon, 6 Feb 2023 22:00:59 +0100 Subject: [PATCH 4/8] Added returns to commands --- src/batch.ts | 304 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 195 insertions(+), 109 deletions(-) diff --git a/src/batch.ts b/src/batch.ts index fd88cbd..a1074da 100644 --- a/src/batch.ts +++ b/src/batch.ts @@ -1,110 +1,196 @@ -import { HttpMethod } from "./drivers/default/enums/httpMethod"; -import { Model } from "./model"; -import { Orion } from "./orion"; - -export class Batch -{ - private static $httpClient() { - const httpClient = Orion.makeHttpClient(Orion.getBaseUrl()); - - return httpClient; - } - - public static async store(items: M[]) - { - if (!items.length) - return; - - const client = this.$httpClient(); - const url = items[0].$resource(); - - const data = { - resources: items.map(x => x.$attributes) - }; - - return client.request( - `${url}/batch`, - HttpMethod.POST, - null, - data - ) - } - - public static async update(items: M[]) - { - if (!items.length) - return; - - const client = this.$httpClient(); - const url = items[0].$resource(); - - const data = { - resources: {} - }; - items.forEach((v, i) => data.resources[v.$getKey()] = v.$attributes); - - return client.request( - `${url}/batch`, - HttpMethod.PATCH, - null, - data - ) - } - - public static async delete(items: number[] | M[], baseUrl?: string) - { - const {ids, url} = this.getBatchIds(items); - const data = { - resources: ids - }; - const finalUrl = url || baseUrl; - const client = this.$httpClient(); - - - if (!finalUrl) - throw "BuilderError: url not found" - return client.request( - `${finalUrl}/batch`, - HttpMethod.DELETE, - data - ) - } - - - public static async restore(items: number[] | M[], baseUrl?: string) - { - const {ids, url} = this.getBatchIds(items); - const data = { - resources: ids - }; - const finalUrl = url || baseUrl; - const client = this.$httpClient(); - - - if (!finalUrl) - throw "BuilderError: url not found" - return client.request( - `${finalUrl}/batch/restore`, - HttpMethod.POST, - data - ) - } - - private static getBatchIds(items: number[] | M[]): {ids: unknown[], url: string | undefined} { - let foundUrl: string | undefined = undefined; - - let ids = items.map((x: number | M) => { - // also find the url while we're at it - if (typeof (x) == 'object' && x.$resource() && foundUrl == undefined) { - foundUrl = x.$resource(); - } - - return typeof(x) == 'number' ? x : x.$attributes[x.$getKeyName()] - }) - - return { - ids, - url: foundUrl, - } - } +import { HttpMethod } from "./drivers/default/enums/httpMethod"; +import { Model } from "./model"; +import { Orion } from "./orion"; +import { ExtractModelAttributesType } from "./types/extractModelAttributesType"; +import { ExtractModelAllAttributesType } from "./types/extractModelAllAttributesType"; +import { ExtractModelRelationsType } from "./types/extractModelRelationsType"; +import { ModelConstructor } from "./contracts/modelConstructor"; +import { QueryBuilder } from "./drivers/default/builders/queryBuilder"; +import { ExtractModelPersistedAttributesType } from "./types/extractModelPersistedAttributesType"; +import { ExtractModelKeyType } from "./types/extractModelKeyType"; +import { UrlBuilder } from "./builders/urlBuilder"; + +type HydrateAttributes = ExtractModelAttributesType & ExtractModelPersistedAttributesType & ExtractModelRelationsType; + + +export class Batch +{ + private static $httpClient( + modelConstructor: ModelConstructor + ) { + let baseUrl = '' + if (Orion.getBaseUrl()) { + baseUrl = Orion.getBaseUrl(); + } else { + baseUrl = UrlBuilder.getResourceBaseUrl(modelConstructor); + } + const httpClient = Orion.makeHttpClient(baseUrl); + + return httpClient; + } + + public static async store< + M extends Model, + Attributes = ExtractModelAttributesType, + PersistedAttributes = ExtractModelPersistedAttributesType, + Relations = ExtractModelRelationsType, + AllAttributes = Attributes & PersistedAttributes + >(items: M[]): Promise + { + if (!items.length) + return []; + + const client = this.$httpClient(items[0].constructor as ModelConstructor); + const url = items[0].$resource(); + + const data = { + resources: items.map(x => x.$attributes) + }; + + const response = await client.request<{ data: Array< AllAttributes & Relations > }>( + `${url}/batch`, + HttpMethod.POST, + null, + data + ); + + return response.data.data.map((attributes: AllAttributes & Relations) => { + const model: M = new (items[0].constructor as ModelConstructor)(); + return (model.$query() as QueryBuilder) + .hydrate(attributes as HydrateAttributes, response) + }); + } + + public static async update< + M extends Model, + Attributes = ExtractModelAttributesType, + PersistedAttributes = ExtractModelPersistedAttributesType, + Relations = ExtractModelRelationsType, + AllAttributes = Attributes & PersistedAttributes + >(items: M[]): Promise + { + if (!items.length) + return []; + + const client = this.$httpClient(items[0].constructor as ModelConstructor); + const url = items[0].$resource(); + + const data = { + resources: {} + }; + items.forEach((v, i) => data.resources[v.$getKey()] = v.$attributes); + + const response = await client.request<{ data: Array< AllAttributes & Relations > }>( + `${url}/batch`, + HttpMethod.PATCH, + null, + data + ) + + return response.data.data.map((attributes: AllAttributes & Relations) => { + const model: M = new (items[0].constructor as ModelConstructor)(); + return (model.$query() as QueryBuilder) + .hydrate(attributes as HydrateAttributes, response) + }); + } + + + + public static async delete(items: M[]): Promise; + public static async delete(items: number[], target: M): Promise; + public static async delete< + M extends Model, + Attributes = ExtractModelAttributesType, + PersistedAttributes = ExtractModelPersistedAttributesType, + Relations = ExtractModelRelationsType, + AllAttributes = Attributes & PersistedAttributes, + >(items: number[] | M[], target?: M): Promise + { + const {ids, url, isModel} = this.getBatchIds(items); + const finalUrl = url || target?.$resource(); + if (!finalUrl) + throw "BuilderError: url not found" + + const data = { + resources: ids + }; + + const client = this.$httpClient(items[0].constructor as ModelConstructor); + const response = await client.request<{ data: Array< AllAttributes & Relations > }>( + `${finalUrl}/batch`, + HttpMethod.DELETE, + null, + data + ); + + const modelConstructor = (isModel ? items[0].constructor : target?.constructor) as ModelConstructor + if (!modelConstructor) + return []; + + return response.data.data.map((attributes: AllAttributes & Relations) => { + const model: M = new modelConstructor(); + return (model.$query() as QueryBuilder) + .hydrate(attributes as HydrateAttributes, response) + }); + } + + + public static async restore(items: M[]): Promise; + public static async restore(items: number[], target: M): Promise; + public static async restore< + M extends Model, + Attributes = ExtractModelAttributesType, + PersistedAttributes = ExtractModelPersistedAttributesType, + Relations = ExtractModelRelationsType, + AllAttributes = Attributes & PersistedAttributes, + >(items: number[] | M[], target?: M): Promise + { + const {ids, url, isModel} = this.getBatchIds(items); + const data = { + resources: ids + }; + const finalUrl = url || target?.$resource(); + const client = this.$httpClient(items[0].constructor as ModelConstructor); + + + if (!finalUrl) + throw "BuilderError: url not found" + const response = await client.request<{ data: Array< AllAttributes & Relations > }>( + `${finalUrl}/batch/restore`, + HttpMethod.POST, + null, + data + ) + + const modelConstructor = (isModel ? items[0].constructor : target?.constructor) as ModelConstructor + if (!modelConstructor) + return []; + + return response.data.data.map((attributes: AllAttributes & Relations) => { + const model: M = new modelConstructor(); + return (model.$query() as QueryBuilder) + .hydrate(attributes as HydrateAttributes, response) + }); + } + + private static getBatchIds(items: number[] | M[]): {ids: unknown[], url: string | undefined, isModel: boolean} { + let foundUrl: string | undefined = undefined; + let isModel = false; + + let ids = items.map((x: number | M) => { + // also find the url while we're at it + if (typeof (x) == 'object' && x.$resource() && foundUrl == undefined) { + foundUrl = x.$resource(); + isModel = true; + } + + return typeof(x) == 'number' ? x : x.$attributes[x.$getKeyName()] + }) + + return { + ids, + url: foundUrl, + isModel + } + } } \ No newline at end of file From 418e2772f2fb1d710e82bf061685948c5e388467 Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Mon, 6 Feb 2023 22:03:05 +0100 Subject: [PATCH 5/8] Changed line ending --- tests/integration/batch.test.ts | 226 ++++++++++++++++---------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts index 1c02528..ecece51 100644 --- a/tests/integration/batch.test.ts +++ b/tests/integration/batch.test.ts @@ -1,113 +1,113 @@ -import makeServer from './drivers/default/server'; -import Post from '../stubs/models/post'; -import { Batch } from '../../src/batch'; -import { Orion } from '../../src/orion'; - -let server: any; - -beforeEach(() => { - server = makeServer(); - Orion.setBaseUrl("https://api-mock.test/api") -}); - -afterEach(() => { - server.shutdown(); -}); - -describe('Batch tests', () => { - test('saving a couple of models', async () => { - const posts = [ - new Post(), - new Post(), - ] - posts[0].$attributes.title = "First"; - posts[1].$attributes.title = "Second"; - - const res = await Batch.store(posts); - - expect(server.schema.posts.all()).toHaveLength(2); - expect(server.schema.posts.find('1').attrs.title).toBe("First") - expect(server.schema.posts.find('2').attrs.title).toBe("Second") - expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) - expect(server.schema.posts.find('1').attrs.created_at).toEqual(res[0].$attributes.created_at) - expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) - expect(server.schema.posts.find('2').attrs.created_at).toEqual(res[1].$attributes.created_at) - }); - - test('updating a couple of models', async () => { - const posts = [ - new Post(), - new Post(), - new Post(), - ] - posts[0].$attributes.title = "First"; - posts[1].$attributes.title = "Second"; - posts[2].$attributes.title = "Third"; - - let res = await Batch.store(posts); - - res[0].$attributes.title = "NewFirst"; - res[1].$attributes.title = "NewSecond"; - - res = await Batch.update([res[0],res[1]]); - - expect(res).toHaveLength(2); - expect(server.schema.posts.find('1').attrs.title).toBe("NewFirst") - expect(server.schema.posts.find('2').attrs.title).toBe("NewSecond") - expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) - expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) - expect(server.schema.posts.find('3').attrs.title).toEqual("Third"); - - }); - - test('deleting a couple of models', async () => { - const posts = [ - new Post(), - new Post(), - new Post(), - ] - posts[0].$attributes.title = "First"; - posts[1].$attributes.title = "Second"; - posts[2].$attributes.title = "Third"; - - let res = await Batch.store(posts); - - let ModelDelete = await Batch.delete([res[1]]); - let idDelete = await Batch.delete([3], new Post); - - expect(server.schema.posts.find('1').attrs.deleted_at).toBeUndefined(); - expect(server.schema.posts.find('2').attrs.deleted_at).toBeDefined(); - expect(server.schema.posts.find('3').attrs.deleted_at).toBeDefined(); - expect(server.schema.posts.find('2').attrs.title).toEqual(ModelDelete[0].$attributes.title) - expect(server.schema.posts.find('3').attrs.title).toEqual(idDelete[0].$attributes.title) - - - }); - - test('restoring a couple of models', async () => { - const posts = [ - new Post(), - new Post(), - new Post(), - ] - posts[0].$attributes.title = "First"; - posts[1].$attributes.title = "Second"; - posts[2].$attributes.title = "Third"; - - let res = await Batch.store(posts); - - // delete ID 2 & 3 - let ModelDelete = await Batch.delete([res[1]]); - let idDelete = await Batch.delete([3], new Post); - - res = await Batch.restore([...ModelDelete, ...idDelete]); - - expect(server.schema.posts.find('1').attrs.deleted_at).toBeFalsy(); - expect(server.schema.posts.find('2').attrs.deleted_at).toBeFalsy(); - expect(server.schema.posts.find('3').attrs.deleted_at).toBeFalsy(); - expect(server.schema.posts.find('2').attrs.title).toEqual(res[0].$attributes.title); - expect(server.schema.posts.find('3').attrs.title).toEqual(res[1].$attributes.title); - - - }); -}); +import makeServer from './drivers/default/server'; +import Post from '../stubs/models/post'; +import { Batch } from '../../src/batch'; +import { Orion } from '../../src/orion'; + +let server: any; + +beforeEach(() => { + server = makeServer(); + Orion.setBaseUrl("https://api-mock.test/api") +}); + +afterEach(() => { + server.shutdown(); +}); + +describe('Batch tests', () => { + test('saving a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + + const res = await Batch.store(posts); + + expect(server.schema.posts.all()).toHaveLength(2); + expect(server.schema.posts.find('1').attrs.title).toBe("First") + expect(server.schema.posts.find('2').attrs.title).toBe("Second") + expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) + expect(server.schema.posts.find('1').attrs.created_at).toEqual(res[0].$attributes.created_at) + expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) + expect(server.schema.posts.find('2').attrs.created_at).toEqual(res[1].$attributes.created_at) + }); + + test('updating a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + res[0].$attributes.title = "NewFirst"; + res[1].$attributes.title = "NewSecond"; + + res = await Batch.update([res[0],res[1]]); + + expect(res).toHaveLength(2); + expect(server.schema.posts.find('1').attrs.title).toBe("NewFirst") + expect(server.schema.posts.find('2').attrs.title).toBe("NewSecond") + expect(server.schema.posts.find('1').attrs.title).toEqual(res[0].$attributes.title) + expect(server.schema.posts.find('2').attrs.title).toEqual(res[1].$attributes.title) + expect(server.schema.posts.find('3').attrs.title).toEqual("Third"); + + }); + + test('deleting a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + let ModelDelete = await Batch.delete([res[1]]); + let idDelete = await Batch.delete([3], new Post); + + expect(server.schema.posts.find('1').attrs.deleted_at).toBeUndefined(); + expect(server.schema.posts.find('2').attrs.deleted_at).toBeDefined(); + expect(server.schema.posts.find('3').attrs.deleted_at).toBeDefined(); + expect(server.schema.posts.find('2').attrs.title).toEqual(ModelDelete[0].$attributes.title) + expect(server.schema.posts.find('3').attrs.title).toEqual(idDelete[0].$attributes.title) + + + }); + + test('restoring a couple of models', async () => { + const posts = [ + new Post(), + new Post(), + new Post(), + ] + posts[0].$attributes.title = "First"; + posts[1].$attributes.title = "Second"; + posts[2].$attributes.title = "Third"; + + let res = await Batch.store(posts); + + // delete ID 2 & 3 + let ModelDelete = await Batch.delete([res[1]]); + let idDelete = await Batch.delete([3], new Post); + + res = await Batch.restore([...ModelDelete, ...idDelete]); + + expect(server.schema.posts.find('1').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('2').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('3').attrs.deleted_at).toBeFalsy(); + expect(server.schema.posts.find('2').attrs.title).toEqual(res[0].$attributes.title); + expect(server.schema.posts.find('3').attrs.title).toEqual(res[1].$attributes.title); + + + }); +}); From 1f77f3e67f4d89cbfb24d90dd2afaa38920cb4e4 Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Mon, 6 Feb 2023 22:20:09 +0100 Subject: [PATCH 6/8] Fixed linter errors --- src/batch.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/batch.ts b/src/batch.ts index a1074da..a877ac6 100644 --- a/src/batch.ts +++ b/src/batch.ts @@ -2,12 +2,10 @@ import { HttpMethod } from "./drivers/default/enums/httpMethod"; import { Model } from "./model"; import { Orion } from "./orion"; import { ExtractModelAttributesType } from "./types/extractModelAttributesType"; -import { ExtractModelAllAttributesType } from "./types/extractModelAllAttributesType"; import { ExtractModelRelationsType } from "./types/extractModelRelationsType"; import { ModelConstructor } from "./contracts/modelConstructor"; import { QueryBuilder } from "./drivers/default/builders/queryBuilder"; import { ExtractModelPersistedAttributesType } from "./types/extractModelPersistedAttributesType"; -import { ExtractModelKeyType } from "./types/extractModelKeyType"; import { UrlBuilder } from "./builders/urlBuilder"; type HydrateAttributes = ExtractModelAttributesType & ExtractModelPersistedAttributesType & ExtractModelRelationsType; @@ -78,7 +76,7 @@ export class Batch const data = { resources: {} }; - items.forEach((v, i) => data.resources[v.$getKey()] = v.$attributes); + items.forEach((v) => data.resources[v.$getKey()] = v.$attributes); const response = await client.request<{ data: Array< AllAttributes & Relations > }>( `${url}/batch`, @@ -177,7 +175,7 @@ export class Batch let foundUrl: string | undefined = undefined; let isModel = false; - let ids = items.map((x: number | M) => { + const ids = items.map((x: number | M) => { // also find the url while we're at it if (typeof (x) == 'object' && x.$resource() && foundUrl == undefined) { foundUrl = x.$resource(); From ff5f48188c1e802d83b20e2af4c9860cfd9eb4d8 Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Mon, 6 Feb 2023 22:54:01 +0100 Subject: [PATCH 7/8] Fixed endpoint error --- src/batch.ts | 2 +- tests/integration/batch.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/batch.ts b/src/batch.ts index a877ac6..2ec48a5 100644 --- a/src/batch.ts +++ b/src/batch.ts @@ -18,7 +18,7 @@ export class Batch ) { let baseUrl = '' if (Orion.getBaseUrl()) { - baseUrl = Orion.getBaseUrl(); + baseUrl = Orion.getApiUrl(); } else { baseUrl = UrlBuilder.getResourceBaseUrl(modelConstructor); } diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts index ecece51..9d97c70 100644 --- a/tests/integration/batch.test.ts +++ b/tests/integration/batch.test.ts @@ -7,7 +7,6 @@ let server: any; beforeEach(() => { server = makeServer(); - Orion.setBaseUrl("https://api-mock.test/api") }); afterEach(() => { From a57428b40aba129ec661aa934911c9b3d853aacc Mon Sep 17 00:00:00 2001 From: Alpha1337k Date: Wed, 1 Mar 2023 17:36:50 +0100 Subject: [PATCH 8/8] Integrated into queryBuilder --- src/batch.ts | 194 ------------------- src/drivers/default/builders/queryBuilder.ts | 74 +++++++ tests/integration/batch.test.ts | 21 +- 3 files changed, 83 insertions(+), 206 deletions(-) delete mode 100644 src/batch.ts diff --git a/src/batch.ts b/src/batch.ts deleted file mode 100644 index 2ec48a5..0000000 --- a/src/batch.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { HttpMethod } from "./drivers/default/enums/httpMethod"; -import { Model } from "./model"; -import { Orion } from "./orion"; -import { ExtractModelAttributesType } from "./types/extractModelAttributesType"; -import { ExtractModelRelationsType } from "./types/extractModelRelationsType"; -import { ModelConstructor } from "./contracts/modelConstructor"; -import { QueryBuilder } from "./drivers/default/builders/queryBuilder"; -import { ExtractModelPersistedAttributesType } from "./types/extractModelPersistedAttributesType"; -import { UrlBuilder } from "./builders/urlBuilder"; - -type HydrateAttributes = ExtractModelAttributesType & ExtractModelPersistedAttributesType & ExtractModelRelationsType; - - -export class Batch -{ - private static $httpClient( - modelConstructor: ModelConstructor - ) { - let baseUrl = '' - if (Orion.getBaseUrl()) { - baseUrl = Orion.getApiUrl(); - } else { - baseUrl = UrlBuilder.getResourceBaseUrl(modelConstructor); - } - const httpClient = Orion.makeHttpClient(baseUrl); - - return httpClient; - } - - public static async store< - M extends Model, - Attributes = ExtractModelAttributesType, - PersistedAttributes = ExtractModelPersistedAttributesType, - Relations = ExtractModelRelationsType, - AllAttributes = Attributes & PersistedAttributes - >(items: M[]): Promise - { - if (!items.length) - return []; - - const client = this.$httpClient(items[0].constructor as ModelConstructor); - const url = items[0].$resource(); - - const data = { - resources: items.map(x => x.$attributes) - }; - - const response = await client.request<{ data: Array< AllAttributes & Relations > }>( - `${url}/batch`, - HttpMethod.POST, - null, - data - ); - - return response.data.data.map((attributes: AllAttributes & Relations) => { - const model: M = new (items[0].constructor as ModelConstructor)(); - return (model.$query() as QueryBuilder) - .hydrate(attributes as HydrateAttributes, response) - }); - } - - public static async update< - M extends Model, - Attributes = ExtractModelAttributesType, - PersistedAttributes = ExtractModelPersistedAttributesType, - Relations = ExtractModelRelationsType, - AllAttributes = Attributes & PersistedAttributes - >(items: M[]): Promise - { - if (!items.length) - return []; - - const client = this.$httpClient(items[0].constructor as ModelConstructor); - const url = items[0].$resource(); - - const data = { - resources: {} - }; - items.forEach((v) => data.resources[v.$getKey()] = v.$attributes); - - const response = await client.request<{ data: Array< AllAttributes & Relations > }>( - `${url}/batch`, - HttpMethod.PATCH, - null, - data - ) - - return response.data.data.map((attributes: AllAttributes & Relations) => { - const model: M = new (items[0].constructor as ModelConstructor)(); - return (model.$query() as QueryBuilder) - .hydrate(attributes as HydrateAttributes, response) - }); - } - - - - public static async delete(items: M[]): Promise; - public static async delete(items: number[], target: M): Promise; - public static async delete< - M extends Model, - Attributes = ExtractModelAttributesType, - PersistedAttributes = ExtractModelPersistedAttributesType, - Relations = ExtractModelRelationsType, - AllAttributes = Attributes & PersistedAttributes, - >(items: number[] | M[], target?: M): Promise - { - const {ids, url, isModel} = this.getBatchIds(items); - const finalUrl = url || target?.$resource(); - if (!finalUrl) - throw "BuilderError: url not found" - - const data = { - resources: ids - }; - - const client = this.$httpClient(items[0].constructor as ModelConstructor); - const response = await client.request<{ data: Array< AllAttributes & Relations > }>( - `${finalUrl}/batch`, - HttpMethod.DELETE, - null, - data - ); - - const modelConstructor = (isModel ? items[0].constructor : target?.constructor) as ModelConstructor - if (!modelConstructor) - return []; - - return response.data.data.map((attributes: AllAttributes & Relations) => { - const model: M = new modelConstructor(); - return (model.$query() as QueryBuilder) - .hydrate(attributes as HydrateAttributes, response) - }); - } - - - public static async restore(items: M[]): Promise; - public static async restore(items: number[], target: M): Promise; - public static async restore< - M extends Model, - Attributes = ExtractModelAttributesType, - PersistedAttributes = ExtractModelPersistedAttributesType, - Relations = ExtractModelRelationsType, - AllAttributes = Attributes & PersistedAttributes, - >(items: number[] | M[], target?: M): Promise - { - const {ids, url, isModel} = this.getBatchIds(items); - const data = { - resources: ids - }; - const finalUrl = url || target?.$resource(); - const client = this.$httpClient(items[0].constructor as ModelConstructor); - - - if (!finalUrl) - throw "BuilderError: url not found" - const response = await client.request<{ data: Array< AllAttributes & Relations > }>( - `${finalUrl}/batch/restore`, - HttpMethod.POST, - null, - data - ) - - const modelConstructor = (isModel ? items[0].constructor : target?.constructor) as ModelConstructor - if (!modelConstructor) - return []; - - return response.data.data.map((attributes: AllAttributes & Relations) => { - const model: M = new modelConstructor(); - return (model.$query() as QueryBuilder) - .hydrate(attributes as HydrateAttributes, response) - }); - } - - private static getBatchIds(items: number[] | M[]): {ids: unknown[], url: string | undefined, isModel: boolean} { - let foundUrl: string | undefined = undefined; - let isModel = false; - - const ids = items.map((x: number | M) => { - // also find the url while we're at it - if (typeof (x) == 'object' && x.$resource() && foundUrl == undefined) { - foundUrl = x.$resource(); - isModel = true; - } - - return typeof(x) == 'number' ? x : x.$attributes[x.$getKeyName()] - }) - - return { - ids, - url: foundUrl, - isModel - } - } -} \ No newline at end of file diff --git a/src/drivers/default/builders/queryBuilder.ts b/src/drivers/default/builders/queryBuilder.ts index 531153d..e63a1df 100644 --- a/src/drivers/default/builders/queryBuilder.ts +++ b/src/drivers/default/builders/queryBuilder.ts @@ -104,6 +104,23 @@ export class QueryBuilder< return this.hydrate(response.data.data, response); } + public async batchStore(items: M[]): Promise { + const data = { + resources: items.map(x => x.$attributes) + }; + + const response = await this.httpClient.request<{data: Array }>( + `/batch`, + HttpMethod.POST, + null, + data + ); + + return response.data.data.map((attributes) => { + return this.hydrate(attributes, response); + }) + } + public async update(key: Key, attributes: Attributes): Promise { const response = await this.httpClient.request<{ data: AllAttributes & Relations }>( `/${key}`, @@ -115,6 +132,24 @@ export class QueryBuilder< return this.hydrate(response.data.data, response); } + public async batchUpdate(items: M[]): Promise { + const data = { + resources: {} + }; + items.forEach((v) => data.resources[v.$getKey()] = v.$attributes); + + const response = await this.httpClient.request<{ data: Array< AllAttributes & Relations > }>( + `batch`, + HttpMethod.PATCH, + null, + data + ) + + return response.data.data.map((attributes: AllAttributes & Relations) => { + return this.hydrate(attributes, response); + }); + } + public async destroy(key: Key, force: boolean = false): Promise { const response = await this.httpClient.request<{ data: AllAttributes & Relations }>( `/${key}`, @@ -125,6 +160,27 @@ export class QueryBuilder< return this.hydrate(response.data.data, response); } + public async batchDelete(items: Key[]): Promise + { + if (!items.length) + return []; + + const data = { + resources: items + }; + + const response = await this.httpClient.request<{ data: Array< AllAttributes & Relations > }>( + `/batch`, + HttpMethod.DELETE, + null, + data + ); + + return response.data.data.map((attributes: AllAttributes & Relations) => { + return this.hydrate(attributes, response); + }); + } + public async restore(key: Key): Promise { const response = await this.httpClient.request<{ data: AllAttributes & Relations }>( `/${key}/restore`, @@ -135,6 +191,24 @@ export class QueryBuilder< return this.hydrate(response.data.data, response); } + public async batchRestore(items: Key[]): Promise { + const data = { + resources: items + }; + + const response = await this.httpClient.request<{ data: Array< AllAttributes & Relations > }>( + `/batch/restore`, + HttpMethod.POST, + null, + data + ); + + return response.data.data.map((attributes: AllAttributes & Relations) => { + return this.hydrate(attributes, response); + }); + } + + public with(relations: string[]): this { this.includes = relations; diff --git a/tests/integration/batch.test.ts b/tests/integration/batch.test.ts index 9d97c70..c345ffb 100644 --- a/tests/integration/batch.test.ts +++ b/tests/integration/batch.test.ts @@ -1,6 +1,5 @@ import makeServer from './drivers/default/server'; import Post from '../stubs/models/post'; -import { Batch } from '../../src/batch'; import { Orion } from '../../src/orion'; let server: any; @@ -22,7 +21,7 @@ describe('Batch tests', () => { posts[0].$attributes.title = "First"; posts[1].$attributes.title = "Second"; - const res = await Batch.store(posts); + const res = await Post.$query().batchStore(posts); expect(server.schema.posts.all()).toHaveLength(2); expect(server.schema.posts.find('1').attrs.title).toBe("First") @@ -43,12 +42,12 @@ describe('Batch tests', () => { posts[1].$attributes.title = "Second"; posts[2].$attributes.title = "Third"; - let res = await Batch.store(posts); + let res = await Post.$query().batchStore(posts); res[0].$attributes.title = "NewFirst"; res[1].$attributes.title = "NewSecond"; - res = await Batch.update([res[0],res[1]]); + res = await Post.$query().batchUpdate([res[0],res[1]]); expect(res).toHaveLength(2); expect(server.schema.posts.find('1').attrs.title).toBe("NewFirst") @@ -69,16 +68,15 @@ describe('Batch tests', () => { posts[1].$attributes.title = "Second"; posts[2].$attributes.title = "Third"; - let res = await Batch.store(posts); + let res = await Post.$query().batchStore(posts); - let ModelDelete = await Batch.delete([res[1]]); - let idDelete = await Batch.delete([3], new Post); + let ModelDelete = await Post.$query().batchDelete([res[1].$getKey(), res[2].$getKey()]); expect(server.schema.posts.find('1').attrs.deleted_at).toBeUndefined(); expect(server.schema.posts.find('2').attrs.deleted_at).toBeDefined(); expect(server.schema.posts.find('3').attrs.deleted_at).toBeDefined(); expect(server.schema.posts.find('2').attrs.title).toEqual(ModelDelete[0].$attributes.title) - expect(server.schema.posts.find('3').attrs.title).toEqual(idDelete[0].$attributes.title) + expect(server.schema.posts.find('3').attrs.title).toEqual(ModelDelete[1].$attributes.title) }); @@ -93,13 +91,12 @@ describe('Batch tests', () => { posts[1].$attributes.title = "Second"; posts[2].$attributes.title = "Third"; - let res = await Batch.store(posts); + let res = await Post.$query().batchStore(posts); // delete ID 2 & 3 - let ModelDelete = await Batch.delete([res[1]]); - let idDelete = await Batch.delete([3], new Post); + let ModelDelete = await Post.$query().batchDelete([res[1].$getKey(), res[2].$getKey()]); - res = await Batch.restore([...ModelDelete, ...idDelete]); + res = await Post.$query().batchRestore(ModelDelete.map(x => x.$getKey())); expect(server.schema.posts.find('1').attrs.deleted_at).toBeFalsy(); expect(server.schema.posts.find('2').attrs.deleted_at).toBeFalsy();