diff --git a/package-lock.json b/package-lock.json index 45d4d99c..e8375093 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23682,8 +23682,7 @@ "version": "8.3.0", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz", "integrity": "sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-eslint-plugin": { "version": "3.6.1", @@ -23898,8 +23897,7 @@ "ajv-errors": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-3.0.0.tgz", - "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==", - "requires": {} + "integrity": "sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==" }, "ajv-keywords": { "version": "5.0.0", @@ -24751,8 +24749,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "requires": {} + "dev": true }, "acorn-walk": { "version": "7.2.0", @@ -26515,8 +26512,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz", "integrity": "sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg==", - "dev": true, - "requires": {} + "dev": true }, "eslint-plugin-mocha": { "version": "5.3.0", @@ -29463,8 +29459,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true, - "requires": {} + "dev": true }, "jest-regex-util": { "version": "26.0.0", @@ -32345,8 +32340,7 @@ "pg-pool": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz", - "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==", - "requires": {} + "integrity": "sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ==" }, "pg-protocol": { "version": "1.5.0", @@ -35163,8 +35157,7 @@ "version": "7.5.5", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.5.tgz", "integrity": "sha512-BAkMFcAzl8as1G/hArkxOxq3G7pjUqQ3gzYbLL0/5zNkph70e+lCoxBGnm6AW1+/aiNeV4fnKqZ8m4GZewmH2w==", - "dev": true, - "requires": {} + "dev": true }, "xml-name-validator": { "version": "3.0.0", diff --git a/src/controllers/growerNote.controller.ts b/src/controllers/growerNote.controller.ts new file mode 100644 index 00000000..ff2f95cb --- /dev/null +++ b/src/controllers/growerNote.controller.ts @@ -0,0 +1,166 @@ +import { inject } from '@loopback/core'; +import { repository } from '@loopback/repository'; +import { + post, + param, + get, + patch, + del, + requestBody, + HttpErrors, + RestBindings, + Response, +} from '@loopback/rest'; +import { GrowerNote } from '../models'; +import { GrowerNoteRepository, PlanterRepository } from '../repositories'; + +export class GrowerNoteController { + constructor( + @repository(GrowerNoteRepository) + public growerNoteRepository: GrowerNoteRepository, + @repository(PlanterRepository) + public planterRepository: PlanterRepository, + ) {} + + @get('/planter/{growerId}/notes', { + responses: { + '200': { + description: 'Array of GrowerNote instances', + content: { + 'application/json': { + schema: { type: 'array', items: { 'x-ts-type': GrowerNote } }, + }, + }, + }, + '404': { description: 'Planter not found' }, + }, + }) + async find( + @param.path.number('growerId') growerId: number, + ): Promise { + await this.planterRepository.findById(growerId).catch(() => { + throw new HttpErrors.NotFound(`Planter ${growerId} not found`); + }); + return this.growerNoteRepository.find({ where: { planterId: growerId } }); + } + + @get('/organization/{organizationId}/planter/{growerId}/notes', { + responses: { + '200': { + description: 'Array of GrowerNote instances for org-scoped planter', + content: { + 'application/json': { + schema: { type: 'array', items: { 'x-ts-type': GrowerNote } }, + }, + }, + }, + '404': { description: 'Planter not found' }, + }, + }) + async findByOrg( + @param.path.number('organizationId') _organizationId: number, + @param.path.number('growerId') growerId: number, + ): Promise { + await this.planterRepository.findById(growerId).catch(() => { + throw new HttpErrors.NotFound(`Planter ${growerId} not found`); + }); + return this.growerNoteRepository.find({ where: { planterId: growerId } }); + } + + @post('/planter/{growerId}/notes', { + responses: { + '201': { + description: 'GrowerNote created successfully', + content: { + 'application/json': { schema: { 'x-ts-type': GrowerNote } }, + }, + }, + '404': { description: 'Planter not found' }, + }, + }) + async create( + @param.path.number('growerId') growerId: number, + @requestBody() note: GrowerNote, + @inject(RestBindings.Http.RESPONSE) res: Response, + ): Promise { + await this.planterRepository.findById(growerId).catch(() => { + throw new HttpErrors.NotFound(`Planter ${growerId} not found`); + }); + note.planterId = growerId; + note.createdAt = new Date().toISOString(); + note.updatedAt = new Date().toISOString(); + const created = await this.growerNoteRepository.create(note); + res.status(201); + return created; + } + + @patch('/planter/{growerId}/notes/{noteId}', { + responses: { + '200': { + description: 'GrowerNote updated successfully', + content: { + 'application/json': { schema: { 'x-ts-type': GrowerNote } }, + }, + }, + '403': { description: 'Note does not belong to this planter' }, + '404': { description: 'GrowerNote not found' }, + }, + }) + async updateById( + @param.path.number('growerId') growerId: number, + @param.path.number('noteId') noteId: number, + @requestBody() note: Partial, + @inject(RestBindings.Http.RESPONSE) res: Response, + ): Promise { + const existing = await this.growerNoteRepository + .findById(noteId) + .catch(() => { + throw new HttpErrors.NotFound(`GrowerNote ${noteId} not found`); + }); + if (existing.planterId !== growerId) { + throw new HttpErrors.Forbidden( + `Note ${noteId} does not belong to planter ${growerId}`, + ); + } + note.updatedAt = new Date().toISOString(); + await this.growerNoteRepository.updateById(noteId, note); + const updated = await this.growerNoteRepository.findById(noteId); + res.status(200); + return updated; + } + + @del('/planter/{growerId}/notes/{noteId}', { + responses: { + '200': { + description: 'GrowerNote deleted successfully', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { message: { type: 'string' } }, + }, + }, + }, + }, + '403': { description: 'Note does not belong to this planter' }, + '404': { description: 'GrowerNote not found' }, + }, + }) + async deleteById( + @param.path.number('growerId') growerId: number, + @param.path.number('noteId') noteId: number, + ): Promise<{ message: string }> { + const existing = await this.growerNoteRepository + .findById(noteId) + .catch(() => { + throw new HttpErrors.NotFound(`GrowerNote ${noteId} not found`); + }); + if (existing.planterId !== growerId) { + throw new HttpErrors.Forbidden( + `Note ${noteId} does not belong to planter ${growerId}`, + ); + } + await this.growerNoteRepository.deleteById(noteId); + return { message: `Note ${noteId} deleted successfully` }; + } +} diff --git a/src/controllers/index.ts b/src/controllers/index.ts index e4ec0b4b..4a153800 100644 --- a/src/controllers/index.ts +++ b/src/controllers/index.ts @@ -6,3 +6,4 @@ export * from './tag.controller'; export * from './treeTag.controller'; export * from './treesTreeTag.controller'; export * from './organization.controller'; +export * from './growerNote.controller'; diff --git a/src/datasources/config.ts b/src/datasources/config.ts index 2a0424b1..38cc26e4 100644 --- a/src/datasources/config.ts +++ b/src/datasources/config.ts @@ -1,5 +1,13 @@ import pg from 'pg'; -pg.defaults.ssl = { rejectUnauthorized: false }; +if ( + process.env.DATABASE_URL && + (process.env.DATABASE_URL.includes('ssl=false') || + process.env.DATABASE_URL.includes('sslmode=disable')) +) { + pg.defaults.ssl = false; +} else { + pg.defaults.ssl = { rejectUnauthorized: false }; +} export interface DatasourceConfig { name: string; connector: string; diff --git a/src/models/adminUser.model.ts b/src/models/adminUser.model.ts index becdf0d6..267b314a 100644 --- a/src/models/adminUser.model.ts +++ b/src/models/adminUser.model.ts @@ -61,7 +61,7 @@ export class AdminUser extends Entity { postgresql: { columnName: 'email', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', diff --git a/src/models/growerNote.model.ts b/src/models/growerNote.model.ts new file mode 100644 index 00000000..a52ee642 --- /dev/null +++ b/src/models/growerNote.model.ts @@ -0,0 +1,124 @@ +import { Entity, model, property } from '@loopback/repository'; + +/* eslint-disable @typescript-eslint/no-empty-interface */ + +@model({ + settings: { + idInjection: false, + postgresql: { schema: 'public', table: 'grower_note' }, + }, +}) +export class GrowerNote extends Entity { + @property({ + type: Number, + required: false, + scale: 0, + id: 1, + postgresql: { + columnName: 'id', + dataType: 'serial', + dataLength: null, + dataPrecision: null, + dataScale: 0, + nullable: 'NO', + }, + }) + id: Number; + + @property({ + type: Number, + required: true, + scale: 0, + postgresql: { + columnName: 'planter_id', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: 0, + nullable: 'NO', + }, + }) + planterId: Number; + + @property({ + type: String, + required: true, + postgresql: { + columnName: 'content', + dataType: 'text', + dataLength: null, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + content: String; + + @property({ + type: Number, + required: true, + scale: 0, + postgresql: { + columnName: 'author_id', + dataType: 'integer', + dataLength: null, + dataPrecision: null, + dataScale: 0, + nullable: 'NO', + }, + }) + authorId: Number; + + @property({ + type: String, + required: true, + postgresql: { + columnName: 'author_name', + dataType: 'character varying', + dataLength: 255, + dataPrecision: null, + dataScale: null, + nullable: 'NO', + }, + }) + authorName: String; + + @property({ + type: String, + required: false, + postgresql: { + columnName: 'created_at', + dataType: 'timestamp without time zone', + dataLength: null, + dataPrecision: 6, + dataScale: null, + nullable: 'YES', + }, + }) + createdAt?: String; + + @property({ + type: String, + required: false, + postgresql: { + columnName: 'updated_at', + dataType: 'timestamp without time zone', + dataLength: null, + dataPrecision: 6, + dataScale: null, + nullable: 'YES', + }, + }) + updatedAt?: String; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [prop: string]: any; + + constructor(data?: Partial) { + super(data); + } +} + +export interface GrowerNoteRelations {} + +export type GrowerNoteWithRelations = GrowerNote & GrowerNoteRelations; diff --git a/src/models/index.ts b/src/models/index.ts index 0eaea7bd..4c1393b4 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -7,3 +7,4 @@ export * from './treeTag.model'; export * from './planterRegistration.model'; export * from './organization.model'; export * from './domainEvent.model'; +export * from './growerNote.model'; diff --git a/src/models/organization.model.ts b/src/models/organization.model.ts index bc4f6898..e3b603f2 100644 --- a/src/models/organization.model.ts +++ b/src/models/organization.model.ts @@ -24,6 +24,7 @@ export class Organization extends Entity { postgresql: { columnName: 'name', dataType: 'character varying', + dataLength: 255, }, }) name: String; @@ -33,6 +34,7 @@ export class Organization extends Entity { postgresql: { columnName: 'type', dataType: 'character varying', + dataLength: 255, }, }) type: String; diff --git a/src/models/planter.model.ts b/src/models/planter.model.ts index 3272ec9e..f0ca73eb 100644 --- a/src/models/planter.model.ts +++ b/src/models/planter.model.ts @@ -72,7 +72,7 @@ export class Planter extends Entity { postgresql: { columnName: 'email', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -86,7 +86,7 @@ export class Planter extends Entity { postgresql: { columnName: 'organization', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -135,7 +135,7 @@ export class Planter extends Entity { postgresql: { columnName: 'image_url', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', diff --git a/src/models/planterRegistration.model.ts b/src/models/planterRegistration.model.ts index d8e33fc0..9e661633 100644 --- a/src/models/planterRegistration.model.ts +++ b/src/models/planterRegistration.model.ts @@ -68,7 +68,7 @@ export class PlanterRegistration extends Entity { postgresql: { columnName: 'device_identifier', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', diff --git a/src/models/tag.model.ts b/src/models/tag.model.ts index 927a972d..d95a8ab1 100644 --- a/src/models/tag.model.ts +++ b/src/models/tag.model.ts @@ -45,7 +45,7 @@ export class Tag extends Entity { postgresql: { columnName: 'tag_name', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'NO', diff --git a/src/models/trees.model.ts b/src/models/trees.model.ts index e40efaa1..627b4a53 100644 --- a/src/models/trees.model.ts +++ b/src/models/trees.model.ts @@ -47,7 +47,7 @@ export class Trees extends Entity { columnName: 'time_created', dataType: 'timestamp without time zone', dataLength: null, - dataPrecision: null, + dataPrecision: 6, dataScale: null, nullable: 'NO', }, @@ -61,7 +61,7 @@ export class Trees extends Entity { columnName: 'time_updated', dataType: 'timestamp without time zone', dataLength: null, - dataPrecision: null, + dataPrecision: 6, dataScale: null, nullable: 'NO', }, @@ -120,7 +120,7 @@ export class Trees extends Entity { postgresql: { columnName: 'image_url', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -176,7 +176,7 @@ export class Trees extends Entity { postgresql: { columnName: 'planter_identifier', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -205,7 +205,7 @@ export class Trees extends Entity { postgresql: { columnName: 'device_identifier', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -219,7 +219,7 @@ export class Trees extends Entity { postgresql: { columnName: 'note', dataType: 'character varying', - dataLength: null, + dataLength: 255, dataPrecision: null, dataScale: null, nullable: 'YES', @@ -369,6 +369,7 @@ export class Trees extends Entity { postgresql: { columnName: 'planter_photo_url', dataType: 'character varying', + dataLength: 255, }, }) planterPhotoUrl?: String; @@ -379,6 +380,7 @@ export class Trees extends Entity { postgresql: { columnName: 'token_id', dataType: 'character varying', + dataLength: 255, }, }) tokenId?: String; diff --git a/src/repositories/growerNote.repository.ts b/src/repositories/growerNote.repository.ts new file mode 100644 index 00000000..a3e3d9e7 --- /dev/null +++ b/src/repositories/growerNote.repository.ts @@ -0,0 +1,16 @@ +import { DefaultCrudRepository } from '@loopback/repository'; +import { GrowerNote, GrowerNoteRelations } from '../models'; +import { TreetrackerDataSource } from '../datasources'; +import { inject } from '@loopback/core'; + +export class GrowerNoteRepository extends DefaultCrudRepository< + GrowerNote, + typeof GrowerNote.prototype.id, + GrowerNoteRelations +> { + constructor( + @inject('datasources.treetracker') dataSource: TreetrackerDataSource, + ) { + super(GrowerNote, dataSource); + } +} diff --git a/src/repositories/index.ts b/src/repositories/index.ts index 75e8e001..02427d20 100644 --- a/src/repositories/index.ts +++ b/src/repositories/index.ts @@ -7,3 +7,4 @@ export * from './treeTag.repository'; export * from './planterRegistration.repository'; export * from './organization.repository'; export * from './domainEvent.repository'; +export * from './growerNote.repository'; diff --git a/src/tests/seed/run-seed.ts b/src/tests/seed/run-seed.ts new file mode 100644 index 00000000..9f883b92 --- /dev/null +++ b/src/tests/seed/run-seed.ts @@ -0,0 +1,12 @@ +import seed from './seed'; + +seed + .seed() + .then(() => { + console.log('Seed completed successfully'); + process.exit(0); + }) + .catch((err) => { + console.error('Seed failed:', err); + process.exit(1); + });