Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/pages/api/v1/builds/[uuid]/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
/**
* Copyright 2025 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextApiRequest, NextApiResponse } from 'next/types';
import rootLogger from 'server/lib/logger';
import BuildService from 'server/services/build';

const logger = rootLogger.child({
filename: 'builds/[uuid]/index.ts',
});

/**
* @openapi
* /api/v1/builds/{uuid}:
* get:
* summary: Get build by UUID
* description: |
* Retrieves detailed information about a specific build by its UUID.
* tags:
* - Builds
* parameters:
* - in: path
* name: uuid
* required: true
* schema:
* type: string
* description: The UUID of the build
* responses:
* 200:
* description: Successfully retrieved build details
* content:
* application/json:
* schema:
* type: object
* properties:
* id:
* type: integer
* uuid:
* type: string
* status:
* type: string
* statusMessage:
* type: string
* enableFullYaml:
* type: boolean
* sha:
* type: string
* createdAt:
* type: string
* format: date-time
* updatedAt:
* type: string
* format: date-time
* deletedAt:
* type: string
* format: date-time
* nullable: true
* pullRequestId:
* type: integer
* manifest:
* type: object
* webhooksYaml:
* type: object
* dashboardLinks:
* type: object
* isStatic:
* type: boolean
* namespace:
* type: string
* 404:
* description: Build not found
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: Build not found
* 405:
* description: Method not allowed
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
*/
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'GET') {
return res.status(405).json({ error: `${req.method} is not allowed` });
}

const { uuid } = req.query;

if (!uuid || typeof uuid !== 'string') {
return res.status(400).json({ error: 'Invalid UUID' });
}

try {
const buildService = new BuildService();

const build = await buildService.db.models.Build.query()
.findOne({ uuid })
.select(
'id',
'uuid',
'status',
'statusMessage',
'enableFullYaml',
'sha',
'createdAt',
'updatedAt',
'deletedAt',
'pullRequestId',
'manifest',
'webhooksYaml',
'dashboardLinks',
'isStatic',
'namespace'
);

if (!build) {
logger.info(`Build with UUID ${uuid} not found`);
return res.status(404).json({ error: 'Build not found' });
}

return res.status(200).json(build);
} catch (error) {
logger.error(`Error fetching build ${uuid}:`, error);
return res.status(500).json({ error: 'An unexpected error occurred' });
}
};
214 changes: 214 additions & 0 deletions src/pages/api/v1/builds/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
/**
* Copyright 2025 GoodRx, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { NextApiRequest, NextApiResponse } from 'next/types';
import rootLogger from 'server/lib/logger';
import BuildService from 'server/services/build';

const logger = rootLogger.child({
filename: 'api/v1/builds/index.ts',
});

/**
* @openapi
* /api/v1/builds:
* get:
* summary: Get all builds with optional status filtering
* description: |
* Retrieves a list of builds with optional status exclusion filtering and pagination support.
* By default, excludes builds with status 'torn_down' and 'pending'.
* tags:
* - Builds
* parameters:
* - in: query
* name: exclude
* required: false
* schema:
* oneOf:
* - type: array
* items:
* type: string
* - type: string
* description: |
* Status values to exclude from results. Can be passed as:
* - JSON array string: exclude=["torn_down","pending"]
* - Multiple query params: exclude=torn_down&exclude=pending
* - Single value: exclude=torn_down
* Default: ["torn_down", "pending"]
* - in: query
* name: page
* required: false
* schema:
* type: integer
* minimum: 1
* description: Page number for pagination (if not provided with limit, all results are returned)
* - in: query
* name: limit
* required: false
* schema:
* type: integer
* minimum: 1
* description: Number of items per page (if not provided with page, all results are returned)
* responses:
* 200:
* description: Successfully retrieved builds list
* content:
* application/json:
* schema:
* type: object
* required:
* - builds
* - metadata
* properties:
* builds:
* type: array
* items:
* type: object
* required:
* - uuid
* - status
* properties:
* uuid:
* type: string
* description: Unique identifier for the build
* example: "550e8400-e29b-41d4-a716-446655440000"
* status:
* type: string
* description: Current status of the build
* example: "success"
* metadata:
* type: object
* required:
* - currentPage
* - totalPages
* - total
* - limit
* properties:
* currentPage:
* type: integer
* description: Current page number
* example: 1
* totalPages:
* type: integer
* description: Total number of pages
* example: 5
* total:
* type: integer
* description: Total number of builds after filtering
* example: 100
* limit:
* type: integer
* description: Number of items per page
* example: 20
* 405:
* description: Method not allowed
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: "POST is not allowed"
* 500:
* description: Internal server error
* content:
* application/json:
* schema:
* type: object
* properties:
* error:
* type: string
* example: "An unexpected error occurred"
*/
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method !== 'GET') {
return res.status(405).json({ error: `${req.method} is not allowed` });
}

try {
const buildService = new BuildService();

// Extract query parameters
const { exclude, page, limit } = req.query;

// Parse exclude parameter - default to ['torn_down', 'pending']
let excludeStatuses: string[] = ['torn_down', 'pending'];
if (exclude) {
if (Array.isArray(exclude)) {
excludeStatuses = exclude.filter((status) => typeof status === 'string');
} else if (typeof exclude === 'string') {
try {
const parsed = JSON.parse(exclude);
if (Array.isArray(parsed)) {
excludeStatuses = parsed.filter((status) => typeof status === 'string');
}
} catch {
// If not valid JSON, treat as single value
excludeStatuses = [exclude];
}
}
}

// Build base query
let query = buildService.db.models.Build.query().select('uuid', 'status').whereNotIn('status', excludeStatuses);

// Handle pagination
let response: any = {};

if (page || limit) {
// If pagination params provided, apply pagination
const pageNumber = Math.max(1, parseInt(page as string, 10) || 1);
const limitNumber = Math.max(1, parseInt(limit as string, 10) || 20);
const offset = (pageNumber - 1) * limitNumber;

// Get total count
const totalCount = await query.resultSize();

// Apply pagination
const builds = await query.offset(offset).limit(limitNumber);

response = {
builds,
metadata: {
currentPage: pageNumber,
totalPages: Math.ceil(totalCount / limitNumber),
total: totalCount,
limit: limitNumber,
},
};
} else {
// No pagination - return all results
const builds = await query;

response = {
builds,
metadata: {
currentPage: 1,
totalPages: 1,
total: builds.length,
limit: builds.length,
},
};
}

return res.status(200).json(response);
} catch (error) {
logger.error('Error fetching builds:', error);
return res.status(500).json({ error: 'An unexpected error occurred' });
}
};
Loading