diff --git a/app/models/component.ts b/app/models/component.ts index 49c8e50..b964d20 100644 --- a/app/models/component.ts +++ b/app/models/component.ts @@ -24,6 +24,9 @@ export default class Component extends BaseModel { @column() declare computerId: number | null + @column() + declare purchaseUrl: string | null + @column({ serializeAs: null }) declare userId: number diff --git a/app/validators/Components/store.ts b/app/validators/Components/store.ts index 4a264b9..1eec7d3 100644 --- a/app/validators/Components/store.ts +++ b/app/validators/Components/store.ts @@ -6,6 +6,13 @@ export const storeComponentValidator = vine.compile( price: vine.number().decimal([0, 2]).min(0), typeId: vine.number().min(1), purchaseDate: vine.string().regex(/^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/), + purchaseUrl: vine + .string() + .trim() + .url({ require_protocol: true }) + .maxLength(2048) + .nullable() + .optional(), quantity: vine.number().min(1).withoutDecimals(), }) ) diff --git a/app/validators/Components/update.ts b/app/validators/Components/update.ts index 4b49771..8b5d323 100644 --- a/app/validators/Components/update.ts +++ b/app/validators/Components/update.ts @@ -6,5 +6,12 @@ export const updateComponentValidator = vine.compile( price: vine.number().decimal([0, 2]).min(0), typeId: vine.number().min(1), purchaseDate: vine.string().regex(/^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/), + purchaseUrl: vine + .string() + .trim() + .url({ require_protocol: true }) + .maxLength(2048) + .nullable() + .optional(), }) ) diff --git a/database/migrations/1784808177630_alter_components_table.ts b/database/migrations/1784808177630_alter_components_table.ts new file mode 100644 index 0000000..a5698d9 --- /dev/null +++ b/database/migrations/1784808177630_alter_components_table.ts @@ -0,0 +1,17 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'components' + + async up() { + this.schema.alterTable(this.tableName, (table) => { + table.string('purchase_url').nullable() + }) + } + + async down() { + this.schema.alterTable(this.tableName, (table) => { + table.dropColumn('purchase_url') + }) + } +} diff --git a/database/schema.ts b/database/schema.ts index 2608b7f..06b97d3 100644 --- a/database/schema.ts +++ b/database/schema.ts @@ -19,7 +19,7 @@ export class ComponentTypeSchema extends BaseModel { } export class ComponentSchema extends BaseModel { - static $columns = ['computerId', 'createdAt', 'id', 'name', 'price', 'purchaseDate', 'typeId', 'updatedAt', 'userId'] as const + static $columns = ['computerId', 'createdAt', 'id', 'name', 'price', 'purchaseDate', 'purchaseUrl', 'typeId', 'updatedAt', 'userId'] as const $columns = ComponentSchema.$columns @column() declare computerId: number | null @@ -34,6 +34,8 @@ export class ComponentSchema extends BaseModel { @column.date() declare purchaseDate: DateTime @column() + declare purchaseUrl: string | null + @column() declare typeId: number | null @column.dateTime({ autoCreate: true, autoUpdate: true }) declare updatedAt: DateTime | null diff --git a/inertia/pages/components/create.vue b/inertia/pages/components/create.vue index cd5a22b..c49022d 100644 --- a/inertia/pages/components/create.vue +++ b/inertia/pages/components/create.vue @@ -12,6 +12,7 @@ const form = reactive({ price: 0, typeId: null as number | null, purchaseDate: new Date().toISOString().split('T')[0], + purchaseUrl: '', quantity: 1, }) @@ -20,6 +21,7 @@ function importPart(part: ScrapedPart) { form.price = part.price form.typeId = part.type.id form.purchaseDate = new Date().toISOString().split('T')[0] + form.purchaseUrl = part.sourceUrl } function submitForm() { @@ -74,6 +76,19 @@ function submitForm() { /> +
+ + +
+
@@ -48,11 +49,7 @@ const form = reactive({
- +
@@ -67,6 +64,19 @@ const form = reactive({ />
+
+ + +
+ ()
+
+ + + {{ props.component.purchaseUrl }} + +
+ { assert.isNotNull(stillExists) }) }) + +test.group('Components purchase URL', () => { + test('a component can be created with a purchase site URL', async ({ client, assert }) => { + const user = await createUser('purchase-url-a@example.com') + const type = await createType('RAM-purchase-1') + + const response = await client + .post('/components') + .loginAs(user) + .withCsrfToken() + .redirects(0) + .json({ + name: 'New GPU', + price: 599.99, + typeId: type.id, + purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'), + purchaseUrl: 'https://fr.pc-builder.io/gpu/example-p1', + quantity: 1, + }) + + response.assertStatus(302) + const component = await Component.query().where('userId', user.id).firstOrFail() + assert.equal(component.purchaseUrl, 'https://fr.pc-builder.io/gpu/example-p1') + }) + + test('a component can be created without a purchase site URL', async ({ client, assert }) => { + const user = await createUser('purchase-url-b@example.com') + const type = await createType('RAM-purchase-2') + + const response = await client + .post('/components') + .loginAs(user) + .withCsrfToken() + .redirects(0) + .json({ + name: 'Used RAM stick', + price: 20, + typeId: type.id, + purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'), + quantity: 1, + }) + + response.assertStatus(302) + const component = await Component.query().where('userId', user.id).firstOrFail() + assert.isNull(component.purchaseUrl) + }) + + test('an invalid purchase URL is rejected', async ({ client, assert }) => { + const user = await createUser('purchase-url-c@example.com') + const type = await createType('RAM-purchase-3') + + const response = await client + .post('/components') + .loginAs(user) + .withCsrfToken() + .redirects(0) + .json({ + name: 'Suspicious component', + price: 20, + typeId: type.id, + purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'), + purchaseUrl: 'not-a-url', + quantity: 1, + }) + + response.assertStatus(302) + assert.property(response.flashMessage('inputErrorsBag'), 'purchaseUrl') + const created = await Component.query().where('userId', user.id).first() + assert.isNull(created) + }) + + test('the purchase URL can be updated on an existing component', async ({ client, assert }) => { + const owner = await createUser('purchase-url-d@example.com') + const type = await createType('RAM-purchase-4') + const component = await createComponent(owner.id, type.id) + + const response = await client + .put(`/components/${component.id}`) + .loginAs(owner) + .withCsrfToken() + .redirects(0) + .json({ + name: component.name, + price: component.price, + typeId: type.id, + purchaseDate: DateTime.now().toFormat('yyyy-MM-dd'), + purchaseUrl: 'https://fr.pc-builder.io/cpu/example-p2', + }) + + response.assertStatus(302) + await component.refresh() + assert.equal(component.purchaseUrl, 'https://fr.pc-builder.io/cpu/example-p2') + }) +})