diff --git a/apps/server/src/config/r2.ts b/apps/server/src/config/r2.ts index c5c34f6..10d6e96 100644 --- a/apps/server/src/config/r2.ts +++ b/apps/server/src/config/r2.ts @@ -1,11 +1,16 @@ -import { S3Client } from "@aws-sdk/client-s3"; +import { GetObjectCommand, S3Client } from "@aws-sdk/client-s3"; +import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { env } from "@fixr/env/server"; import { buildObjectPublicUrl as _buildObjectPublicUrl, isAllowedCompanyPhotoUrl as _isAllowedCompanyPhotoUrl, } from "../core/lib/r2"; -export { buildUploadObjectKey, sanitizeUploadFileName } from "../core/lib/r2"; +export { + buildModelObjectKey, + buildUploadObjectKey, + sanitizeUploadFileName, +} from "../core/lib/r2"; function parseR2BucketUrl(bucketUrl: string) { const parsed = new URL(bucketUrl); @@ -45,3 +50,17 @@ export function buildObjectPublicUrl(key: string) { export function isAllowedCompanyPhotoUrl(url: string, companyId: string) { return _isAllowedCompanyPhotoUrl(r2PublicBaseUrl, url, companyId); } + +/** + * Generate a presigned GET URL for reading an object from R2 + * + * @param key - The R2 object key (nullable) + * @returns A presigned URL valid for 24 hours, or null if key is empty + */ +export async function generatePresignedGetUrl(key: string): Promise { + const command = new GetObjectCommand({ + Bucket: r2Bucket, + Key: key, + }); + return await getSignedUrl(r2Client, command, { expiresIn: 86_400 }); +} diff --git a/apps/server/src/core/docs/categories/categories.docs.ts b/apps/server/src/core/docs/categories/categories.docs.ts new file mode 100644 index 0000000..8d02610 --- /dev/null +++ b/apps/server/src/core/docs/categories/categories.docs.ts @@ -0,0 +1,61 @@ +import { modelCategorySelectSchema } from "@fixr/db/schema"; +import { + getModelCategoriesQuerySchema, + getModelCategoryParamsSchema, +} from "@fixr/schemas/models"; +import type { FastifySchema } from "fastify"; +import { z } from "zod"; +import { zodResponseSchema } from "../types"; + +const listCategoriesSchema: FastifySchema = { + tags: ["Devices"], + summary: "List model categories", + description: ` +**Retrieves all device categories.** +Categories are global and not tied to a specific company. + +Optional filter (query string): +- \`query\`: search by category name +`, + querystring: getModelCategoriesQuerySchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Categories retrieved successfully.", + code: "list_categories_success", + data: z.array(modelCategorySelectSchema), + }).describe("Categories retrieved successfully."), + }, + security: [{ JWT: [] }], +}; + +const getCategoryBySlugSchema: FastifySchema = { + tags: ["Devices"], + summary: "Get category by slug", + description: "Retrieves a single device category by its slug.", + params: getModelCategoryParamsSchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Category retrieved successfully.", + code: "get_category_success", + data: modelCategorySelectSchema, + }).describe("Category retrieved successfully."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "category_not_found", + message: "Category not found.", + data: null, + }).describe("Category not found."), + }, + security: [{ JWT: [] }], +}; + +/** @description OpenAPI schemas for the categories module */ +export const categoriesDocs = { + listCategoriesSchema, + getCategoryBySlugSchema, +}; diff --git a/apps/server/src/core/docs/makers/makers.docs.ts b/apps/server/src/core/docs/makers/makers.docs.ts new file mode 100644 index 0000000..225aa62 --- /dev/null +++ b/apps/server/src/core/docs/makers/makers.docs.ts @@ -0,0 +1,79 @@ +import { modelMakerSelectSchema } from "@fixr/db/schema"; +import { + getModelMakerParamsSchema, + getModelMakersQuerySchema, +} from "@fixr/schemas/models"; +import { paginatedDataSchema } from "@fixr/schemas/utils"; +import type { FastifySchema } from "fastify"; +import { z } from "zod"; +import { zodResponseSchema } from "../types"; + +const makerListRecordSchema = z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + url: z.string(), + deviceCount: z.number(), + pageCount: z.number().nullable(), + createdAt: z.coerce.date(), +}); + +const listMakersSchema: FastifySchema = { + tags: ["Devices"], + summary: "List model makers", + description: ` +**Retrieves device makers (brands) with pagination.** + +Optional filters (query string): +- \`query\`: search by maker name +- \`page\`, \`perPage\`, \`sort\` (\`newer\` | \`older\` | \`name\` | \`most_devices\`): pagination (see API pagination docs) +`, + querystring: getModelMakersQuerySchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Makers successfully retrieved.", + code: "list_makers_success", + data: paginatedDataSchema(makerListRecordSchema), + }).describe("Makers retrieved successfully."), + 416: zodResponseSchema({ + status: 416, + error: "Range Not Satisfiable", + code: "page_out_of_bounds", + message: "The requested page exceeds the total number of pages.", + data: null, + }).describe("Requested page exceeds total pages."), + }, + security: [{ JWT: [] }], +}; + +const getMakerBySlugSchema: FastifySchema = { + tags: ["Devices"], + summary: "Get maker by slug", + description: "Retrieves a single device maker by its slug.", + params: getModelMakerParamsSchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Maker retrieved successfully.", + code: "get_maker_success", + data: modelMakerSelectSchema, + }).describe("Maker retrieved successfully."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "maker_not_found", + message: "Maker not found.", + data: null, + }).describe("Maker not found."), + }, + security: [{ JWT: [] }], +}; + +/** @description OpenAPI schemas for the makers module */ +export const makersDocs = { + listMakersSchema, + getMakerBySlugSchema, +}; diff --git a/apps/server/src/core/docs/models/models.docs.ts b/apps/server/src/core/docs/models/models.docs.ts new file mode 100644 index 0000000..cfd4413 --- /dev/null +++ b/apps/server/src/core/docs/models/models.docs.ts @@ -0,0 +1,391 @@ +import { getCompanyNestedDataSchema } from "@fixr/schemas/companies"; +import { + createModelBodySchema, + createModelImageBodySchema, + getModelsQuerySchema, + patchModelBodySchema, +} from "@fixr/schemas/models"; +import { paginatedDataSchema } from "@fixr/schemas/utils"; +import type { FastifySchema } from "fastify"; +import { z } from "zod"; +import { zodResponseSchema } from "../types"; + +const modelListRecordSchema = z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + imageUrl: z.string().nullable(), + status: z.string().nullable(), + price: z.string().nullable(), + released: z.string().nullable(), + maker: z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + }), + category: z + .object({ + id: z.string(), + name: z.string(), + slug: z.string(), + }) + .nullable(), +}); + +const modelImageRecordSchema = z.object({ + id: z.string(), + modelId: z.string(), + r2Key: z.string().nullable(), + presignedUrl: z.string().nullable(), + isPrimary: z.boolean(), + variant: z.string().nullable(), + position: z.number(), + createdAt: z.coerce.date(), +}); + +const modelDetailRecordSchema = z.object({ + id: z.string(), + makerId: z.string(), + name: z.string(), + slug: z.string(), + url: z.string(), + imageUrl: z.string().nullable(), + categoryId: z.string().nullable(), + announced: z.string().nullable(), + status: z.string().nullable(), + dimensions: z.string().nullable(), + weight: z.string().nullable(), + build: z.string().nullable(), + sim: z.string().nullable(), + displayType: z.string().nullable(), + displaySize: z.string().nullable(), + displayResolution: z.string().nullable(), + displayProtection: z.string().nullable(), + os: z.string().nullable(), + chipset: z.string().nullable(), + cpu: z.string().nullable(), + gpu: z.string().nullable(), + cardSlot: z.string().nullable(), + internalMemory: z.string().nullable(), + mainCamera: z.string().nullable(), + mainCameraFeatures: z.string().nullable(), + mainCameraVideo: z.string().nullable(), + selfieCamera: z.string().nullable(), + selfieFeatures: z.string().nullable(), + selfieVideo: z.string().nullable(), + battery: z.string().nullable(), + batteryCharging: z.string().nullable(), + networkTech: z.string().nullable(), + sensors: z.string().nullable(), + colors: z.string().nullable(), + colorsHex: z.string().nullable(), + modelsText: z.string().nullable(), + price: z.string().nullable(), + dimensionsWidth: z.number().nullable(), + dimensionsHeight: z.number().nullable(), + dimensionsThickness: z.number().nullable(), + weightGrams: z.number().nullable(), + displaySizeInches: z.number().nullable(), + displaySizeRatio: z.string().nullable(), + displayResWidth: z.number().nullable(), + displayResHeight: z.number().nullable(), + displayResPpi: z.number().nullable(), + released: z.string().nullable(), + meta: z.string().nullable(), + companyId: z.string().nullable(), + createdAt: z.coerce.date(), + maker: z.object({ + id: z.string(), + name: z.string(), + slug: z.string(), + url: z.string(), + }), + category: z + .object({ + id: z.string(), + name: z.string(), + slug: z.string(), + }) + .nullable(), + images: z.array(modelImageRecordSchema), +}); + +const modelDetailParamsSchema = z + .object({ + subdomain: z.string(), + slug: z.string(), + }) + .describe("Company subdomain and model slug."); + +const listModelsSchema: FastifySchema = { + tags: ["Devices"], + summary: "List device models", + description: ` +**Retrieves device models (paginated minimal list) for mounting a table.** + +Returns base (global) models plus company-specific models. + +Optional filters (query string): +- \`query\`: fulltext search on name, model variants, chipset, CPU, internal memory, OS +- \`makerId\`: filter by brand (cuid2) +- \`categoryId\`: filter by category (cuid2) +- \`status\`: release status: Available, Discontinued, Cancelled, Rumored +- \`page\`, \`perPage\`, \`sort\` (\`newer\` | \`older\` | \`name\`): pagination (see API pagination docs) +`, + params: getCompanyNestedDataSchema, + querystring: getModelsQuerySchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Models successfully retrieved.", + code: "list_models_success", + data: paginatedDataSchema(modelListRecordSchema), + }).describe("Models retrieved successfully."), + 416: zodResponseSchema({ + status: 416, + error: "Range Not Satisfiable", + code: "page_out_of_bounds", + message: "The requested page exceeds the total number of pages.", + data: null, + }).describe("Requested page exceeds total pages."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed to access this company."), + }, + security: [{ JWT: [] }], +}; + +const getModelBySlugSchema: FastifySchema = { + tags: ["Devices"], + summary: "Get device model by slug", + description: ` +**Retrieves full device model details by slug.** + +Returns all spec fields, related maker and category, and model images with presigned URLs. +`, + params: modelDetailParamsSchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Model retrieved successfully.", + code: "get_model_success", + data: modelDetailRecordSchema, + }).describe("Model retrieved successfully."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "model_not_found", + message: "Model not found.", + data: null, + }).describe("Model not found."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed to access this company."), + }, + security: [{ JWT: [] }], +}; + +const createModelSchema: FastifySchema = { + tags: ["Devices"], + summary: "Create device model", + description: ` +**Creates a new device model record.** + +Only company-specific models can be created. +- \`name\` and \`makerId\` are required. +- \`slug\` is auto-generated from \`name\` if not provided. +- All spec fields are optional and can be filled later via PATCH. +`, + params: getCompanyNestedDataSchema, + body: createModelBodySchema, + response: { + 201: zodResponseSchema({ + status: 201, + error: null, + message: "Model created successfully.", + code: "create_model_success", + data: z.object({ id: z.string(), name: z.string(), slug: z.string() }), + }).describe("Model created successfully."), + 409: zodResponseSchema({ + status: 409, + error: "Conflict", + code: "model_slug_conflict", + message: "A model with this slug already exists.", + data: null, + }).describe("Slug already exists."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed to access this company."), + }, + security: [{ JWT: [] }], +}; + +const patchModelSchema: FastifySchema = { + tags: ["Devices"], + summary: "Update device model (partial)", + description: ` +**Partially updates a device model record.** + +All fields are optional: only provided fields will be updated. +Returns the full model detail with presigned image URLs. +`, + params: z.object({ subdomain: z.string(), modelId: z.string() }), + body: patchModelBodySchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Model updated successfully.", + code: "patch_model_success", + data: modelDetailRecordSchema, + }).describe("Model updated successfully."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "model_not_found", + message: "Model not found.", + data: null, + }).describe("Model not found."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed to access this company."), + }, + security: [{ JWT: [] }], +}; + +const deleteModelSchema: FastifySchema = { + tags: ["Devices"], + summary: "Delete device model", + description: ` +**Deletes a device model and its associated images.** + +Removes all related \`model_images\` records and deletes the uploaded files from storage. +`, + params: z.object({ subdomain: z.string(), modelId: z.string() }), + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Model deleted successfully.", + code: "delete_model_success", + data: null, + }).describe("Model deleted successfully."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "model_not_found", + message: "Model not found.", + data: null, + }).describe("Model not found."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed to access this company."), + }, + security: [{ JWT: [] }], +}; + +const createModelImageSchema: FastifySchema = { + tags: ["Devices"], + summary: "Assign an uploaded image to a model", + description: ` +**Creates a model image record, linking an uploaded file to a device model.** + +Provide the \`r2Key\` returned from the presign upload endpoint. Returns the created model image with a presigned URL. +`, + params: z.object({ subdomain: z.string(), modelId: z.string() }), + body: createModelImageBodySchema, + response: { + 201: zodResponseSchema({ + status: 201, + error: null, + message: "Model image created successfully.", + code: "create_model_image_success", + data: modelImageRecordSchema, + }).describe("Model image created."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "model_not_found", + message: "Model not found.", + data: null, + }).describe("Model not found."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed."), + }, + security: [{ JWT: [] }], +}; + +const deleteModelImageSchema: FastifySchema = { + tags: ["Devices"], + summary: "Delete a model image", + description: ` +**Deletes a model image record and removes the uploaded file from storage.** +`, + params: z.object({ + subdomain: z.string(), + modelId: z.string(), + imageId: z.string(), + }), + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Model image deleted successfully.", + code: "delete_model_image_success", + data: null, + }).describe("Model image deleted."), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "model_image_not_found", + message: "Model image not found.", + data: null, + }).describe("Image not found."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to access this company.", + data: null, + }).describe("Not allowed."), + }, + security: [{ JWT: [] }], +}; + +/** @description OpenAPI schemas for the models module */ +export const modelsDocs = { + listModelsSchema, + getModelBySlugSchema, + createModelSchema, + patchModelSchema, + deleteModelSchema, + createModelImageSchema, + deleteModelImageSchema, +}; diff --git a/apps/server/src/core/docs/service-orders.docs.ts b/apps/server/src/core/docs/service-orders.docs.ts index 0ed65a1..1c8f511 100644 --- a/apps/server/src/core/docs/service-orders.docs.ts +++ b/apps/server/src/core/docs/service-orders.docs.ts @@ -44,12 +44,12 @@ const getCompanyServiceOrdersSchema: FastifySchema = { **Retrieves company service orders (paginated)** Optional filters (query string): -- \`deviceCategoryId\` — device category (cuid2) -- \`employeeId\` — responsible employee (cuid2) -- \`status\` — one of: ${serviceOrderStatuses.options.join(", ")} -- \`dateFrom\` / \`dateTo\` — filter by \`created_at\` (inclusive; ISO date or datetime) -- \`query\` — search in device model, reported defect, or client name -- \`page\`, \`perPage\`, \`sort\` (\`newer\` | \`older\`) — pagination (see API pagination docs) +- \`deviceCategoryId\`: device category (cuid2) +- \`employeeId\`: responsible employee (cuid2) +- \`status\`: one of: ${serviceOrderStatuses.options.join(", ")} +- \`dateFrom\` / \`dateTo\`: filter by \`created_at\` (inclusive; ISO date or datetime) +- \`query\`: search in device model, reported defect, or client name +- \`page\`, \`perPage\`, \`sort\` (\`newer\` | \`older\`): pagination (see API pagination docs) `, params: getCompanyNestedDataSchema, querystring: getServiceOrdersQuerySchema, diff --git a/apps/server/src/core/docs/uploads.docs.ts b/apps/server/src/core/docs/uploads.docs.ts index 63b1d9e..1f435ba 100644 --- a/apps/server/src/core/docs/uploads.docs.ts +++ b/apps/server/src/core/docs/uploads.docs.ts @@ -1,4 +1,5 @@ import { + createModelImageUploadPresignSchema, createUploadPresignSchema, uploadPresignResponseSchema, } from "@fixr/schemas/uploads"; @@ -42,6 +43,42 @@ The request accepts file metadata (\`fileName\`, \`contentType\`, \`size\`) and security: [{ JWT: [] }], }; +const createModelImagePresignSchemaDoc: FastifySchema = { + tags: ["Uploads"], + summary: "Generate pre-signed upload URL for a model image", + description: ` +**Generates a pre-signed URL for uploading a model image to Cloudflare R2** + +Returns a time-limited presigned PUT URL and an upload ID. Upload the file directly to R2 using the returned URL, then use \`POST /{modelId}/images\` in the devices module to assign it to a model. +`, + body: createModelImageUploadPresignSchema, + response: { + 200: zodResponseSchema({ + status: 200, + error: null, + message: "Upload URL generated successfully.", + code: "create_model_image_presign_success", + data: uploadPresignResponseSchema, + }).describe("Presigned URL generated."), + 403: zodResponseSchema({ + status: 403, + error: "Forbidden", + code: "not_allowed", + message: "You are not authorized to perform this action.", + data: null, + }), + 404: zodResponseSchema({ + status: 404, + error: "Not Found", + code: "company_not_found", + message: "There's no company associated with this account.", + data: null, + }), + }, + security: [{ JWT: [] }], +}; + export const uploadsDocs = { createUploadPresignSchema: createUploadPresignSchemaDoc, + createModelImagePresignSchema: createModelImagePresignSchemaDoc, }; diff --git a/apps/server/src/core/errors/index.ts b/apps/server/src/core/errors/index.ts index 6156b0a..c0d64fc 100644 --- a/apps/server/src/core/errors/index.ts +++ b/apps/server/src/core/errors/index.ts @@ -1,8 +1,11 @@ import { accountErrors } from "../../modules/account/errors"; import { authErrors } from "../../modules/auth/errors"; +import { categoriesErrors } from "../../modules/categories/errors"; import { companiesErrors } from "../../modules/companies/errors"; import { credentialsErrors } from "../../modules/credentials/errors"; import { employeesErrors } from "../../modules/employees/errors"; +import { makersErrors } from "../../modules/makers/errors"; +import { modelsErrors } from "../../modules/models/errors"; import { serviceOrdersErrors } from "../../modules/service-orders/errors"; import { tokensErrors } from "../../modules/tokens/errors"; import { uploadsErrors } from "../../modules/uploads/errors"; @@ -14,6 +17,9 @@ export const errors = defineErrors({ ...credentialsErrors, ...companiesErrors, ...employeesErrors, + ...categoriesErrors, + ...makersErrors, + ...modelsErrors, ...serviceOrdersErrors, ...tokensErrors, ...uploadsErrors, diff --git a/apps/server/src/core/lib/r2.ts b/apps/server/src/core/lib/r2.ts index 6e0af8d..bd518f4 100644 --- a/apps/server/src/core/lib/r2.ts +++ b/apps/server/src/core/lib/r2.ts @@ -56,3 +56,19 @@ export function buildUploadObjectKey({ return `companies/${companyId}/service-orders/${uniquePrefix}-${safeName}`; } + +/** + * Build an R2 object key for a model image + */ +export function buildModelObjectKey({ + companyId, + fileName, +}: { + companyId: string; + fileName: string; +}): string { + const safeName = sanitizeUploadFileName(fileName); + const uniquePrefix = `${Date.now()}-${randomUUID().slice(0, 8)}`; + + return `companies/${companyId}/models/${uniquePrefix}-${safeName}`; +} diff --git a/apps/server/src/core/lib/response.ts b/apps/server/src/core/lib/response.ts index d7953dc..67bf1a5 100644 --- a/apps/server/src/core/lib/response.ts +++ b/apps/server/src/core/lib/response.ts @@ -28,6 +28,7 @@ export const httpStatusCodes: Record = { 405: "Method Not Allowed", 409: "Conflict", 410: "Gone", + 416: "Range Not Satisfiable", 418: "I'm a teapot", 429: "Too Many Requests", 500: "Internal Server Error", diff --git a/apps/server/src/modules/categories/controllers/index.ts b/apps/server/src/modules/categories/controllers/index.ts new file mode 100644 index 0000000..da13dc5 --- /dev/null +++ b/apps/server/src/modules/categories/controllers/index.ts @@ -0,0 +1,32 @@ +import type { + getModelCategoriesQuerySchema, + getModelCategoryParamsSchema, +} from "@fixr/schemas/models"; +import type { FastifyReply } from "fastify"; +import type { z } from "zod"; +import { CategoriesService } from "../services"; + +/** @description Categories request handlers */ +export class CategoriesController { + /** @description List all categories */ + static listCategories({ + query, + response, + }: { + query?: string; + response: FastifyReply; + } & z.infer) { + return CategoriesService.listCategories({ query, response }); + } + + /** @description Get a category by slug */ + static getCategoryBySlug({ + slug, + response, + }: { + slug: string; + response: FastifyReply; + } & z.infer) { + return CategoriesService.getCategoryBySlug({ slug, response }); + } +} diff --git a/apps/server/src/modules/categories/errors/index.ts b/apps/server/src/modules/categories/errors/index.ts new file mode 100644 index 0000000..15ccd62 --- /dev/null +++ b/apps/server/src/modules/categories/errors/index.ts @@ -0,0 +1,10 @@ +import { defineErrors } from "../../../core/utils/errors"; + +/** @description Error definitions for the categories module */ +export const categoriesErrors = defineErrors({ + CATEGORY_NOT_FOUND: { + code: "category_not_found", + message: "Category not found.", + status: 404, + }, +}); diff --git a/apps/server/src/modules/categories/repositories/index.ts b/apps/server/src/modules/categories/repositories/index.ts new file mode 100644 index 0000000..702446d --- /dev/null +++ b/apps/server/src/modules/categories/repositories/index.ts @@ -0,0 +1,33 @@ +import { asc, db, eq, like } from "@fixr/db/connection"; +import { modelCategories } from "@fixr/db/schema"; + +/** @description Data access layer for device categories */ +export class CategoriesRepository { + /** + * Query all categories, optionally filtering by name + * + * @param query - Optional name filter + */ + static async queryAllCategories(query?: string) { + const base = db.select().from(modelCategories).$dynamic(); + if (query) { + base.where(like(modelCategories.name, `%${query}%`)); + } + return await base.orderBy(asc(modelCategories.name)); + } + + /** + * Find a category by its slug + * + * @param slug - The category slug + * @returns The category record or null + */ + static async queryCategoryBySlug(slug: string) { + const [category] = await db + .select() + .from(modelCategories) + .where(eq(modelCategories.slug, slug)) + .limit(1); + return category ?? null; + } +} diff --git a/apps/server/src/modules/categories/routes/index.ts b/apps/server/src/modules/categories/routes/index.ts new file mode 100644 index 0000000..be1c8a8 --- /dev/null +++ b/apps/server/src/modules/categories/routes/index.ts @@ -0,0 +1,46 @@ +import { permissions } from "@fixr/permissions"; +import { + getModelCategoriesQuerySchema, + getModelCategoryParamsSchema, +} from "@fixr/schemas/models"; +import { categoriesDocs } from "../../../core/docs/categories/categories.docs"; +import type { FastifyTypedInstance } from "../../../core/interfaces/fastify"; +import { authenticateEmployee } from "../../../core/middlewares/authenticate-employee"; +import { requirePermission } from "../../../core/middlewares/rbac"; +import { withErrorHandler } from "../../../core/middlewares/with-error-handler"; +import { CategoriesController } from "../controllers"; + +/** @description Categories routes plugin */ +export function categoriesRoutes(fastify: FastifyTypedInstance) { + fastify.get( + "/", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: categoriesDocs.listCategoriesSchema, + }, + withErrorHandler(async (request, response) => { + const { query } = getModelCategoriesQuerySchema.parse(request.query); + + await CategoriesController.listCategories({ query, response }); + }) + ); + + fastify.get( + "/:slug", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: categoriesDocs.getCategoryBySlugSchema, + }, + withErrorHandler(async (request, response) => { + const { slug } = getModelCategoryParamsSchema.parse(request.params); + + await CategoriesController.getCategoryBySlug({ slug, response }); + }) + ); +} diff --git a/apps/server/src/modules/categories/services/index.ts b/apps/server/src/modules/categories/services/index.ts new file mode 100644 index 0000000..a724fda --- /dev/null +++ b/apps/server/src/modules/categories/services/index.ts @@ -0,0 +1,64 @@ +import { modelCategorySelectSchema } from "@fixr/db/schema"; +import type { FastifyReply } from "fastify"; +import { AppError } from "../../../core/lib/app-error"; +import { apiResponse } from "../../../core/lib/response"; +import { CategoriesRepository } from "../repositories"; + +/** @description Business logic for device categories */ +export class CategoriesService { + /** + * List all categories, optionally filtered by query + * + * @param query - Optional name filter + * @param response - Fastify reply + */ + static async listCategories({ + query, + response, + }: { + query?: string; + response: FastifyReply; + }) { + const categories = await CategoriesRepository.queryAllCategories(query); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "list_categories_success", + message: "Categories retrieved successfully.", + data: categories.map((c) => modelCategorySelectSchema.parse(c)), + }) + ); + } + + /** + * Get a category by its slug + * + * @param slug - The category slug + * @param response - Fastify reply + */ + static async getCategoryBySlug({ + slug, + response, + }: { + slug: string; + response: FastifyReply; + }) { + const category = await CategoriesRepository.queryCategoryBySlug(slug); + + if (!category) { + throw new AppError("CATEGORY_NOT_FOUND"); + } + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "get_category_success", + message: "Category retrieved successfully.", + data: modelCategorySelectSchema.parse(category), + }) + ); + } +} diff --git a/apps/server/src/modules/makers/controllers/index.ts b/apps/server/src/modules/makers/controllers/index.ts new file mode 100644 index 0000000..7843df9 --- /dev/null +++ b/apps/server/src/modules/makers/controllers/index.ts @@ -0,0 +1,41 @@ +import type { getModelMakersQuerySchema } from "@fixr/schemas/models"; +import type { FastifyReply } from "fastify"; +import type { z } from "zod"; +import { MakersService } from "../services"; + +/** @description Makers request handlers */ +export class MakersController { + /** @description List makers with pagination */ + static listMakers({ + page, + perPage, + query, + sort, + response, + }: { + page: number; + perPage?: number; + query?: string; + sort?: string; + response: FastifyReply; + } & z.infer) { + return MakersService.listMakers({ + page, + perPage, + query, + sort, + response, + }); + } + + /** @description Get a maker by slug */ + static getMakerBySlug({ + slug, + response, + }: { + slug: string; + response: FastifyReply; + }) { + return MakersService.getMakerBySlug({ slug, response }); + } +} diff --git a/apps/server/src/modules/makers/errors/index.ts b/apps/server/src/modules/makers/errors/index.ts new file mode 100644 index 0000000..345015a --- /dev/null +++ b/apps/server/src/modules/makers/errors/index.ts @@ -0,0 +1,15 @@ +import { defineErrors } from "../../../core/utils/errors"; + +/** @description Error definitions for the makers module */ +export const makersErrors = defineErrors({ + MAKER_NOT_FOUND: { + code: "maker_not_found", + message: "Maker not found.", + status: 404, + }, + MAKER_PAGE_OUT_OF_BOUNDS: { + code: "page_out_of_bounds", + message: "The requested page exceeds the total number of pages.", + status: 416, + }, +}); diff --git a/apps/server/src/modules/makers/repositories/index.ts b/apps/server/src/modules/makers/repositories/index.ts new file mode 100644 index 0000000..5b7d57d --- /dev/null +++ b/apps/server/src/modules/makers/repositories/index.ts @@ -0,0 +1,62 @@ +import { and, asc, db, desc, eq, like, type SQL } from "@fixr/db/connection"; +import { modelMakers } from "@fixr/db/schema"; + +/** @description Column selection for paginated makers list */ +export const makersListSelect = { + id: modelMakers.id, + name: modelMakers.name, + slug: modelMakers.slug, + url: modelMakers.url, + deviceCount: modelMakers.deviceCount, + pageCount: modelMakers.pageCount, + createdAt: modelMakers.createdAt, +}; + +/** @description Data access layer for device makers */ +export class MakersRepository { + /** + * Build a WHERE clause for filtering makers + * + * @param query - Optional name filter + */ + static buildListFilter(query?: string) { + const conditions: SQL[] = []; + if (query) { + conditions.push(like(modelMakers.name, `%${query}%`)); + } + return conditions.length > 0 ? and(...conditions) : undefined; + } + + /** + * Build an ORDER BY clause for makers + * + * @param sort - Sort key: newer, older, name, most_devices + */ + static buildOrder(sort?: string) { + switch (sort) { + case "newer": + return desc(modelMakers.createdAt); + case "older": + return asc(modelMakers.createdAt); + case "most_devices": + return desc(modelMakers.deviceCount); + default: + return asc(modelMakers.name); + } + } + + /** + * Find a maker by its slug + * + * @param slug - The maker slug + * @returns The maker record or null + */ + static async queryMakerBySlug(slug: string) { + const [maker] = await db + .select() + .from(modelMakers) + .where(eq(modelMakers.slug, slug)) + .limit(1); + return maker ?? null; + } +} diff --git a/apps/server/src/modules/makers/routes/index.ts b/apps/server/src/modules/makers/routes/index.ts new file mode 100644 index 0000000..28970a3 --- /dev/null +++ b/apps/server/src/modules/makers/routes/index.ts @@ -0,0 +1,54 @@ +import { permissions } from "@fixr/permissions"; +import { + getModelMakerParamsSchema, + getModelMakersQuerySchema, +} from "@fixr/schemas/models"; +import { makersDocs } from "../../../core/docs/makers/makers.docs"; +import type { FastifyTypedInstance } from "../../../core/interfaces/fastify"; +import { authenticateEmployee } from "../../../core/middlewares/authenticate-employee"; +import { requirePermission } from "../../../core/middlewares/rbac"; +import { withErrorHandler } from "../../../core/middlewares/with-error-handler"; +import { MakersController } from "../controllers"; + +/** @description Makers routes plugin */ +export function makersRoutes(fastify: FastifyTypedInstance) { + fastify.get( + "/", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: makersDocs.listMakersSchema, + }, + withErrorHandler(async (request, response) => { + const { query, sort, page, perPage } = getModelMakersQuerySchema.parse( + request.query + ); + + await MakersController.listMakers({ + page, + perPage, + query, + sort, + response, + }); + }) + ); + + fastify.get( + "/:slug", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: makersDocs.getMakerBySlugSchema, + }, + withErrorHandler(async (request, response) => { + const { slug } = getModelMakerParamsSchema.parse(request.params); + + await MakersController.getMakerBySlug({ slug, response }); + }) + ); +} diff --git a/apps/server/src/modules/makers/services/index.ts b/apps/server/src/modules/makers/services/index.ts new file mode 100644 index 0000000..881f54a --- /dev/null +++ b/apps/server/src/modules/makers/services/index.ts @@ -0,0 +1,134 @@ +import { modelMakerSelectSchema } from "@fixr/db/schema"; +import type { FastifyReply } from "fastify"; +import { AppError } from "../../../core/lib/app-error"; +import { + getPaginatedCount, + getPaginatedRecords, +} from "../../../core/lib/pagination"; +import { apiResponse, paginatedData } from "../../../core/lib/response"; +import { MakersRepository, makersListSelect } from "../repositories"; + +/** @description Business logic for device makers */ +export class MakersService { + /** + * List makers with pagination, filtering, and sorting + * + * @param page - Current page number + * @param perPage - Items per page + * @param query - Optional name filter + * @param sort - Sort direction + * @param response - Fastify reply + */ + static async listMakers({ + page, + perPage, + query, + sort, + response, + }: { + page: number; + perPage?: number; + query?: string; + sort?: string; + response: FastifyReply; + }) { + const PER_PAGE = perPage ?? 10; + + const filter = MakersRepository.buildListFilter(query); + const order = MakersRepository.buildOrder(sort); + + const [records, totalRecords] = await Promise.all([ + getPaginatedRecords({ + table: makersListSelect.id.table, + select: makersListSelect, + skip: (page - 1) * PER_PAGE, + take: PER_PAGE, + where: filter, + order, + }), + getPaginatedCount({ + table: makersListSelect.id.table, + where: filter, + }), + ]); + + if (totalRecords === 0) { + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + message: "Makers successfully retrieved.", + code: "list_makers_success", + data: paginatedData({ + records: [], + pagination: { + total_records: 0, + total_pages: 0, + current_page: 1, + next_page: null, + prev_page: null, + }, + }), + }) + ); + } + + const total_pages = Math.ceil(totalRecords / PER_PAGE); + + if (page > total_pages) { + throw new AppError("MAKER_PAGE_OUT_OF_BOUNDS"); + } + + const next_page = + PER_PAGE * (page - 1) + records.length < totalRecords ? page + 1 : null; + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + message: "Makers successfully retrieved.", + code: "list_makers_success", + data: paginatedData({ + records, + pagination: { + total_records: totalRecords, + total_pages, + current_page: page, + next_page, + prev_page: page > 1 ? page - 1 : null, + }, + }), + }) + ); + } + + /** + * Get a maker by its slug + * + * @param slug - The maker slug + * @param response - Fastify reply + */ + static async getMakerBySlug({ + slug, + response, + }: { + slug: string; + response: FastifyReply; + }) { + const maker = await MakersRepository.queryMakerBySlug(slug); + + if (!maker) { + throw new AppError("MAKER_NOT_FOUND"); + } + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "get_maker_success", + message: "Maker retrieved successfully.", + data: modelMakerSelectSchema.parse(maker), + }) + ); + } +} diff --git a/apps/server/src/modules/models/controllers/index.ts b/apps/server/src/modules/models/controllers/index.ts new file mode 100644 index 0000000..16134ba --- /dev/null +++ b/apps/server/src/modules/models/controllers/index.ts @@ -0,0 +1,165 @@ +import type { jwtPayload } from "@fixr/schemas/auth"; +import type { + createModelBodySchema, + createModelImageBodySchema, + getModelBySlugParamsSchema, + getModelsQuerySchema, + modelIdParamsSchema, + patchModelBodySchema, +} from "@fixr/schemas/models"; +import type { FastifyReply } from "fastify"; +import type { z } from "zod"; +import { ModelsService } from "../services"; + +/** @description Models request handlers */ +export class ModelsController { + /** @description List models with pagination and filtering */ + static listModels({ + userJwt, + subdomain, + page, + perPage, + query, + makerId, + categoryId, + status, + sort, + response, + }: { + userJwt: z.infer; + subdomain: string; + response: FastifyReply; + } & z.infer) { + return ModelsService.listModels({ + userJwt, + subdomain, + page, + perPage, + query, + makerId, + categoryId, + status, + sort, + response, + }); + } + + /** @description Get a model by slug with full details */ + static getModelBySlug({ + userJwt, + subdomain, + slug, + response, + }: { + userJwt: z.infer; + subdomain: string; + slug: string; + response: FastifyReply; + } & z.infer) { + return ModelsService.getModelBySlug({ + userJwt, + subdomain, + slug, + response, + }); + } + + /** @description Create a new model */ + static createModel({ + userJwt, + subdomain, + data, + response, + }: { + userJwt: z.infer; + subdomain: string; + data: z.infer; + response: FastifyReply; + }) { + return ModelsService.createModel({ userJwt, subdomain, data, response }); + } + + /** @description Partially update a model */ + static patchModel({ + userJwt, + subdomain, + modelId, + data, + response, + }: { + userJwt: z.infer; + subdomain: string; + modelId: string; + data: Record; + response: FastifyReply; + } & z.infer) { + return ModelsService.patchModel({ + userJwt, + subdomain, + modelId, + data, + response, + }); + } + + /** @description Delete a model */ + static deleteModel({ + userJwt, + subdomain, + modelId, + response, + }: { + userJwt: z.infer; + subdomain: string; + modelId: string; + response: FastifyReply; + } & z.infer) { + return ModelsService.deleteModel({ userJwt, subdomain, modelId, response }); + } + + /** @description Assign an uploaded image to a model */ + static createModelImage({ + userJwt, + subdomain, + modelId, + data, + response, + }: { + userJwt: z.infer; + subdomain: string; + modelId: string; + data: z.infer; + response: FastifyReply; + }) { + return ModelsService.createModelImage({ + userJwt, + subdomain, + modelId, + data, + response, + }); + } + + /** @description Delete a model image */ + static deleteModelImage({ + userJwt, + subdomain, + modelId, + imageId, + response, + }: { + userJwt: z.infer; + subdomain: string; + modelId: string; + imageId: string; + response: FastifyReply; + }) { + return ModelsService.deleteModelImage({ + userJwt, + subdomain, + modelId, + imageId, + response, + }); + } +} diff --git a/apps/server/src/modules/models/errors/index.ts b/apps/server/src/modules/models/errors/index.ts new file mode 100644 index 0000000..57f3feb --- /dev/null +++ b/apps/server/src/modules/models/errors/index.ts @@ -0,0 +1,45 @@ +import { defineErrors } from "../../../core/utils/errors"; + +/** @description Error definitions for the models module */ +export const modelsErrors = defineErrors({ + MODEL_NOT_FOUND: { + code: "model_not_found", + message: "Model not found.", + status: 404, + }, + MODEL_PAGE_OUT_OF_BOUNDS: { + code: "page_out_of_bounds", + message: "The requested page exceeds the total number of pages.", + status: 416, + }, + MODEL_COMPANY_NOT_FOUND: { + code: "company_not_found", + message: "There's no companies bound to your account", + status: 404, + }, + MODEL_NOT_ALLOWED: { + code: "not_allowed", + message: "You are not authorized to access this company.", + status: 403, + }, + MODEL_SLUG_CONFLICT: { + code: "model_slug_conflict", + message: "A model with this slug already exists.", + status: 409, + }, + MODEL_MAKER_NOT_FOUND: { + code: "maker_not_found", + message: "The specified maker does not exist.", + status: 404, + }, + MODEL_IMAGE_NOT_FOUND: { + code: "model_image_not_found", + message: "Model image not found.", + status: 404, + }, + MODEL_IMAGE_KEY_MISMATCH: { + code: "model_image_key_mismatch", + message: "The provided image key does not belong to your company.", + status: 403, + }, +}); diff --git a/apps/server/src/modules/models/repositories/index.ts b/apps/server/src/modules/models/repositories/index.ts new file mode 100644 index 0000000..a5ee04c --- /dev/null +++ b/apps/server/src/modules/models/repositories/index.ts @@ -0,0 +1,511 @@ +import { DeleteObjectCommand } from "@aws-sdk/client-s3"; +import { + and, + asc, + db, + desc, + eq, + inArray, + or, + type SQL, + sql, +} from "@fixr/db/connection"; +import { + type ModelImageSelect, + modelCategories, + modelImages, + modelMakers, + models, +} from "@fixr/db/schema"; +import { + generatePresignedGetUrl, + r2Bucket, + r2Client, +} from "../../../config/r2"; + +const FTS_OPERATOR_REGEX = /[+\-*~()<>@]/; +const WHITESPACE_REGEX = /\s+/; + +/** @description Column selection for paginated models minimal list */ +export const modelMinimalListSelect = { + id: models.id, + name: models.name, + slug: models.slug, + status: models.status, + price: models.price, + released: models.released, + makerId: modelMakers.id, + makerName: modelMakers.name, + makerSlug: modelMakers.slug, + categoryId: modelCategories.id, + categoryName: modelCategories.name, + categorySlug: modelCategories.slug, +}; + +/** @description Join definitions for models list queries */ +export const modelListJoins = [ + { + type: "inner" as const, + table: modelMakers, + on: eq(modelMakers.id, models.makerId), + }, + { + type: "left" as const, + table: modelCategories, + on: eq(modelCategories.id, models.categoryId), + }, +]; + +export interface ModelFlatRecord { + id: string; + name: string; + slug: string; + status: string | null; + price: string | null; + released: string | null; + makerId: string; + makerName: string; + makerSlug: string; + categoryId: string | null; + categoryName: string | null; + categorySlug: string | null; +} + +export interface ModelListRecord { + id: string; + name: string; + slug: string; + status: string; + price: string | null; + released: string | null; + maker: { id: string; name: string; slug: string }; + category: { id: string; name: string; slug: string } | null; + imageUrl: string | null; +} + +/** @description Data access layer for device models */ +export class ModelsRepository { + /** + * Build a WHERE clause for filtering models with fulltext search support + * + * @param companyId - Company ID for scoping + * @param filters - Query, maker, category, and status filters + */ + static buildListFilter( + companyId: string, + filters: { + query?: string; + makerId?: string; + categoryId?: string; + status?: string; + } + ) { + const conditions: SQL[] = [ + or(eq(models.companyId, companyId), sql`${models.companyId} IS NULL`)!, + ]; + + if (filters.makerId) { + conditions.push(eq(models.makerId, filters.makerId)); + } + if (filters.categoryId) { + conditions.push(eq(models.categoryId, filters.categoryId)); + } + if (filters.status) { + conditions.push(eq(models.status, filters.status)); + } + if (filters.query) { + const hasOperators = FTS_OPERATOR_REGEX.test(filters.query); + const ftsQuery = hasOperators + ? filters.query + : filters.query + .trim() + .split(WHITESPACE_REGEX) + .map((w) => `${w}*`) + .join(" "); + conditions.push( + sql`MATCH(${models.name}, ${models.modelsText}, ${models.chipset}, ${models.cpu}, ${models.internalMemory}, ${models.os}) AGAINST(${ftsQuery} IN BOOLEAN MODE)` + ); + } + + return and(...conditions)!; + } + + /** + * Build an ORDER BY clause for models + * + * @param sort - Sort key: newer, older, name + */ + static buildOrder(sort?: string) { + switch (sort) { + case "newer": + return desc(models.createdAt); + case "older": + return asc(models.createdAt); + case "name": + return asc(models.name); + default: + return desc(models.createdAt); + } + } + + /** + * Find a model by its slug with full maker and category relations + * + * @param slug - The model slug + * @param companyId - Optional company ID for scoping + * @returns The model record with relations or null + */ + static async queryModelBySlug(slug: string, companyId?: string) { + const conditions: SQL[] = [eq(models.slug, slug)]; + if (companyId) { + conditions.push( + or(eq(models.companyId, companyId), sql`${models.companyId} IS NULL`)! + ); + } + + const result = await db + .select({ + id: models.id, + makerId: models.makerId, + name: models.name, + slug: models.slug, + url: models.url, + categoryId: models.categoryId, + announced: models.announced, + status: models.status, + dimensions: models.dimensions, + weight: models.weight, + build: models.build, + sim: models.sim, + displayType: models.displayType, + displaySize: models.displaySize, + displayResolution: models.displayResolution, + displayProtection: models.displayProtection, + os: models.os, + chipset: models.chipset, + cpu: models.cpu, + gpu: models.gpu, + cardSlot: models.cardSlot, + internalMemory: models.internalMemory, + mainCamera: models.mainCamera, + mainCameraFeatures: models.mainCameraFeatures, + mainCameraVideo: models.mainCameraVideo, + selfieCamera: models.selfieCamera, + selfieFeatures: models.selfieFeatures, + selfieVideo: models.selfieVideo, + battery: models.battery, + batteryCharging: models.batteryCharging, + networkTech: models.networkTech, + sensors: models.sensors, + colors: models.colors, + colorsHex: models.colorsHex, + modelsText: models.modelsText, + price: models.price, + dimensionsWidth: models.dimensionsWidth, + dimensionsHeight: models.dimensionsHeight, + dimensionsThickness: models.dimensionsThickness, + weightGrams: models.weightGrams, + displaySizeInches: models.displaySizeInches, + displaySizeRatio: models.displaySizeRatio, + displayResWidth: models.displayResWidth, + displayResHeight: models.displayResHeight, + displayResPpi: models.displayResPpi, + released: models.released, + meta: models.meta, + companyId: models.companyId, + createdAt: models.createdAt, + maker: { + id: modelMakers.id, + name: modelMakers.name, + slug: modelMakers.slug, + url: modelMakers.url, + }, + category: { + id: modelCategories.id, + name: modelCategories.name, + slug: modelCategories.slug, + }, + }) + .from(models) + .innerJoin(modelMakers, eq(modelMakers.id, models.makerId)) + .leftJoin(modelCategories, eq(modelCategories.id, models.categoryId)) + .where(and(...conditions)) + .limit(1); + + return result[0] ?? null; + } + + /** + * Query all images for a model, ordered by position + * + * @param modelId - The model ID + * @returns Array of model images + */ + static async queryModelImages(modelId: string) { + return await db + .select() + .from(modelImages) + .where(eq(modelImages.modelId, modelId)) + .orderBy(asc(modelImages.position)); + } + + /** + * Batch fetch primary model images for a set of model IDs + * + * @param modelIds - Array of model IDs + * @returns Map of modelId -> r2Key + */ + static async queryPrimaryImages( + modelIds: string[] + ): Promise> { + if (modelIds.length === 0) return new Map(); + const rows = await db + .select({ + modelId: modelImages.modelId, + r2Key: modelImages.r2Key, + }) + .from(modelImages) + .where( + and( + inArray(modelImages.modelId, modelIds), + eq(modelImages.isPrimary, true) + ) + ); + return new Map( + rows.filter((r) => !!r.r2Key).map((r) => [r.modelId, r.r2Key!]) + ); + } + + /** + * Generate a presigned GET URL for an R2 key + * + * @param r2Key - The R2 object key + * @returns Presigned URL or null + */ + static async generateImagePresignedUrl(r2Key: string | null) { + if (!r2Key) return null; + return await generatePresignedGetUrl(r2Key); + } + + /** + * Attach presigned GET URLs to an array of model images + * + * @param images - Array of model image records + * @returns Images with presignedUrl attached + */ + static async attachPresignedUrlsToImages( + images: ModelImageSelect[] + ): Promise<(ModelImageSelect & { presignedUrl: string | null })[]> { + return await Promise.all( + images.map(async (img) => { + const presignedUrl = img.r2Key + ? await generatePresignedGetUrl(img.r2Key) + : null; + return { ...img, presignedUrl }; + }) + ); + } + + /** + * Find a model by its ID with full maker and category relations + * + * @param id - The model ID + * @returns The model record with relations or null + */ + static async queryModelById(id: string) { + const [model] = await db + .select({ + id: models.id, + makerId: models.makerId, + name: models.name, + slug: models.slug, + url: models.url, + categoryId: models.categoryId, + announced: models.announced, + status: models.status, + dimensions: models.dimensions, + weight: models.weight, + build: models.build, + sim: models.sim, + displayType: models.displayType, + displaySize: models.displaySize, + displayResolution: models.displayResolution, + displayProtection: models.displayProtection, + os: models.os, + chipset: models.chipset, + cpu: models.cpu, + gpu: models.gpu, + cardSlot: models.cardSlot, + internalMemory: models.internalMemory, + mainCamera: models.mainCamera, + mainCameraFeatures: models.mainCameraFeatures, + mainCameraVideo: models.mainCameraVideo, + selfieCamera: models.selfieCamera, + selfieFeatures: models.selfieFeatures, + selfieVideo: models.selfieVideo, + battery: models.battery, + batteryCharging: models.batteryCharging, + networkTech: models.networkTech, + sensors: models.sensors, + colors: models.colors, + colorsHex: models.colorsHex, + modelsText: models.modelsText, + price: models.price, + dimensionsWidth: models.dimensionsWidth, + dimensionsHeight: models.dimensionsHeight, + dimensionsThickness: models.dimensionsThickness, + weightGrams: models.weightGrams, + displaySizeInches: models.displaySizeInches, + displaySizeRatio: models.displaySizeRatio, + displayResWidth: models.displayResWidth, + displayResHeight: models.displayResHeight, + displayResPpi: models.displayResPpi, + released: models.released, + meta: models.meta, + companyId: models.companyId, + createdAt: models.createdAt, + maker: { + id: modelMakers.id, + name: modelMakers.name, + slug: modelMakers.slug, + url: modelMakers.url, + }, + category: { + id: modelCategories.id, + name: modelCategories.name, + slug: modelCategories.slug, + }, + }) + .from(models) + .innerJoin(modelMakers, eq(modelMakers.id, models.makerId)) + .leftJoin(modelCategories, eq(modelCategories.id, models.categoryId)) + .where(eq(models.id, id)) + .limit(1); + return model ?? null; + } + + /** + * Check if a slug already exists for a given company + * + * @param slug - The slug to check + * @param companyId - Company ID for scoping + * @returns The matching model or undefined + */ + static async queryBySlugAndCompany(slug: string, companyId: string) { + const [model] = await db + .select({ id: models.id }) + .from(models) + .where( + and( + eq(models.slug, slug), + or(eq(models.companyId, companyId), sql`${models.companyId} IS NULL`) + ) + ) + .limit(1); + return model; + } + + /** + * Check if a maker exists by ID + * + * @param makerId - The maker ID + * @returns The maker or undefined + */ + static async queryMakerById(makerId: string) { + const [maker] = await db + .select({ id: modelMakers.id }) + .from(modelMakers) + .where(eq(modelMakers.id, makerId)) + .limit(1); + return maker; + } + + /** + * Insert a new model record + * + * @param data - The model data to insert + */ + static async insertModel(data: typeof models.$inferInsert) { + await db.insert(models).values(data); + } + + /** + * Insert a model image record + * + * @param data - The model image data + * @returns The created model image + */ + static async insertModelImage(data: typeof modelImages.$inferInsert) { + const id = data.id as string; + await db.insert(modelImages).values(data); + const [created] = await db + .select() + .from(modelImages) + .where(eq(modelImages.id, id)) + .limit(1); + return created; + } + + /** + * Delete a single model image record + * + * @param imageId - The image ID + */ + static async deleteModelImageRecord(imageId: string) { + await db.delete(modelImages).where(eq(modelImages.id, imageId)); + } + + /** + * Get all R2 keys associated with a model (model images + the model's own imageLocalPath) + * + * @param modelId - The model ID + * @returns Array of R2 keys + */ + static async queryR2KeysByModel(modelId: string) { + const imageKeys = await db + .select({ key: modelImages.r2Key }) + .from(modelImages) + .where(eq(modelImages.modelId, modelId)); + + return imageKeys.map((img) => img.key).filter((k): k is string => !!k); + } + + /** + * Delete an R2 object by key + * + * @param key - The R2 object key + */ + static async deleteR2Object(key: string) { + await r2Client.send( + new DeleteObjectCommand({ + Bucket: r2Bucket, + Key: key, + }) + ); + } + + /** + * Update a model record (partial) + * + * @param id - The model ID + * @param data - The fields to update + */ + static async updateModel( + id: string, + data: Partial + ) { + await db.update(models).set(data).where(eq(models.id, id)); + } + + /** + * Delete a model record and its associated images + * + * @param id - The model ID + */ + static async deleteModel(id: string) { + const keys = await ModelsRepository.queryR2KeysByModel(id); + await Promise.all(keys.map((k) => ModelsRepository.deleteR2Object(k))); + await db.delete(modelImages).where(eq(modelImages.modelId, id)); + await db.delete(models).where(eq(models.id, id)); + } +} diff --git a/apps/server/src/modules/models/routes/index.ts b/apps/server/src/modules/models/routes/index.ts new file mode 100644 index 0000000..18aecd7 --- /dev/null +++ b/apps/server/src/modules/models/routes/index.ts @@ -0,0 +1,188 @@ +import { permissions } from "@fixr/permissions"; +import type { userJWT } from "@fixr/schemas/auth"; +import { getCompanyNestedDataSchema } from "@fixr/schemas/companies"; +import { + createModelBodySchema, + createModelImageBodySchema, + getModelBySlugParamsSchema, + getModelsQuerySchema, + modelIdParamsSchema, + modelImageParamsSchema, + patchModelBodySchema, +} from "@fixr/schemas/models"; +import type { z } from "zod"; +import { modelsDocs } from "../../../core/docs/models/models.docs"; +import type { FastifyTypedInstance } from "../../../core/interfaces/fastify"; +import { authenticateEmployee } from "../../../core/middlewares/authenticate-employee"; +import { requirePermission } from "../../../core/middlewares/rbac"; +import { withErrorHandler } from "../../../core/middlewares/with-error-handler"; +import { ModelsController } from "../controllers"; + +/** @description Models routes plugin */ +export function modelsRoutes(fastify: FastifyTypedInstance) { + fastify.get( + "/", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: modelsDocs.listModelsSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const query = getModelsQuerySchema.parse(request.query); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.listModels({ + userJwt, + subdomain, + response, + ...query, + }); + }) + ); + + fastify.get( + "/:slug", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.read), + ], + schema: modelsDocs.getModelBySlugSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const { slug } = getModelBySlugParamsSchema.parse(request.params); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.getModelBySlug({ + userJwt, + subdomain, + slug, + response, + }); + }) + ); + + fastify.post( + "/", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.create), + ], + schema: modelsDocs.createModelSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const data = createModelBodySchema.parse(request.body); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.createModel({ + userJwt, + subdomain, + data, + response, + }); + }) + ); + + fastify.patch( + "/:modelId", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.update), + ], + schema: modelsDocs.patchModelSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const { modelId } = modelIdParamsSchema.parse(request.params); + const data = patchModelBodySchema.parse(request.body); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.patchModel({ + userJwt, + subdomain, + modelId, + data, + response, + }); + }) + ); + + fastify.delete( + "/:modelId", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.delete), + ], + schema: modelsDocs.deleteModelSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const { modelId } = modelIdParamsSchema.parse(request.params); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.deleteModel({ + userJwt, + subdomain, + modelId, + response, + }); + }) + ); + + fastify.post( + "/:modelId/images", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.update), + ], + schema: modelsDocs.createModelImageSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const { modelId } = modelIdParamsSchema.parse(request.params); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + const data = createModelImageBodySchema.parse(request.body); + + await ModelsController.createModelImage({ + userJwt, + subdomain, + modelId, + data, + response, + }); + }) + ); + + fastify.delete( + "/:modelId/images/:imageId", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.update), + ], + schema: modelsDocs.deleteModelImageSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const { modelId, imageId } = modelImageParamsSchema.parse(request.params); + const { subdomain } = getCompanyNestedDataSchema.parse(request.params); + + await ModelsController.deleteModelImage({ + userJwt, + subdomain, + modelId, + imageId, + response, + }); + }) + ); +} diff --git a/apps/server/src/modules/models/services/index.ts b/apps/server/src/modules/models/services/index.ts new file mode 100644 index 0000000..95bb192 --- /dev/null +++ b/apps/server/src/modules/models/services/index.ts @@ -0,0 +1,552 @@ +import { slugify } from "@fixr/constants/slug"; +import { models } from "@fixr/db/schema"; +import type { + createModelBodySchema, + createModelImageBodySchema, +} from "@fixr/schemas/models"; +import { createId } from "@paralleldrive/cuid2"; +import type { FastifyReply } from "fastify"; +import type { z } from "zod"; +import { AppError } from "../../../core/lib/app-error"; +import { + getPaginatedCount, + getPaginatedRecords, +} from "../../../core/lib/pagination"; +import { apiResponse, paginatedData } from "../../../core/lib/response"; +import { + type ModelFlatRecord, + type ModelListRecord, + ModelsRepository, + modelListJoins, + modelMinimalListSelect, +} from "../repositories"; + +/** @description Business logic for device models */ +export class ModelsService { + private static buildCreateModelValues( + data: z.infer, + modelId: string, + companyId: string, + slug: string + ): typeof models.$inferInsert { + return { + id: modelId, + slug, + url: `/models/${slug}`, + companyId, + ...Object.fromEntries( + Object.entries(data).filter(([_, v]) => v !== undefined) + ), + } as typeof models.$inferInsert; + } + /** + * List models with pagination, fulltext search, and filters + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param page - Current page number + * @param perPage - Items per page + * @param query - Fulltext search query + * @param makerId - Filter by maker + * @param categoryId - Filter by category + * @param status - Filter by release status + * @param sort - Sort direction + * @param response - Fastify reply + */ + static async listModels({ + userJwt, + subdomain, + page, + perPage, + query, + makerId, + categoryId, + status, + sort, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + page: number; + perPage?: number; + query?: string; + makerId?: string; + categoryId?: string; + status?: string; + sort?: string; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const companyId = userJwt.company.id; + const PER_PAGE = perPage ?? 10; + + const filter = ModelsRepository.buildListFilter(companyId, { + query, + makerId, + categoryId, + status, + }); + const order = ModelsRepository.buildOrder(sort); + + const [records, totalRecords] = await Promise.all([ + getPaginatedRecords({ + table: models, + select: modelMinimalListSelect, + skip: (page - 1) * PER_PAGE, + take: PER_PAGE, + where: filter, + order, + joins: modelListJoins, + }), + getPaginatedCount({ + table: models, + where: filter, + }), + ]); + + if (totalRecords === 0) { + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + message: "Models successfully retrieved.", + code: "list_models_success", + data: paginatedData({ + records: [], + pagination: { + total_records: 0, + total_pages: 0, + current_page: 1, + next_page: null, + prev_page: null, + }, + }), + }) + ); + } + + const total_pages = Math.ceil(totalRecords / PER_PAGE); + + if (page > total_pages) { + throw new AppError("MODEL_PAGE_OUT_OF_BOUNDS"); + } + + const next_page = + PER_PAGE * (page - 1) + records.length < totalRecords ? page + 1 : null; + + const modelIds = (records as ModelFlatRecord[]).map((r) => r.id); + const primaryImageMap = await ModelsRepository.queryPrimaryImages(modelIds); + + const recordsWithImages: ModelListRecord[] = await Promise.all( + (records as ModelFlatRecord[]).map(async (r) => ({ + id: r.id, + name: r.name, + slug: r.slug, + status: r.status ?? "Available", + price: r.price, + released: r.released, + maker: { + id: r.makerId, + name: r.makerName, + slug: r.makerSlug, + }, + category: r.categoryId + ? { + id: r.categoryId, + name: r.categoryName!, + slug: r.categorySlug!, + } + : null, + imageUrl: await ModelsRepository.generateImagePresignedUrl( + primaryImageMap.get(r.id) ?? null + ), + })) + ); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + message: "Models successfully retrieved.", + code: "list_models_success", + data: paginatedData({ + records: recordsWithImages, + pagination: { + total_records: totalRecords, + total_pages, + current_page: page, + next_page, + prev_page: page > 1 ? page - 1 : null, + }, + }), + }) + ); + } + + /** + * Get a single model by slug with full details and presigned image URLs + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param slug - The model slug + * @param response - Fastify reply + */ + static async getModelBySlug({ + userJwt, + subdomain, + slug, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + slug: string; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const model = await ModelsRepository.queryModelBySlug( + slug, + userJwt.company.id + ); + + if (!model) { + throw new AppError("MODEL_NOT_FOUND"); + } + + const images = await ModelsRepository.queryModelImages(model.id); + + const primaryImage = images.find((img) => img.isPrimary); + const [imageUrl, imagesWithPresignedUrls] = await Promise.all([ + ModelsRepository.generateImagePresignedUrl(primaryImage?.r2Key ?? null), + ModelsRepository.attachPresignedUrlsToImages(images), + ]); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "get_model_success", + message: "Model retrieved successfully.", + data: { + ...model, + status: model.status ?? "Available", + imageUrl, + images: imagesWithPresignedUrls, + }, + }) + ); + } + + /** + * Create a new model + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param data - The model data + * @param response - Fastify reply + */ + static async createModel({ + userJwt, + subdomain, + data, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + data: z.infer; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const maker = await ModelsRepository.queryMakerById(data.makerId); + + if (!maker) { + throw new AppError("MODEL_MAKER_NOT_FOUND"); + } + + const slug = slugify(data.name); + + const existing = await ModelsRepository.queryBySlugAndCompany( + slug, + userJwt.company.id + ); + + if (existing) { + throw new AppError("MODEL_SLUG_CONFLICT"); + } + + const modelId = createId(); + const companyId = userJwt.company.id; + + const values = ModelsService.buildCreateModelValues( + data, + modelId, + companyId, + slug + ); + await ModelsRepository.insertModel(values); + + return response.status(201).send( + apiResponse({ + status: 201, + error: null, + code: "create_model_success", + message: "Model created successfully.", + data: { id: modelId, name: data.name, slug }, + }) + ); + } + + /** + * Partially update a model + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param modelId - The model ID + * @param data - The fields to update + * @param response - Fastify reply + */ + static async patchModel({ + userJwt, + subdomain, + modelId, + data, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + modelId: string; + data: Record; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const model = await ModelsRepository.queryModelById(modelId); + + if (!model) { + throw new AppError("MODEL_NOT_FOUND"); + } + + const updateData: Record = {}; + + for (const [key, value] of Object.entries(data)) { + updateData[key] = value ?? null; + } + + if (Object.keys(updateData).length > 0) { + await ModelsRepository.updateModel(modelId, updateData); + } + + const updated = await ModelsRepository.queryModelById(modelId); + + const images = await ModelsRepository.queryModelImages(model.id); + + const primaryImage = images.find((img) => img.isPrimary); + const [imageUrl, imagesWithPresignedUrls] = await Promise.all([ + ModelsRepository.generateImagePresignedUrl(primaryImage?.r2Key ?? null), + ModelsRepository.attachPresignedUrlsToImages(images), + ]); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "patch_model_success", + message: "Model updated successfully.", + data: { + ...(updated ?? model), + status: (updated ?? model).status ?? "Available", + imageUrl, + images: imagesWithPresignedUrls, + }, + }) + ); + } + + /** + * Delete a model + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param modelId - The model ID + * @param response - Fastify reply + */ + static async deleteModel({ + userJwt, + subdomain, + modelId, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + modelId: string; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const model = await ModelsRepository.queryModelById(modelId); + if (!model) { + throw new AppError("MODEL_NOT_FOUND"); + } + + await ModelsRepository.deleteModel(model.id); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "delete_model_success", + message: "Model deleted successfully.", + data: null, + }) + ); + } + + /** + * Assign an uploaded image to a model + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param modelId - The model ID + * @param data - The image data + * @param response - Fastify reply + */ + static async createModelImage({ + userJwt, + subdomain, + modelId, + data, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + modelId: string; + data: z.infer; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const model = await ModelsRepository.queryModelById(modelId); + if (!model) { + throw new AppError("MODEL_NOT_FOUND"); + } + + const expectedPrefix = `companies/${userJwt.company.id}/models/`; + if (!data.r2Key.startsWith(expectedPrefix)) { + throw new AppError("MODEL_IMAGE_KEY_MISMATCH"); + } + + const imageId = createId(); + const image = await ModelsRepository.insertModelImage({ + id: imageId, + modelId: model.id, + r2Key: data.r2Key, + isPrimary: data.isPrimary ?? false, + variant: data.variant ?? null, + position: data.position ?? 0, + }); + + const [imageWithPresignedUrl] = + await ModelsRepository.attachPresignedUrlsToImages([image!]); + + return response.status(201).send( + apiResponse({ + status: 201, + error: null, + code: "create_model_image_success", + message: "Model image created successfully.", + data: imageWithPresignedUrl, + }) + ); + } + + /** + * Delete a model image + * + * @param userJwt - Authenticated user JWT payload + * @param subdomain - Company subdomain + * @param modelId - The model ID + * @param imageId - The image ID + * @param response - Fastify reply + */ + static async deleteModelImage({ + userJwt, + subdomain, + modelId, + imageId, + response, + }: { + userJwt: { id: string; company?: { id: string; subdomain: string } }; + subdomain: string; + modelId: string; + imageId: string; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("MODEL_COMPANY_NOT_FOUND"); + } + if (userJwt.company.subdomain !== subdomain) { + throw new AppError("MODEL_NOT_ALLOWED"); + } + + const model = await ModelsRepository.queryModelById(modelId); + if (!model) { + throw new AppError("MODEL_NOT_FOUND"); + } + + const images = await ModelsRepository.queryModelImages(model.id); + const image = images.find((img) => img.id === imageId); + if (!image) { + throw new AppError("MODEL_IMAGE_NOT_FOUND"); + } + + if (image.r2Key) { + await ModelsRepository.deleteR2Object(image.r2Key); + } + await ModelsRepository.deleteModelImageRecord(imageId); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "delete_model_image_success", + message: "Model image deleted successfully.", + data: null, + }) + ); + } +} diff --git a/apps/server/src/modules/uploads/controllers/index.ts b/apps/server/src/modules/uploads/controllers/index.ts index 48e0b5d..87d66ea 100644 --- a/apps/server/src/modules/uploads/controllers/index.ts +++ b/apps/server/src/modules/uploads/controllers/index.ts @@ -1,5 +1,8 @@ import type { jwtPayload } from "@fixr/schemas/auth"; -import type { createUploadPresignSchema } from "@fixr/schemas/uploads"; +import type { + createModelImageUploadPresignSchema, + createUploadPresignSchema, +} from "@fixr/schemas/uploads"; import type { FastifyReply } from "fastify"; import type { z } from "zod"; import { UploadsService } from "../services"; @@ -21,4 +24,20 @@ export class UploadsController { response, }); } + + static createModelImagePresign({ + userJwt, + data, + response, + }: { + userJwt: z.infer; + data: z.infer; + response: FastifyReply; + }) { + return UploadsService.createModelImagePresign({ + userJwt, + data, + response, + }); + } } diff --git a/apps/server/src/modules/uploads/repositories/index.ts b/apps/server/src/modules/uploads/repositories/index.ts index 1596c7d..a11215c 100644 --- a/apps/server/src/modules/uploads/repositories/index.ts +++ b/apps/server/src/modules/uploads/repositories/index.ts @@ -5,6 +5,7 @@ import { uploads } from "@fixr/db/schema"; import type { createUploadPresignSchema } from "@fixr/schemas/uploads"; import type { z } from "zod"; import { + buildModelObjectKey, buildObjectPublicUrl, buildUploadObjectKey, r2Bucket, @@ -58,4 +59,54 @@ export class UploadsRepository { expiresIn: r2PresignExpiresIn, }; } + + static async createModelPresignedUpload({ + companyId, + employeeId, + fileName, + contentType, + size, + }: { + companyId: string; + employeeId: string; + fileName: string; + contentType: string; + size: number; + }) { + const key = buildModelObjectKey({ companyId, fileName }); + const url = buildObjectPublicUrl(key); + + const command = new PutObjectCommand({ + Bucket: r2Bucket, + Key: key, + ContentType: contentType, + ContentLength: size, + }); + + const uploadUrl = await getSignedUrl(r2Client, command, { + expiresIn: r2PresignExpiresIn, + }); + + const [record] = await db + .insert(uploads) + .values({ + companyId, + employeeId, + key, + url, + fileName, + contentType, + sizeInBytes: size, + status: "pending", + }) + .$returningId(); + + return { + id: record.id, + uploadUrl, + key, + url, + expiresIn: r2PresignExpiresIn, + }; + } } diff --git a/apps/server/src/modules/uploads/routes/index.ts b/apps/server/src/modules/uploads/routes/index.ts index 8bf4698..ad6fe85 100644 --- a/apps/server/src/modules/uploads/routes/index.ts +++ b/apps/server/src/modules/uploads/routes/index.ts @@ -1,6 +1,9 @@ import { permissions } from "@fixr/permissions"; import type { userJWT } from "@fixr/schemas/auth"; -import { createUploadPresignSchema } from "@fixr/schemas/uploads"; +import { + createModelImageUploadPresignSchema, + createUploadPresignSchema, +} from "@fixr/schemas/uploads"; import type { z } from "zod"; import { uploadsDocs } from "../../../core/docs/uploads.docs"; import type { FastifyTypedInstance } from "../../../core/interfaces/fastify"; @@ -30,4 +33,25 @@ export function uploadsRoutes(fastify: FastifyTypedInstance) { }); }) ); + + fastify.post( + "/models/presign", + { + preHandler: [ + authenticateEmployee, + requirePermission(permissions.devices.update), + ], + schema: uploadsDocs.createModelImagePresignSchema, + }, + withErrorHandler(async (request, response) => { + const userJwt = request.user as z.infer; + const body = createModelImageUploadPresignSchema.parse(request.body); + + await UploadsController.createModelImagePresign({ + userJwt, + data: body, + response, + }); + }) + ); } diff --git a/apps/server/src/modules/uploads/services/index.ts b/apps/server/src/modules/uploads/services/index.ts index b390bb3..ac0846f 100644 --- a/apps/server/src/modules/uploads/services/index.ts +++ b/apps/server/src/modules/uploads/services/index.ts @@ -48,4 +48,46 @@ export class UploadsService { }) ); } + + static async createModelImagePresign({ + userJwt, + data, + response, + }: { + userJwt: z.infer; + data: { fileName: string; contentType: string; size: number }; + response: FastifyReply; + }) { + if (!userJwt.company) { + throw new AppError("UPLOAD_COMPANY_NOT_FOUND"); + } + + const [employee] = await db + .select() + .from(employees) + .where(eq(employees.userId, userJwt.id)) + .limit(1); + + if (!employee || employee.companyId !== userJwt.company.id) { + throw new AppError("UPLOAD_COMPANY_NOT_FOUND"); + } + + const presign = await UploadsRepository.createModelPresignedUpload({ + companyId: userJwt.company.id, + employeeId: employee.id, + fileName: data.fileName, + contentType: data.contentType, + size: data.size, + }); + + return response.status(200).send( + apiResponse({ + status: 200, + error: null, + code: "create_model_image_presign_success", + message: "Upload URL generated successfully.", + data: presign, + }) + ); + } } diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 4546008..82b5670 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -30,9 +30,12 @@ import { AppError } from "./core/lib/app-error"; import { apiResponse } from "./core/lib/response"; import { accountRoutes } from "./modules/account/routes"; import { authRoutes } from "./modules/auth/routes"; +import { categoriesRoutes } from "./modules/categories/routes"; import { companiesRoutes } from "./modules/companies/routes"; import { credentialsRoutes } from "./modules/credentials/routes"; import { employeesRoutes } from "./modules/employees/routes"; +import { makersRoutes } from "./modules/makers/routes"; +import { modelsRoutes } from "./modules/models/routes"; import { serviceOrdersRoutes } from "./modules/service-orders/routes"; import { uploadsRoutes } from "./modules/uploads/routes"; @@ -155,6 +158,11 @@ async function registerPlugins() { name: "Uploads", description: "Pre-signed uploads to Cloudflare R2.", }, + { + name: "Devices", + description: + "Device catalog management: categories, makers, and models.", + }, ], security: [], components: { @@ -212,6 +220,18 @@ async function registerPlugins() { prefix: "/companies/:subdomain/service-orders", }); + await server.register(categoriesRoutes, { + prefix: "/companies/:subdomain/categories", + }); + + await server.register(makersRoutes, { + prefix: "/companies/:subdomain/makers", + }); + + await server.register(modelsRoutes, { + prefix: "/companies/:subdomain/models", + }); + await server.register(uploadsRoutes, { prefix: "/uploads", }); @@ -227,7 +247,7 @@ async function registerPlugins() { server.swagger() ); - // Scalar UI — register after all routes so the spec is complete. + // Scalar UI: register after all routes so the spec is complete. await server.register(scalarUi, { routePrefix: "/docs", configuration: { diff --git a/apps/web/app/(public)/downtime/page.tsx b/apps/web/app/(public)/downtime/page.tsx index 73c4f01..f4ae3d2 100644 --- a/apps/web/app/(public)/downtime/page.tsx +++ b/apps/web/app/(public)/downtime/page.tsx @@ -28,8 +28,8 @@ export default function DowntimePage() { projeto interdisciplinar desenvolvido na FAM (Faculdade das Américas) - . Sua infraestrutura envolve diversos serviços — como o servidor da - API, banco de dados e sistema de cache — todos hospedados em uma{" "} + . Sua infraestrutura envolve diversos serviços: como o servidor da + API, banco de dados e sistema de cache: todos hospedados em uma{" "} VPS privada.

