Skip to content
Merged
23 changes: 21 additions & 2 deletions apps/server/src/config/r2.ts
Original file line number Diff line number Diff line change
@@ -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);
Expand Down Expand Up @@ -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<string> {
const command = new GetObjectCommand({
Bucket: r2Bucket,
Key: key,
});
return await getSignedUrl(r2Client, command, { expiresIn: 86_400 });
}
61 changes: 61 additions & 0 deletions apps/server/src/core/docs/categories/categories.docs.ts
Original file line number Diff line number Diff line change
@@ -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,
};
79 changes: 79 additions & 0 deletions apps/server/src/core/docs/makers/makers.docs.ts
Original file line number Diff line number Diff line change
@@ -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,
};
Loading