diff --git a/package.json b/package.json index 017ae81..e9bbae4 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "db:studio": "bun run --elide-lines 0 --filter @fixr/db db:studio", "db:generate": "bun run --elide-lines 0 --filter @fixr/db db:generate", "db:migrate": "bun run --elide-lines 0 --filter @fixr/db db:migrate", + "db:seed": "bun run --elide-lines 0 --filter @fixr/db db:seed", "db:start": "bun run --elide-lines 0 --filter @fixr/db db:start", "db:watch": "bun run --elide-lines 0 --filter @fixr/db db:watch", "db:stop": "bun run --elide-lines 0 --filter @fixr/db db:stop", diff --git a/packages/constants/package.json b/packages/constants/package.json index 3645f15..31731f9 100644 --- a/packages/constants/package.json +++ b/packages/constants/package.json @@ -33,6 +33,10 @@ "./enforcements": { "types": "./src/enforcements.ts", "default": "./src/enforcements.ts" + }, + "./slug": { + "types": "./src/slug.ts", + "default": "./src/slug.ts" } }, "devDependencies": { diff --git a/packages/constants/src/slug.ts b/packages/constants/src/slug.ts new file mode 100644 index 0000000..0240373 --- /dev/null +++ b/packages/constants/src/slug.ts @@ -0,0 +1,13 @@ +const SLUGIFY_SPACE_REGEX = /\s+/g; +const SLUGIFY_SPECIAL_REGEX = /[^\w-]/g; +const SLUGIFY_DUPLICATE_DASHES = /-+/g; +const SLUGIFY_TRIM_DASHES = /^-|-$/g; + +export function slugify(text: string): string { + return text + .toLowerCase() + .replace(SLUGIFY_SPACE_REGEX, "-") + .replace(SLUGIFY_SPECIAL_REGEX, "") + .replace(SLUGIFY_DUPLICATE_DASHES, "-") + .replace(SLUGIFY_TRIM_DASHES, ""); +} diff --git a/packages/db/drizzle/0006_awesome_cloak.sql b/packages/db/drizzle/0006_awesome_cloak.sql new file mode 100644 index 0000000..d05a057 --- /dev/null +++ b/packages/db/drizzle/0006_awesome_cloak.sql @@ -0,0 +1,76 @@ +CREATE TABLE `model_categories` ( + `id` varchar(25) NOT NULL, + `name` varchar(100) NOT NULL, + `slug` varchar(100) NOT NULL, + CONSTRAINT `model_categories_id` PRIMARY KEY(`id`), + CONSTRAINT `model_categories_slug_unique` UNIQUE(`slug`) +); +--> statement-breakpoint +CREATE TABLE `model_makers` ( + `id` varchar(25) NOT NULL, + `name` varchar(100) NOT NULL, + `slug` varchar(100) NOT NULL, + `url` varchar(255) NOT NULL, + `device_count` int NOT NULL DEFAULT 0, + `page_count` int, + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `model_makers_id` PRIMARY KEY(`id`), + CONSTRAINT `model_makers_slug_unique` UNIQUE(`slug`) +); +--> statement-breakpoint +CREATE TABLE `service_order_images` ( + `id` varchar(25) NOT NULL, + `service_order_id` varchar(25) NOT NULL, + `employee_id` varchar(25) NOT NULL, + `upload_id` varchar(25) NOT NULL, + `image_url` varchar(255) NOT NULL, + `file_name` varchar(255) NOT NULL, + `size_in_bytes` int NOT NULL, + `content_type` varchar(50) NOT NULL, + `description` varchar(255), + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `service_order_images_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `service_orders` ( + `id` varchar(25) NOT NULL, + `company_id` varchar(25) NOT NULL, + `client_id` varchar(25) NOT NULL, + `employee_id` varchar(25) NOT NULL, + `device_brand_id` varchar(25) NOT NULL, + `device_category_id` varchar(25) NOT NULL, + `device_model` varchar(100) NOT NULL, + `imei` varchar(50), + `reported_defect` text NOT NULL, + `observations` text, + `status` enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered') NOT NULL DEFAULT 'pending', + `created_at` timestamp NOT NULL DEFAULT (now()), + `updated_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `service_orders_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `uploads` ( + `id` varchar(25) NOT NULL, + `company_id` varchar(25) NOT NULL, + `employee_id` varchar(25) NOT NULL, + `key` varchar(512) NOT NULL, + `url` varchar(512) NOT NULL, + `file_name` varchar(255) NOT NULL, + `content_type` varchar(50) NOT NULL, + `size_in_bytes` int NOT NULL, + `status` varchar(20) NOT NULL DEFAULT 'pending', + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `uploads_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +ALTER TABLE `employees` MODIFY COLUMN `roles` enum('guest','technician','warehouse','financial','manager','admin') NOT NULL;--> statement-breakpoint +ALTER TABLE `service_order_images` ADD CONSTRAINT `service_order_images_service_order_id_service_orders_id_fk` FOREIGN KEY (`service_order_id`) REFERENCES `service_orders`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_order_images` ADD CONSTRAINT `service_order_images_employee_id_employees_id_fk` FOREIGN KEY (`employee_id`) REFERENCES `employees`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_order_images` ADD CONSTRAINT `service_order_images_upload_id_uploads_id_fk` FOREIGN KEY (`upload_id`) REFERENCES `uploads`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_orders` ADD CONSTRAINT `service_orders_company_id_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_orders` ADD CONSTRAINT `service_orders_client_id_clients_id_fk` FOREIGN KEY (`client_id`) REFERENCES `clients`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_orders` ADD CONSTRAINT `service_orders_employee_id_employees_id_fk` FOREIGN KEY (`employee_id`) REFERENCES `employees`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_orders` ADD CONSTRAINT `service_orders_device_brand_id_model_makers_id_fk` FOREIGN KEY (`device_brand_id`) REFERENCES `model_makers`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `service_orders` ADD CONSTRAINT `service_orders_device_category_id_model_categories_id_fk` FOREIGN KEY (`device_category_id`) REFERENCES `model_categories`(`id`) ON DELETE restrict ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `uploads` ADD CONSTRAINT `uploads_company_id_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `uploads` ADD CONSTRAINT `uploads_employee_id_employees_id_fk` FOREIGN KEY (`employee_id`) REFERENCES `employees`(`id`) ON DELETE restrict ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/drizzle/0007_romantic_union_jack.sql b/packages/db/drizzle/0007_romantic_union_jack.sql new file mode 100644 index 0000000..0c62a17 --- /dev/null +++ b/packages/db/drizzle/0007_romantic_union_jack.sql @@ -0,0 +1,74 @@ +CREATE TABLE `model_images` ( + `id` varchar(25) NOT NULL, + `model_id` varchar(25) NOT NULL, + `original_url` varchar(255), + `r2_key` varchar(255), + `is_primary` boolean NOT NULL DEFAULT false, + `variant` varchar(50), + `position` int NOT NULL DEFAULT 0, + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `model_images_id` PRIMARY KEY(`id`) +); +--> statement-breakpoint +CREATE TABLE `models` ( + `id` varchar(25) NOT NULL, + `maker_id` varchar(25) NOT NULL, + `name` varchar(255) NOT NULL, + `slug` varchar(100) NOT NULL, + `url` varchar(255) NOT NULL, + `image_url` varchar(255), + `image_local_path` varchar(255), + `category_id` varchar(25), + `announced` text, + `status` text, + `dimensions` text, + `weight` text, + `build` text, + `sim` text, + `display_type` text, + `display_size` text, + `display_resolution` text, + `display_protection` text, + `os` text, + `chipset` text, + `cpu` text, + `gpu` text, + `card_slot` text, + `internal_memory` text, + `main_camera` text, + `main_camera_features` text, + `main_camera_video` text, + `selfie_camera` text, + `selfie_features` text, + `selfie_video` text, + `battery` text, + `battery_charging` text, + `network_tech` text, + `sensors` text, + `colors` text, + `colors_hex` text, + `models_text` text, + `price` text, + `dimensions_width` float, + `dimensions_height` float, + `dimensions_thickness` float, + `weight_grams` float, + `display_size_inches` float, + `display_size_ratio` text, + `display_res_width` int, + `display_res_height` int, + `display_res_ppi` int, + `released` text, + `meta` text, + `company_id` varchar(25), + `created_at` timestamp NOT NULL DEFAULT (now()), + CONSTRAINT `models_id` PRIMARY KEY(`id`), + CONSTRAINT `slug_company_unique` UNIQUE(`slug`,`company_id`) +); +--> statement-breakpoint +ALTER TABLE `model_categories` DROP INDEX `model_categories_slug_unique`;--> statement-breakpoint +ALTER TABLE `model_makers` DROP INDEX `model_makers_slug_unique`;--> statement-breakpoint +ALTER TABLE `model_images` ADD CONSTRAINT `model_images_model_id_models_id_fk` FOREIGN KEY (`model_id`) REFERENCES `models`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `models` ADD CONSTRAINT `models_maker_id_model_makers_id_fk` FOREIGN KEY (`maker_id`) REFERENCES `model_makers`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `models` ADD CONSTRAINT `models_category_id_model_categories_id_fk` FOREIGN KEY (`category_id`) REFERENCES `model_categories`(`id`) ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE `models` ADD CONSTRAINT `models_company_id_companies_id_fk` FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/packages/db/drizzle/0008_slimy_abomination.sql b/packages/db/drizzle/0008_slimy_abomination.sql new file mode 100644 index 0000000..5008d6b --- /dev/null +++ b/packages/db/drizzle/0008_slimy_abomination.sql @@ -0,0 +1 @@ +ALTER TABLE `models` DROP COLUMN `image_local_path`; \ No newline at end of file diff --git a/packages/db/drizzle/0009_ambiguous_hammerhead.sql b/packages/db/drizzle/0009_ambiguous_hammerhead.sql new file mode 100644 index 0000000..c1374d7 --- /dev/null +++ b/packages/db/drizzle/0009_ambiguous_hammerhead.sql @@ -0,0 +1 @@ +ALTER TABLE `models` DROP COLUMN `image_url`; \ No newline at end of file diff --git a/packages/db/drizzle/0010_sturdy_fulltext.sql b/packages/db/drizzle/0010_sturdy_fulltext.sql new file mode 100644 index 0000000..d2c3875 --- /dev/null +++ b/packages/db/drizzle/0010_sturdy_fulltext.sql @@ -0,0 +1 @@ +ALTER TABLE `models` ADD FULLTEXT INDEX `models_fulltext_idx` (`name`, `models_text`, `chipset`, `cpu`, `internal_memory`, `os`); diff --git a/packages/db/drizzle/0011_crazy_slapstick.sql b/packages/db/drizzle/0011_crazy_slapstick.sql new file mode 100644 index 0000000..0243498 --- /dev/null +++ b/packages/db/drizzle/0011_crazy_slapstick.sql @@ -0,0 +1 @@ +ALTER TABLE `model_images` DROP COLUMN `original_url`; \ No newline at end of file diff --git a/packages/db/drizzle/meta/0006_snapshot.json b/packages/db/drizzle/meta/0006_snapshot.json new file mode 100644 index 0000000..0249b7c --- /dev/null +++ b/packages/db/drizzle/meta/0006_snapshot.json @@ -0,0 +1,962 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "ab909380-0bce-443b-b2a8-5ebb6488b252", + "prevId": "96fef0d8-3c1e-4d2f-990d-625797fa1861", + "tables": { + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "clients_id": { + "name": "clients_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "clients_cpf_unique": { + "name": "clients_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "companies": { + "name": "companies", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cnpj": { + "name": "cnpj", + "type": "varchar(14)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subdomain": { + "name": "subdomain", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "companies_id": { + "name": "companies_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "companies_cnpj_unique": { + "name": "companies_cnpj_unique", + "columns": ["cnpj"] + }, + "companies_subdomain_unique": { + "name": "companies_subdomain_unique", + "columns": ["subdomain"] + } + }, + "checkConstraint": {} + }, + "employees": { + "name": "employees", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "enum('guest','technician','warehouse','financial','manager','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "employees_user_id_users_id_fk": { + "name": "employees_user_id_users_id_fk", + "tableFrom": "employees", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "employees_company_id_companies_id_fk": { + "name": "employees_company_id_companies_id_fk", + "tableFrom": "employees", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "employees_id": { + "name": "employees_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "employees_cpf_unique": { + "name": "employees_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "model_categories": { + "name": "model_categories", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_categories_id": { + "name": "model_categories_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "model_categories_slug_unique": { + "name": "model_categories_slug_unique", + "columns": ["slug"] + } + }, + "checkConstraint": {} + }, + "model_makers": { + "name": "model_makers", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_count": { + "name": "device_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "page_count": { + "name": "page_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_makers_id": { + "name": "model_makers_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "model_makers_slug_unique": { + "name": "model_makers_slug_unique", + "columns": ["slug"] + } + }, + "checkConstraint": {} + }, + "one_time_tokens": { + "name": "one_time_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ott_type": { + "name": "ott_type", + "type": "enum('confirmation','password_reset','account_deletion')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relates_to": { + "name": "relates_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_tokens_user_id_users_id_fk": { + "name": "one_time_tokens_user_id_users_id_fk", + "tableFrom": "one_time_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "one_time_tokens_id": { + "name": "one_time_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "one_time_tokens_token_unique": { + "name": "one_time_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "refresh_tokens_id": { + "name": "refresh_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "service_order_images": { + "name": "service_order_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_order_id": { + "name": "service_order_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_order_images_service_order_id_service_orders_id_fk": { + "name": "service_order_images_service_order_id_service_orders_id_fk", + "tableFrom": "service_order_images", + "tableTo": "service_orders", + "columnsFrom": ["service_order_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_order_images_employee_id_employees_id_fk": { + "name": "service_order_images_employee_id_employees_id_fk", + "tableFrom": "service_order_images", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_order_images_upload_id_uploads_id_fk": { + "name": "service_order_images_upload_id_uploads_id_fk", + "tableFrom": "service_order_images", + "tableTo": "uploads", + "columnsFrom": ["upload_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_order_images_id": { + "name": "service_order_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_orders": { + "name": "service_orders", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_brand_id": { + "name": "device_brand_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_category_id": { + "name": "device_category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_model": { + "name": "device_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imei": { + "name": "imei", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reported_defect": { + "name": "reported_defect", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_orders_company_id_companies_id_fk": { + "name": "service_orders_company_id_companies_id_fk", + "tableFrom": "service_orders", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_orders_client_id_clients_id_fk": { + "name": "service_orders_client_id_clients_id_fk", + "tableFrom": "service_orders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_employee_id_employees_id_fk": { + "name": "service_orders_employee_id_employees_id_fk", + "tableFrom": "service_orders", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_brand_id_model_makers_id_fk": { + "name": "service_orders_device_brand_id_model_makers_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_makers", + "columnsFrom": ["device_brand_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_category_id_model_categories_id_fk": { + "name": "service_orders_device_category_id_model_categories_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_categories", + "columnsFrom": ["device_category_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_orders_id": { + "name": "service_orders_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "uploads": { + "name": "uploads", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_company_id_companies_id_fk": { + "name": "uploads_company_id_companies_id_fk", + "tableFrom": "uploads", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uploads_employee_id_employees_id_fk": { + "name": "uploads_employee_id_employees_id_fk", + "tableFrom": "uploads", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "uploads_id": { + "name": "uploads_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"] + }, + "users_google_id_unique": { + "name": "users_google_id_unique", + "columns": ["google_id"] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/0007_snapshot.json b/packages/db/drizzle/meta/0007_snapshot.json new file mode 100644 index 0000000..ba12de1 --- /dev/null +++ b/packages/db/drizzle/meta/0007_snapshot.json @@ -0,0 +1,1442 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "f8e272be-5fd8-4902-aa53-8ee19994cce6", + "prevId": "ab909380-0bce-443b-b2a8-5ebb6488b252", + "tables": { + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "clients_id": { + "name": "clients_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "clients_cpf_unique": { + "name": "clients_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "companies": { + "name": "companies", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cnpj": { + "name": "cnpj", + "type": "varchar(14)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subdomain": { + "name": "subdomain", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "companies_id": { + "name": "companies_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "companies_cnpj_unique": { + "name": "companies_cnpj_unique", + "columns": ["cnpj"] + }, + "companies_subdomain_unique": { + "name": "companies_subdomain_unique", + "columns": ["subdomain"] + } + }, + "checkConstraint": {} + }, + "model_makers": { + "name": "model_makers", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_count": { + "name": "device_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "page_count": { + "name": "page_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_makers_id": { + "name": "model_makers_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "employees": { + "name": "employees", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "enum('guest','technician','warehouse','financial','manager','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "employees_user_id_users_id_fk": { + "name": "employees_user_id_users_id_fk", + "tableFrom": "employees", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "employees_company_id_companies_id_fk": { + "name": "employees_company_id_companies_id_fk", + "tableFrom": "employees", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "employees_id": { + "name": "employees_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "employees_cpf_unique": { + "name": "employees_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "model_categories": { + "name": "model_categories", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_categories_id": { + "name": "model_categories_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model_images": { + "name": "model_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_url": { + "name": "original_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "variant": { + "name": "variant", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "model_images_model_id_models_id_fk": { + "name": "model_images_model_id_models_id_fk", + "tableFrom": "model_images", + "tableTo": "models", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_images_id": { + "name": "model_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "models": { + "name": "models", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maker_id": { + "name": "maker_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image_local_path": { + "name": "image_local_path", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "announced": { + "name": "announced", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build": { + "name": "build", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sim": { + "name": "sim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size": { + "name": "display_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_resolution": { + "name": "display_resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_protection": { + "name": "display_protection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chipset": { + "name": "chipset", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu": { + "name": "cpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gpu": { + "name": "gpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "card_slot": { + "name": "card_slot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_memory": { + "name": "internal_memory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera": { + "name": "main_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_features": { + "name": "main_camera_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_video": { + "name": "main_camera_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_camera": { + "name": "selfie_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_features": { + "name": "selfie_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_video": { + "name": "selfie_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery": { + "name": "battery", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery_charging": { + "name": "battery_charging", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_tech": { + "name": "network_tech", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sensors": { + "name": "sensors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors_hex": { + "name": "colors_hex", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "models_text": { + "name": "models_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price": { + "name": "price", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_width": { + "name": "dimensions_width", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_height": { + "name": "dimensions_height", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_thickness": { + "name": "dimensions_thickness", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_inches": { + "name": "display_size_inches", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_ratio": { + "name": "display_size_ratio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_width": { + "name": "display_res_width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_height": { + "name": "display_res_height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_ppi": { + "name": "display_res_ppi", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released": { + "name": "released", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "models_maker_id_model_makers_id_fk": { + "name": "models_maker_id_model_makers_id_fk", + "tableFrom": "models", + "tableTo": "model_makers", + "columnsFrom": ["maker_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_category_id_model_categories_id_fk": { + "name": "models_category_id_model_categories_id_fk", + "tableFrom": "models", + "tableTo": "model_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_company_id_companies_id_fk": { + "name": "models_company_id_companies_id_fk", + "tableFrom": "models", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "models_id": { + "name": "models_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "slug_company_unique": { + "name": "slug_company_unique", + "columns": ["slug", "company_id"] + } + }, + "checkConstraint": {} + }, + "one_time_tokens": { + "name": "one_time_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ott_type": { + "name": "ott_type", + "type": "enum('confirmation','password_reset','account_deletion')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relates_to": { + "name": "relates_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_tokens_user_id_users_id_fk": { + "name": "one_time_tokens_user_id_users_id_fk", + "tableFrom": "one_time_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "one_time_tokens_id": { + "name": "one_time_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "one_time_tokens_token_unique": { + "name": "one_time_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "refresh_tokens_id": { + "name": "refresh_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "service_order_images": { + "name": "service_order_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_order_id": { + "name": "service_order_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_order_images_service_order_id_service_orders_id_fk": { + "name": "service_order_images_service_order_id_service_orders_id_fk", + "tableFrom": "service_order_images", + "tableTo": "service_orders", + "columnsFrom": ["service_order_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_order_images_employee_id_employees_id_fk": { + "name": "service_order_images_employee_id_employees_id_fk", + "tableFrom": "service_order_images", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_order_images_upload_id_uploads_id_fk": { + "name": "service_order_images_upload_id_uploads_id_fk", + "tableFrom": "service_order_images", + "tableTo": "uploads", + "columnsFrom": ["upload_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_order_images_id": { + "name": "service_order_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_orders": { + "name": "service_orders", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_brand_id": { + "name": "device_brand_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_category_id": { + "name": "device_category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_model": { + "name": "device_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imei": { + "name": "imei", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reported_defect": { + "name": "reported_defect", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_orders_company_id_companies_id_fk": { + "name": "service_orders_company_id_companies_id_fk", + "tableFrom": "service_orders", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_orders_client_id_clients_id_fk": { + "name": "service_orders_client_id_clients_id_fk", + "tableFrom": "service_orders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_employee_id_employees_id_fk": { + "name": "service_orders_employee_id_employees_id_fk", + "tableFrom": "service_orders", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_brand_id_model_makers_id_fk": { + "name": "service_orders_device_brand_id_model_makers_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_makers", + "columnsFrom": ["device_brand_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_category_id_model_categories_id_fk": { + "name": "service_orders_device_category_id_model_categories_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_categories", + "columnsFrom": ["device_category_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_orders_id": { + "name": "service_orders_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "uploads": { + "name": "uploads", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_company_id_companies_id_fk": { + "name": "uploads_company_id_companies_id_fk", + "tableFrom": "uploads", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uploads_employee_id_employees_id_fk": { + "name": "uploads_employee_id_employees_id_fk", + "tableFrom": "uploads", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "uploads_id": { + "name": "uploads_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"] + }, + "users_google_id_unique": { + "name": "users_google_id_unique", + "columns": ["google_id"] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/0008_snapshot.json b/packages/db/drizzle/meta/0008_snapshot.json new file mode 100644 index 0000000..3f21437 --- /dev/null +++ b/packages/db/drizzle/meta/0008_snapshot.json @@ -0,0 +1,1435 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "7a33fdf9-9138-4db6-96e0-c729b47e5f0a", + "prevId": "f8e272be-5fd8-4902-aa53-8ee19994cce6", + "tables": { + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "clients_id": { + "name": "clients_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "clients_cpf_unique": { + "name": "clients_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "companies": { + "name": "companies", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cnpj": { + "name": "cnpj", + "type": "varchar(14)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subdomain": { + "name": "subdomain", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "companies_id": { + "name": "companies_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "companies_cnpj_unique": { + "name": "companies_cnpj_unique", + "columns": ["cnpj"] + }, + "companies_subdomain_unique": { + "name": "companies_subdomain_unique", + "columns": ["subdomain"] + } + }, + "checkConstraint": {} + }, + "model_makers": { + "name": "model_makers", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_count": { + "name": "device_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "page_count": { + "name": "page_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_makers_id": { + "name": "model_makers_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "employees": { + "name": "employees", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "enum('guest','technician','warehouse','financial','manager','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "employees_user_id_users_id_fk": { + "name": "employees_user_id_users_id_fk", + "tableFrom": "employees", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "employees_company_id_companies_id_fk": { + "name": "employees_company_id_companies_id_fk", + "tableFrom": "employees", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "employees_id": { + "name": "employees_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "employees_cpf_unique": { + "name": "employees_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "model_categories": { + "name": "model_categories", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_categories_id": { + "name": "model_categories_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model_images": { + "name": "model_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_url": { + "name": "original_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "variant": { + "name": "variant", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "model_images_model_id_models_id_fk": { + "name": "model_images_model_id_models_id_fk", + "tableFrom": "model_images", + "tableTo": "models", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_images_id": { + "name": "model_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "models": { + "name": "models", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maker_id": { + "name": "maker_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "announced": { + "name": "announced", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build": { + "name": "build", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sim": { + "name": "sim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size": { + "name": "display_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_resolution": { + "name": "display_resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_protection": { + "name": "display_protection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chipset": { + "name": "chipset", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu": { + "name": "cpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gpu": { + "name": "gpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "card_slot": { + "name": "card_slot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_memory": { + "name": "internal_memory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera": { + "name": "main_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_features": { + "name": "main_camera_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_video": { + "name": "main_camera_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_camera": { + "name": "selfie_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_features": { + "name": "selfie_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_video": { + "name": "selfie_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery": { + "name": "battery", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery_charging": { + "name": "battery_charging", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_tech": { + "name": "network_tech", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sensors": { + "name": "sensors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors_hex": { + "name": "colors_hex", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "models_text": { + "name": "models_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price": { + "name": "price", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_width": { + "name": "dimensions_width", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_height": { + "name": "dimensions_height", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_thickness": { + "name": "dimensions_thickness", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_inches": { + "name": "display_size_inches", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_ratio": { + "name": "display_size_ratio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_width": { + "name": "display_res_width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_height": { + "name": "display_res_height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_ppi": { + "name": "display_res_ppi", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released": { + "name": "released", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "models_maker_id_model_makers_id_fk": { + "name": "models_maker_id_model_makers_id_fk", + "tableFrom": "models", + "tableTo": "model_makers", + "columnsFrom": ["maker_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_category_id_model_categories_id_fk": { + "name": "models_category_id_model_categories_id_fk", + "tableFrom": "models", + "tableTo": "model_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_company_id_companies_id_fk": { + "name": "models_company_id_companies_id_fk", + "tableFrom": "models", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "models_id": { + "name": "models_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "slug_company_unique": { + "name": "slug_company_unique", + "columns": ["slug", "company_id"] + } + }, + "checkConstraint": {} + }, + "one_time_tokens": { + "name": "one_time_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ott_type": { + "name": "ott_type", + "type": "enum('confirmation','password_reset','account_deletion')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relates_to": { + "name": "relates_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_tokens_user_id_users_id_fk": { + "name": "one_time_tokens_user_id_users_id_fk", + "tableFrom": "one_time_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "one_time_tokens_id": { + "name": "one_time_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "one_time_tokens_token_unique": { + "name": "one_time_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "refresh_tokens_id": { + "name": "refresh_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "service_order_images": { + "name": "service_order_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_order_id": { + "name": "service_order_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_order_images_service_order_id_service_orders_id_fk": { + "name": "service_order_images_service_order_id_service_orders_id_fk", + "tableFrom": "service_order_images", + "tableTo": "service_orders", + "columnsFrom": ["service_order_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_order_images_employee_id_employees_id_fk": { + "name": "service_order_images_employee_id_employees_id_fk", + "tableFrom": "service_order_images", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_order_images_upload_id_uploads_id_fk": { + "name": "service_order_images_upload_id_uploads_id_fk", + "tableFrom": "service_order_images", + "tableTo": "uploads", + "columnsFrom": ["upload_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_order_images_id": { + "name": "service_order_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_orders": { + "name": "service_orders", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_brand_id": { + "name": "device_brand_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_category_id": { + "name": "device_category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_model": { + "name": "device_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imei": { + "name": "imei", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reported_defect": { + "name": "reported_defect", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_orders_company_id_companies_id_fk": { + "name": "service_orders_company_id_companies_id_fk", + "tableFrom": "service_orders", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_orders_client_id_clients_id_fk": { + "name": "service_orders_client_id_clients_id_fk", + "tableFrom": "service_orders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_employee_id_employees_id_fk": { + "name": "service_orders_employee_id_employees_id_fk", + "tableFrom": "service_orders", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_brand_id_model_makers_id_fk": { + "name": "service_orders_device_brand_id_model_makers_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_makers", + "columnsFrom": ["device_brand_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_category_id_model_categories_id_fk": { + "name": "service_orders_device_category_id_model_categories_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_categories", + "columnsFrom": ["device_category_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_orders_id": { + "name": "service_orders_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "uploads": { + "name": "uploads", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_company_id_companies_id_fk": { + "name": "uploads_company_id_companies_id_fk", + "tableFrom": "uploads", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uploads_employee_id_employees_id_fk": { + "name": "uploads_employee_id_employees_id_fk", + "tableFrom": "uploads", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "uploads_id": { + "name": "uploads_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"] + }, + "users_google_id_unique": { + "name": "users_google_id_unique", + "columns": ["google_id"] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/0009_snapshot.json b/packages/db/drizzle/meta/0009_snapshot.json new file mode 100644 index 0000000..3ac8f5c --- /dev/null +++ b/packages/db/drizzle/meta/0009_snapshot.json @@ -0,0 +1,1428 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "0f499b87-ef09-4b34-a384-2a6f28f37c2d", + "prevId": "7a33fdf9-9138-4db6-96e0-c729b47e5f0a", + "tables": { + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "clients_id": { + "name": "clients_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "clients_cpf_unique": { + "name": "clients_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "companies": { + "name": "companies", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cnpj": { + "name": "cnpj", + "type": "varchar(14)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subdomain": { + "name": "subdomain", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "companies_id": { + "name": "companies_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "companies_cnpj_unique": { + "name": "companies_cnpj_unique", + "columns": ["cnpj"] + }, + "companies_subdomain_unique": { + "name": "companies_subdomain_unique", + "columns": ["subdomain"] + } + }, + "checkConstraint": {} + }, + "model_makers": { + "name": "model_makers", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_count": { + "name": "device_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "page_count": { + "name": "page_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_makers_id": { + "name": "model_makers_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "employees": { + "name": "employees", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "enum('guest','technician','warehouse','financial','manager','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "employees_user_id_users_id_fk": { + "name": "employees_user_id_users_id_fk", + "tableFrom": "employees", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "employees_company_id_companies_id_fk": { + "name": "employees_company_id_companies_id_fk", + "tableFrom": "employees", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "employees_id": { + "name": "employees_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "employees_cpf_unique": { + "name": "employees_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "model_categories": { + "name": "model_categories", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_categories_id": { + "name": "model_categories_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model_images": { + "name": "model_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "original_url": { + "name": "original_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "variant": { + "name": "variant", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "model_images_model_id_models_id_fk": { + "name": "model_images_model_id_models_id_fk", + "tableFrom": "model_images", + "tableTo": "models", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_images_id": { + "name": "model_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "models": { + "name": "models", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maker_id": { + "name": "maker_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "announced": { + "name": "announced", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build": { + "name": "build", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sim": { + "name": "sim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size": { + "name": "display_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_resolution": { + "name": "display_resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_protection": { + "name": "display_protection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chipset": { + "name": "chipset", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu": { + "name": "cpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gpu": { + "name": "gpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "card_slot": { + "name": "card_slot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_memory": { + "name": "internal_memory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera": { + "name": "main_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_features": { + "name": "main_camera_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_video": { + "name": "main_camera_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_camera": { + "name": "selfie_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_features": { + "name": "selfie_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_video": { + "name": "selfie_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery": { + "name": "battery", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery_charging": { + "name": "battery_charging", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_tech": { + "name": "network_tech", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sensors": { + "name": "sensors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors_hex": { + "name": "colors_hex", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "models_text": { + "name": "models_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price": { + "name": "price", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_width": { + "name": "dimensions_width", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_height": { + "name": "dimensions_height", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_thickness": { + "name": "dimensions_thickness", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_inches": { + "name": "display_size_inches", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_ratio": { + "name": "display_size_ratio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_width": { + "name": "display_res_width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_height": { + "name": "display_res_height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_ppi": { + "name": "display_res_ppi", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released": { + "name": "released", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "models_maker_id_model_makers_id_fk": { + "name": "models_maker_id_model_makers_id_fk", + "tableFrom": "models", + "tableTo": "model_makers", + "columnsFrom": ["maker_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_category_id_model_categories_id_fk": { + "name": "models_category_id_model_categories_id_fk", + "tableFrom": "models", + "tableTo": "model_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_company_id_companies_id_fk": { + "name": "models_company_id_companies_id_fk", + "tableFrom": "models", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "models_id": { + "name": "models_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "slug_company_unique": { + "name": "slug_company_unique", + "columns": ["slug", "company_id"] + } + }, + "checkConstraint": {} + }, + "one_time_tokens": { + "name": "one_time_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ott_type": { + "name": "ott_type", + "type": "enum('confirmation','password_reset','account_deletion')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relates_to": { + "name": "relates_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_tokens_user_id_users_id_fk": { + "name": "one_time_tokens_user_id_users_id_fk", + "tableFrom": "one_time_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "one_time_tokens_id": { + "name": "one_time_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "one_time_tokens_token_unique": { + "name": "one_time_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "refresh_tokens_id": { + "name": "refresh_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "service_order_images": { + "name": "service_order_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_order_id": { + "name": "service_order_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_order_images_service_order_id_service_orders_id_fk": { + "name": "service_order_images_service_order_id_service_orders_id_fk", + "tableFrom": "service_order_images", + "tableTo": "service_orders", + "columnsFrom": ["service_order_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_order_images_employee_id_employees_id_fk": { + "name": "service_order_images_employee_id_employees_id_fk", + "tableFrom": "service_order_images", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_order_images_upload_id_uploads_id_fk": { + "name": "service_order_images_upload_id_uploads_id_fk", + "tableFrom": "service_order_images", + "tableTo": "uploads", + "columnsFrom": ["upload_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_order_images_id": { + "name": "service_order_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_orders": { + "name": "service_orders", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_brand_id": { + "name": "device_brand_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_category_id": { + "name": "device_category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_model": { + "name": "device_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imei": { + "name": "imei", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reported_defect": { + "name": "reported_defect", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_orders_company_id_companies_id_fk": { + "name": "service_orders_company_id_companies_id_fk", + "tableFrom": "service_orders", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_orders_client_id_clients_id_fk": { + "name": "service_orders_client_id_clients_id_fk", + "tableFrom": "service_orders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_employee_id_employees_id_fk": { + "name": "service_orders_employee_id_employees_id_fk", + "tableFrom": "service_orders", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_brand_id_model_makers_id_fk": { + "name": "service_orders_device_brand_id_model_makers_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_makers", + "columnsFrom": ["device_brand_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_category_id_model_categories_id_fk": { + "name": "service_orders_device_category_id_model_categories_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_categories", + "columnsFrom": ["device_category_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_orders_id": { + "name": "service_orders_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "uploads": { + "name": "uploads", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_company_id_companies_id_fk": { + "name": "uploads_company_id_companies_id_fk", + "tableFrom": "uploads", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uploads_employee_id_employees_id_fk": { + "name": "uploads_employee_id_employees_id_fk", + "tableFrom": "uploads", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "uploads_id": { + "name": "uploads_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"] + }, + "users_google_id_unique": { + "name": "users_google_id_unique", + "columns": ["google_id"] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/0011_snapshot.json b/packages/db/drizzle/meta/0011_snapshot.json new file mode 100644 index 0000000..4ecf4c3 --- /dev/null +++ b/packages/db/drizzle/meta/0011_snapshot.json @@ -0,0 +1,1421 @@ +{ + "version": "5", + "dialect": "mysql", + "id": "0b530de8-4672-48e9-872d-56bc108a654e", + "prevId": "0f499b87-ef09-4b34-a384-2a6f28f37c2d", + "tables": { + "clients": { + "name": "clients", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "clients_user_id_users_id_fk": { + "name": "clients_user_id_users_id_fk", + "tableFrom": "clients", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "clients_id": { + "name": "clients_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "clients_cpf_unique": { + "name": "clients_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "companies": { + "name": "companies", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cnpj": { + "name": "cnpj", + "type": "varchar(14)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "address": { + "name": "address", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "subdomain": { + "name": "subdomain", + "type": "varchar(32)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "companies_id": { + "name": "companies_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "companies_cnpj_unique": { + "name": "companies_cnpj_unique", + "columns": ["cnpj"] + }, + "companies_subdomain_unique": { + "name": "companies_subdomain_unique", + "columns": ["subdomain"] + } + }, + "checkConstraint": {} + }, + "model_makers": { + "name": "model_makers", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_count": { + "name": "device_count", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "page_count": { + "name": "page_count", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_makers_id": { + "name": "model_makers_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "employees": { + "name": "employees", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "cpf": { + "name": "cpf", + "type": "varchar(11)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "phone": { + "name": "phone", + "type": "varchar(11)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "roles": { + "name": "roles", + "type": "enum('guest','technician','warehouse','financial','manager','admin')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "employees_user_id_users_id_fk": { + "name": "employees_user_id_users_id_fk", + "tableFrom": "employees", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "employees_company_id_companies_id_fk": { + "name": "employees_company_id_companies_id_fk", + "tableFrom": "employees", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "employees_id": { + "name": "employees_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "employees_cpf_unique": { + "name": "employees_cpf_unique", + "columns": ["cpf"] + } + }, + "checkConstraint": {} + }, + "model_categories": { + "name": "model_categories", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "model_categories_id": { + "name": "model_categories_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "model_images": { + "name": "model_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "model_id": { + "name": "model_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "r2_key": { + "name": "r2_key", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "variant": { + "name": "variant", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "position": { + "name": "position", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "model_images_model_id_models_id_fk": { + "name": "model_images_model_id_models_id_fk", + "tableFrom": "model_images", + "tableTo": "models", + "columnsFrom": ["model_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "model_images_id": { + "name": "model_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "models": { + "name": "models", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "maker_id": { + "name": "maker_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "slug": { + "name": "slug", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "category_id": { + "name": "category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "announced": { + "name": "announced", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions": { + "name": "dimensions", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight": { + "name": "weight", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "build": { + "name": "build", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sim": { + "name": "sim", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_type": { + "name": "display_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size": { + "name": "display_size", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_resolution": { + "name": "display_resolution", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_protection": { + "name": "display_protection", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "os": { + "name": "os", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "chipset": { + "name": "chipset", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cpu": { + "name": "cpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "gpu": { + "name": "gpu", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "card_slot": { + "name": "card_slot", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "internal_memory": { + "name": "internal_memory", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera": { + "name": "main_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_features": { + "name": "main_camera_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "main_camera_video": { + "name": "main_camera_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_camera": { + "name": "selfie_camera", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_features": { + "name": "selfie_features", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "selfie_video": { + "name": "selfie_video", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery": { + "name": "battery", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "battery_charging": { + "name": "battery_charging", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "network_tech": { + "name": "network_tech", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sensors": { + "name": "sensors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors": { + "name": "colors", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "colors_hex": { + "name": "colors_hex", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "models_text": { + "name": "models_text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "price": { + "name": "price", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_width": { + "name": "dimensions_width", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_height": { + "name": "dimensions_height", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "dimensions_thickness": { + "name": "dimensions_thickness", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "weight_grams": { + "name": "weight_grams", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_inches": { + "name": "display_size_inches", + "type": "float", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_size_ratio": { + "name": "display_size_ratio", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_width": { + "name": "display_res_width", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_height": { + "name": "display_res_height", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "display_res_ppi": { + "name": "display_res_ppi", + "type": "int", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "released": { + "name": "released", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "meta": { + "name": "meta", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "models_maker_id_model_makers_id_fk": { + "name": "models_maker_id_model_makers_id_fk", + "tableFrom": "models", + "tableTo": "model_makers", + "columnsFrom": ["maker_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_category_id_model_categories_id_fk": { + "name": "models_category_id_model_categories_id_fk", + "tableFrom": "models", + "tableTo": "model_categories", + "columnsFrom": ["category_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "models_company_id_companies_id_fk": { + "name": "models_company_id_companies_id_fk", + "tableFrom": "models", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "models_id": { + "name": "models_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "slug_company_unique": { + "name": "slug_company_unique", + "columns": ["slug", "company_id"] + } + }, + "checkConstraint": {} + }, + "one_time_tokens": { + "name": "one_time_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "ott_type": { + "name": "ott_type", + "type": "enum('confirmation','password_reset','account_deletion')", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "relates_to": { + "name": "relates_to", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "one_time_tokens_user_id_users_id_fk": { + "name": "one_time_tokens_user_id_users_id_fk", + "tableFrom": "one_time_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "one_time_tokens_id": { + "name": "one_time_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "one_time_tokens_token_unique": { + "name": "one_time_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "refresh_tokens": { + "name": "refresh_tokens", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "refresh_tokens_user_id_users_id_fk": { + "name": "refresh_tokens_user_id_users_id_fk", + "tableFrom": "refresh_tokens", + "tableTo": "users", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "refresh_tokens_id": { + "name": "refresh_tokens_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "refresh_tokens_token_unique": { + "name": "refresh_tokens_token_unique", + "columns": ["token"] + } + }, + "checkConstraint": {} + }, + "service_order_images": { + "name": "service_order_images", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "service_order_id": { + "name": "service_order_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "upload_id": { + "name": "upload_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "image_url": { + "name": "image_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_order_images_service_order_id_service_orders_id_fk": { + "name": "service_order_images_service_order_id_service_orders_id_fk", + "tableFrom": "service_order_images", + "tableTo": "service_orders", + "columnsFrom": ["service_order_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_order_images_employee_id_employees_id_fk": { + "name": "service_order_images_employee_id_employees_id_fk", + "tableFrom": "service_order_images", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_order_images_upload_id_uploads_id_fk": { + "name": "service_order_images_upload_id_uploads_id_fk", + "tableFrom": "service_order_images", + "tableTo": "uploads", + "columnsFrom": ["upload_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_order_images_id": { + "name": "service_order_images_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "service_orders": { + "name": "service_orders", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_brand_id": { + "name": "device_brand_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_category_id": { + "name": "device_category_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "device_model": { + "name": "device_model", + "type": "varchar(100)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "imei": { + "name": "imei", + "type": "varchar(50)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "reported_defect": { + "name": "reported_defect", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "observations": { + "name": "observations", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "enum('pending','diagnosing','waiting_approval','approved','fixing','ready','delivered')", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "service_orders_company_id_companies_id_fk": { + "name": "service_orders_company_id_companies_id_fk", + "tableFrom": "service_orders", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "service_orders_client_id_clients_id_fk": { + "name": "service_orders_client_id_clients_id_fk", + "tableFrom": "service_orders", + "tableTo": "clients", + "columnsFrom": ["client_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_employee_id_employees_id_fk": { + "name": "service_orders_employee_id_employees_id_fk", + "tableFrom": "service_orders", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_brand_id_model_makers_id_fk": { + "name": "service_orders_device_brand_id_model_makers_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_makers", + "columnsFrom": ["device_brand_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + }, + "service_orders_device_category_id_model_categories_id_fk": { + "name": "service_orders_device_category_id_model_categories_id_fk", + "tableFrom": "service_orders", + "tableTo": "model_categories", + "columnsFrom": ["device_category_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "service_orders_id": { + "name": "service_orders_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "uploads": { + "name": "uploads", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "company_id": { + "name": "company_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "employee_id": { + "name": "employee_id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "key": { + "name": "key", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "varchar(512)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "file_name": { + "name": "file_name", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content_type": { + "name": "content_type", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "size_in_bytes": { + "name": "size_in_bytes", + "type": "int", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "status": { + "name": "status", + "type": "varchar(20)", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + } + }, + "indexes": {}, + "foreignKeys": { + "uploads_company_id_companies_id_fk": { + "name": "uploads_company_id_companies_id_fk", + "tableFrom": "uploads", + "tableTo": "companies", + "columnsFrom": ["company_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "uploads_employee_id_employees_id_fk": { + "name": "uploads_employee_id_employees_id_fk", + "tableFrom": "uploads", + "tableTo": "employees", + "columnsFrom": ["employee_id"], + "columnsTo": ["id"], + "onDelete": "restrict", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "uploads_id": { + "name": "uploads_id", + "columns": ["id"] + } + }, + "uniqueConstraints": {}, + "checkConstraint": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "varchar(25)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "varchar(128)", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "google_id": { + "name": "google_id", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "avatar_url": { + "name": "avatar_url", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(now())" + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "users_id": { + "name": "users_id", + "columns": ["id"] + } + }, + "uniqueConstraints": { + "users_email_unique": { + "name": "users_email_unique", + "columns": ["email"] + }, + "users_google_id_unique": { + "name": "users_google_id_unique", + "columns": ["google_id"] + } + }, + "checkConstraint": {} + } + }, + "views": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "tables": {}, + "indexes": {} + } +} diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 6e81e16..6f2e713 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -43,6 +43,48 @@ "when": 1760755210443, "tag": "0005_faulty_mandarin", "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1779902286658, + "tag": "0006_awesome_cloak", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1780260069002, + "tag": "0007_romantic_union_jack", + "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1780260191632, + "tag": "0008_slimy_abomination", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1780262325715, + "tag": "0009_ambiguous_hammerhead", + "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1780263000000, + "tag": "0010_sturdy_fulltext", + "breakpoints": true + }, + { + "idx": 11, + "version": "5", + "when": 1780264072893, + "tag": "0011_crazy_slapstick", + "breakpoints": true } ] } diff --git a/packages/db/package.json b/packages/db/package.json index abdebb7..3eb2c53 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -4,10 +4,10 @@ "type": "module", "private": true, "scripts": { - "db:push": "dotenv -- drizzle-kit push", - "db:generate": "dotenv -- drizzle-kit generate", - "db:studio": "dotenv -- drizzle-kit studio", - "db:migrate": "dotenv -- tsx src/migrate.ts", + "db:push": "bunx --bun drizzle-kit push", + "db:generate": "bunx --bun drizzle-kit generate", + "db:studio": "bunx --bun drizzle-kit studio", + "db:migrate": "tsx src/migrate.ts", "db:start": "docker compose up -d", "db:watch": "docker compose up", "db:stop": "docker compose stop", diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 4cbaa08..4d8e44a 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -2,7 +2,9 @@ export * from "./clients"; export * from "./companies"; export * from "./employees"; export * from "./model-categories"; +export * from "./model-images"; export * from "./model-makers"; +export * from "./models"; export * from "./one-time-tokens"; export * from "./refresh-tokens"; export * from "./service-order-photos"; diff --git a/packages/db/src/schema/model-categories.ts b/packages/db/src/schema/model-categories.ts index 9037fa4..dfbdde9 100644 --- a/packages/db/src/schema/model-categories.ts +++ b/packages/db/src/schema/model-categories.ts @@ -2,16 +2,16 @@ import { createId } from "@paralleldrive/cuid2"; import { mysqlTable, varchar } from "drizzle-orm/mysql-core"; import { createSelectSchema } from "drizzle-zod"; +/** @description Device categories table: groups models by type (e.g. smartphone, tablet) */ export const modelCategories = mysqlTable("model_categories", { id: varchar("id", { length: 25 }) .$defaultFn(() => createId()) .primaryKey(), name: varchar("name", { length: 100 }).notNull(), + slug: varchar("slug", { length: 100 }).notNull(), }); +/** @description Zod schema for selecting a category record */ export const modelCategorySelectSchema = createSelectSchema(modelCategories); - -/** @deprecated Renamed to {@link modelCategories} */ -export const deviceCategories = modelCategories; -/** @deprecated Renamed to {@link modelCategorySelectSchema} */ -export const deviceCategorySelectSchema = modelCategorySelectSchema; +export type CategoryInsert = typeof modelCategories.$inferInsert; +export type CategorySelect = typeof modelCategories.$inferSelect; diff --git a/packages/db/src/schema/model-images.ts b/packages/db/src/schema/model-images.ts new file mode 100644 index 0000000..081e067 --- /dev/null +++ b/packages/db/src/schema/model-images.ts @@ -0,0 +1,31 @@ +import { createId } from "@paralleldrive/cuid2"; +import { + boolean, + int, + mysqlTable, + timestamp, + varchar, +} from "drizzle-orm/mysql-core"; +import { createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { models } from "./models"; + +export const modelImages = mysqlTable("model_images", { + id: varchar("id", { length: 25 }) + .$defaultFn(() => createId()) + .primaryKey(), + modelId: varchar("model_id", { length: 25 }) + .notNull() + .references(() => models.id), + r2Key: varchar("r2_key", { length: 255 }), + isPrimary: boolean("is_primary").notNull().default(false), + variant: varchar("variant", { length: 50 }), + position: int("position").notNull().default(0), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +export const modelImageSelectSchema = createSelectSchema(modelImages, { + createdAt: z.coerce.date(), +}); +export type ModelImageInsert = typeof modelImages.$inferInsert; +export type ModelImageSelect = typeof modelImages.$inferSelect; diff --git a/packages/db/src/schema/model-makers.ts b/packages/db/src/schema/model-makers.ts index eb30ca0..3ac0249 100644 --- a/packages/db/src/schema/model-makers.ts +++ b/packages/db/src/schema/model-makers.ts @@ -1,14 +1,21 @@ import { createId } from "@paralleldrive/cuid2"; -import { mysqlTable, varchar } from "drizzle-orm/mysql-core"; +import { int, mysqlTable, timestamp, varchar } from "drizzle-orm/mysql-core"; import { createSelectSchema } from "drizzle-zod"; +/** @description Device makers (brands) table: stores manufacturer/brand info */ export const modelMakers = mysqlTable("model_makers", { id: varchar("id", { length: 25 }) .$defaultFn(() => createId()) .primaryKey(), name: varchar("name", { length: 100 }).notNull(), + slug: varchar("slug", { length: 100 }).notNull(), + url: varchar("url", { length: 255 }).notNull(), + deviceCount: int("device_count").notNull().default(0), + pageCount: int("page_count"), + createdAt: timestamp("created_at").notNull().defaultNow(), }); +/** @description Zod schema for selecting a maker record */ export const modelMakerSelectSchema = createSelectSchema(modelMakers); /** @deprecated Renamed to {@link modelMakers} */ diff --git a/packages/db/src/schema/models.ts b/packages/db/src/schema/models.ts new file mode 100644 index 0000000..ca5860c --- /dev/null +++ b/packages/db/src/schema/models.ts @@ -0,0 +1,105 @@ +import { createId } from "@paralleldrive/cuid2"; +import { + float, + index, + int, + mysqlTable, + text, + timestamp, + unique, + varchar, +} from "drizzle-orm/mysql-core"; +import { createSelectSchema } from "drizzle-zod"; +import { z } from "zod"; +import { companies } from "./companies"; +import { modelCategories } from "./model-categories"; +import { modelMakers } from "./model-makers"; + +export const models = mysqlTable( + "models", + { + id: varchar("id", { length: 25 }) + .$defaultFn(() => createId()) + .primaryKey(), + makerId: varchar("maker_id", { length: 25 }) + .notNull() + .references(() => modelMakers.id), + name: varchar("name", { length: 255 }).notNull(), + slug: varchar("slug", { length: 100 }).notNull(), + url: varchar("url", { length: 255 }).notNull(), + + categoryId: varchar("category_id", { length: 25 }).references( + () => modelCategories.id + ), + + announced: text("announced"), + status: text("status"), + dimensions: text("dimensions"), + weight: text("weight"), + build: text("build"), + sim: text("sim"), + displayType: text("display_type"), + displaySize: text("display_size"), + displayResolution: text("display_resolution"), + displayProtection: text("display_protection"), + os: text("os"), + chipset: text("chipset"), + cpu: text("cpu"), + gpu: text("gpu"), + cardSlot: text("card_slot"), + internalMemory: text("internal_memory"), + mainCamera: text("main_camera"), + mainCameraFeatures: text("main_camera_features"), + mainCameraVideo: text("main_camera_video"), + selfieCamera: text("selfie_camera"), + selfieFeatures: text("selfie_features"), + selfieVideo: text("selfie_video"), + battery: text("battery"), + batteryCharging: text("battery_charging"), + networkTech: text("network_tech"), + sensors: text("sensors"), + colors: text("colors"), + colorsHex: text("colors_hex"), + modelsText: text("models_text"), + price: text("price"), + dimensionsWidth: float("dimensions_width"), + dimensionsHeight: float("dimensions_height"), + dimensionsThickness: float("dimensions_thickness"), + weightGrams: float("weight_grams"), + displaySizeInches: float("display_size_inches"), + displaySizeRatio: text("display_size_ratio"), + displayResWidth: int("display_res_width"), + displayResHeight: int("display_res_height"), + displayResPpi: int("display_res_ppi"), + released: text("released"), + + meta: text("meta"), + + companyId: varchar("company_id", { length: 25 }).references( + () => companies.id + ), + createdAt: timestamp("created_at").defaultNow().notNull(), + }, + (table) => ({ + slugCompanyUnique: unique("slug_company_unique").on( + table.slug, + table.companyId + ), + modelsFulltextIdx: index("models_fulltext_idx") + .on( + table.name, + table.modelsText, + table.chipset, + table.cpu, + table.internalMemory, + table.os + ) + .using("fulltext" as "btree"), + }) +); + +export const modelSelectSchema = createSelectSchema(models, { + createdAt: z.coerce.date(), +}); +export type ModelInsert = typeof models.$inferInsert; +export type ModelSelect = typeof models.$inferSelect; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index 314d12f..8ee2d72 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -10,5 +10,5 @@ "esModuleInterop": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/sqlites/sqlite-db"] } diff --git a/packages/permissions/src/abilities.ts b/packages/permissions/src/abilities.ts index ab0e4dc..6388c07 100644 --- a/packages/permissions/src/abilities.ts +++ b/packages/permissions/src/abilities.ts @@ -21,11 +21,13 @@ const roleAbilities: Record = { permissions.serviceOrders.read, permissions.serviceOrders.update, permissions.serviceOrders.changeStatus, + permissions.devices.read, ], warehouse: [ ...baseEmployee, permissions.inventory.read, permissions.inventory.update, + permissions.devices.read, ], financial: [ ...baseEmployee, @@ -34,6 +36,7 @@ const roleAbilities: Record = { permissions.estimates.update, permissions.estimates.delete, permissions.estimates.sendToCustomer, + permissions.devices.read, ], manager: [ ...baseEmployee, @@ -52,6 +55,10 @@ const roleAbilities: Record = { permissions.estimates.sendToCustomer, permissions.employees.read, permissions.employees.create, + permissions.devices.read, + permissions.devices.create, + permissions.devices.update, + permissions.devices.delete, ], admin: [ ...baseEmployee, diff --git a/packages/schemas/package.json b/packages/schemas/package.json index b96c951..907449d 100644 --- a/packages/schemas/package.json +++ b/packages/schemas/package.json @@ -57,6 +57,10 @@ "./uploads": { "types": "./src/uploads.ts", "default": "./src/uploads.ts" + }, + "./models": { + "types": "./src/models.ts", + "default": "./src/models.ts" } }, "devDependencies": { diff --git a/packages/schemas/src/models.ts b/packages/schemas/src/models.ts new file mode 100644 index 0000000..559f172 --- /dev/null +++ b/packages/schemas/src/models.ts @@ -0,0 +1,113 @@ +import { z } from "zod"; +import { getPaginatedDataSchema } from "./utils"; + +/** @description Query schema for listing categories: optional name filter */ +export const getModelCategoriesQuerySchema = z.object({ + query: z.string().optional(), +}); + +/** @description Params schema for getting a category by slug */ +export const getModelCategoryParamsSchema = z.object({ + slug: z.string().min(1), +}); + +/** @description Query schema for listing makers: pagination, sorting, and optional name filter */ +export const getModelMakersQuerySchema = getPaginatedDataSchema.extend({ + sort: z.enum(["newer", "older", "name", "most_devices"]).optional(), +}); + +/** @description Params schema for getting a maker by slug */ +export const getModelMakerParamsSchema = z.object({ + slug: z.string().min(1), +}); + +/** @description Enum of valid device model release statuses */ +export const modelStatuses = z.enum([ + "Available", + "Discontinued", + "Cancelled", + "Rumored", +]); + +/** @description Query schema for listing models: pagination, fulltext search, filters, and sorting */ +export const getModelsQuerySchema = getPaginatedDataSchema.extend({ + makerId: z.string().cuid2().optional(), + categoryId: z.string().cuid2().optional(), + status: modelStatuses.optional(), + sort: z.enum(["newer", "older", "name"]).optional(), +}); + +/** @description Params schema for getting a model by slug */ +export const getModelBySlugParamsSchema = z.object({ + slug: z.string().min(1), +}); + +/** @description Body schema for creating a model */ +export const createModelBodySchema = z.object({ + name: z.string().min(1).max(255), + makerId: z.string().min(1), + categoryId: z.string().optional(), + status: modelStatuses.optional(), + announced: z.string().optional(), + dimensions: z.string().optional(), + weight: z.string().optional(), + build: z.string().optional(), + sim: z.string().optional(), + displayType: z.string().optional(), + displaySize: z.string().optional(), + displayResolution: z.string().optional(), + displayProtection: z.string().optional(), + os: z.string().optional(), + chipset: z.string().optional(), + cpu: z.string().optional(), + gpu: z.string().optional(), + cardSlot: z.string().optional(), + internalMemory: z.string().optional(), + mainCamera: z.string().optional(), + mainCameraFeatures: z.string().optional(), + mainCameraVideo: z.string().optional(), + selfieCamera: z.string().optional(), + selfieFeatures: z.string().optional(), + selfieVideo: z.string().optional(), + battery: z.string().optional(), + batteryCharging: z.string().optional(), + networkTech: z.string().optional(), + sensors: z.string().optional(), + colors: z.string().optional(), + colorsHex: z.string().optional(), + modelsText: z.string().optional(), + price: z.string().optional(), + dimensionsWidth: z.number().optional(), + dimensionsHeight: z.number().optional(), + dimensionsThickness: z.number().optional(), + weightGrams: z.number().optional(), + displaySizeInches: z.number().optional(), + displaySizeRatio: z.string().optional(), + displayResWidth: z.number().optional(), + displayResHeight: z.number().optional(), + displayResPpi: z.number().optional(), + released: z.string().optional(), + meta: z.string().optional(), +}); + +/** @description Body schema for partially updating a model (all fields optional) */ +export const patchModelBodySchema = createModelBodySchema.partial(); + +/** @description Params schema for operating on a model by its ID */ +export const modelIdParamsSchema = z.object({ + modelId: z.string().min(1), +}); + +/** @description Params schema for model image operations */ +export const modelImageParamsSchema = z.object({ + modelId: z.string().min(1), + imageId: z.string().min(1), +}); + +/** @description Body schema for assigning an uploaded image to a model */ +export const createModelImageBodySchema = z.object({ + r2Key: z.string().min(1), + isPrimary: z.boolean().optional(), + variant: z.string().optional(), + position: z.number().int().optional(), +}); diff --git a/packages/schemas/src/uploads.ts b/packages/schemas/src/uploads.ts index a78343c..b73ddd2 100644 --- a/packages/schemas/src/uploads.ts +++ b/packages/schemas/src/uploads.ts @@ -30,3 +30,13 @@ export const uploadPresignResponseSchema = z.object({ url: z.string().url(), expiresIn: z.number().int().positive(), }); + +export const createModelImageUploadPresignSchema = z.object({ + fileName: z.string().min(1).max(255), + contentType: z + .string() + .min(1) + .max(255) + .regex(/^[^/]+\/[^/]+$/), + size: z.number().int().positive().max(MAX_UPLOAD_SIZE_BYTES), +});