refactor(API): Migrate GET data-tables columns endpoint to PublicApiController (no-changelog) (#39772)

This commit is contained in:
Sam Wooler 2026-09-28 14:45:03 +00:00 committed by GitHub
parent 80a1596a3d
commit bd2d361afc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 175 additions and 47 deletions

View File

@ -62,3 +62,9 @@ export class CreateDataTablePublicDto extends Z.class({
export class UpdateDataTablePublicDto extends Z.class({
name: dataTableNameSchema.openapi(updateDataTableFieldDocs.name),
}) {}
const dataTableListColumnPublicSchema = dataTableColumnPublicSchema.extend({
dataTableId: z.string().openapi(dataTableColumnFieldDocs.dataTableId),
});
export class DataTableColumnListPublicDto extends Z.array(dataTableListColumnPublicSchema) {}

View File

@ -24,6 +24,7 @@ export const dataTableColumnFieldDocs = {
index: { type: 'integer', description: 'Column position' },
createdAt: { description: 'Timestamp when the column was created' },
updatedAt: { description: 'Timestamp when the column was last updated' },
dataTableId: { description: 'ID of the data table this column belongs to' },
} as const satisfies Record<string, ZodOpenAPIMetadata>;
export const dataTableListFieldDocs = {

View File

@ -422,6 +422,7 @@ export { CreateDataTableDto } from './data-table/create-data-table.dto';
export { UpdateDataTableDto } from './data-table/update-data-table.dto';
export {
CreateDataTablePublicDto,
DataTableColumnListPublicDto,
DataTableListPublicDto,
DataTablePublicDto,
UpdateDataTablePublicDto,

View File

@ -159,8 +159,6 @@ export declare namespace DataTableRequest {
}
>;
type ListColumns = AuthenticatedRequest<{ dataTableId: string }, {}, {}, {}>;
type CreateColumn = AuthenticatedRequest<{ dataTableId: string }, {}, AddDataTableColumnDto, {}>;
type DeleteColumn = AuthenticatedRequest<{ dataTableId: string; columnId: string }, {}, {}, {}>;

View File

@ -1,5 +1,6 @@
import {
CreateDataTablePublicDto,
DataTableColumnListPublicDto,
DataTableListPublicDto,
DataTablePublicDto,
PublicApiListDataTableQueryDto,
@ -31,6 +32,7 @@ import { ConflictError } from '@/errors/response-errors/conflict.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { DataTableAggregateService } from '@/modules/data-table/data-table-aggregate.service';
import type { DataTableColumn } from '@/modules/data-table/data-table-column.entity';
import type { DataTable } from '@/modules/data-table/data-table.entity';
import { DataTableService } from '@/modules/data-table/data-table.service';
import { DataTableAccessDeniedError } from '@/modules/data-table/errors/data-table-access-denied.error';
@ -83,6 +85,18 @@ const toDataTablePublicDto = (dataTable: DataTable, sizeBytes: number): DataTabl
sizeBytes,
});
const toDataTableColumnPublicDto = (
column: DataTableColumn,
): DataTableColumnListPublicDto[number] => ({
id: column.id,
name: column.name,
dataTableId: column.dataTableId,
type: column.type,
index: column.index,
createdAt: column.createdAt.toISOString(),
updatedAt: column.updatedAt.toISOString(),
});
@PublicApiController('/data-tables')
export class DataTablesPublicController {
constructor(
@ -232,6 +246,25 @@ export class DataTablesPublicController {
}
}
@Get('/:dataTableId/columns')
@ApiKeyScope('dataTableColumn:read')
@ProjectScope('dataTable:readColumn')
@ApiSummary('List columns of a data table')
@ApiDescription('Retrieve all columns for a specific data table.')
@ApiTags(tags)
@ApiResponse(200, DataTableColumnListPublicDto)
@ApiErrorResponse(404)
async listDataTableColumns(
_req: AuthenticatedRequest,
_res: Response,
@Param('dataTableId', dataTableIdParamSchema) dataTableId: string,
): Promise<DataTableColumnListPublicDto> {
const projectId = await this.dataTableService.getProjectIdForDataTable(dataTableId);
const columns = await this.dataTableService.getColumns(dataTableId, projectId);
return columns.map(toDataTableColumnPublicDto);
}
private async withSize(dataTable: DataTable): Promise<DataTablePublicDto> {
const sizes = await this.dataTableService.getCachedSizeBytesByIds([dataTable.id]);

View File

@ -23,23 +23,12 @@ const handleError = (error: unknown) => {
};
type DataTableColumnsHandlers = {
listDataTableColumns: PublicAPIEndpoint<DataTableRequest.ListColumns>;
createDataTableColumn: PublicAPIEndpoint<DataTableRequest.CreateColumn>;
deleteDataTableColumn: PublicAPIEndpoint<DataTableRequest.DeleteColumn>;
updateDataTableColumn: PublicAPIEndpoint<DataTableRequest.UpdateColumn>;
};
const dataTableColumnsHandlers: DataTableColumnsHandlers = {
listDataTableColumns: [
publicApiScope('dataTableColumn:read'),
projectScope('dataTable:readColumn', 'dataTable'),
async (req, res) => {
const { dataTableId } = req.params;
const projectId = await Container.get(DataTableService).getProjectIdForDataTable(dataTableId);
return res.json(await Container.get(DataTableService).getColumns(dataTableId, projectId));
},
],
createDataTableColumn: [
publicApiScope('dataTableColumn:create'),
projectScope('dataTable:writeColumn', 'dataTable'),

View File

@ -1,30 +1,3 @@
get:
x-eov-operation-id: listDataTableColumns
x-required-scope: dataTableColumn:read
x-eov-operation-handler: v1/handlers/data-tables/data-tables.columns.handler
tags:
- DataTable
summary: List columns of a data table
description: Retrieve all columns for a specific data table.
operationId: list-data-table-columns
parameters:
- $ref: '../schemas/parameters/dataTableId.yml'
responses:
'200':
description: Successfully retrieved columns
content:
application/json:
schema:
type: array
items:
$ref: '../schemas/dataTableColumn.yml'
'401':
$ref: '../../../../shared/spec/responses/unauthorized.yml'
'404':
$ref: '../../../../shared/spec/responses/notFound.yml'
security:
- ApiKeyAuth: []
post:
x-eov-operation-id: createDataTableColumn
x-required-scope: dataTableColumn:create

View File

@ -0,0 +1,70 @@
operationId: listDataTableColumns
tags:
- DataTable
summary: List columns of a data table
description: Retrieve all columns for a specific data table.
x-required-scope: dataTableColumn:read
x-eov-operation-id: unreachable
x-eov-operation-handler: v1/handlers/decorator-routed.handler
x-decorator-routed: true
parameters:
- schema:
type: string
format: nanoid
pattern: ^[A-Za-z0-9]{16}$
required: true
description: The ID of the data table
name: dataTableId
in: path
responses:
'200':
description: Operation successful.
content:
application/json:
schema:
type: array
items:
type: object
properties:
id:
type: string
description: Column ID
name:
type: string
description: Column name
type:
type: string
description: Column data type
enum:
- string
- number
- boolean
- date
index:
type: integer
description: Column position
createdAt:
type: string
format: date-time
description: Timestamp when the column was created
updatedAt:
type: string
format: date-time
description: Timestamp when the column was last updated
dataTableId:
type: string
description: ID of the data table this column belongs to
required:
- id
- name
- type
- index
- createdAt
- updatedAt
- dataTableId
'401':
$ref: ../../../../shared/spec/responses/unauthorized.yml
'403':
$ref: ../../../../shared/spec/responses/forbidden.yml
'404':
$ref: ../../../../shared/spec/responses/notFound.yml

View File

@ -74,6 +74,9 @@ paths:
$ref: ./handlers/data-tables/spec/paths/updateDataTable.generated.yml
delete:
$ref: ./handlers/data-tables/spec/paths/deleteDataTable.generated.yml
/data-tables/{dataTableId}/columns:
get:
$ref: ./handlers/data-tables/spec/paths/listDataTableColumns.generated.yml
/discover:
get:
$ref: ./handlers/discover/spec/paths/getDiscover.generated.yml

View File

@ -2079,6 +2079,25 @@ describe('GET /data-tables/:dataTableId/columns', () => {
testWithAPIKey('get', '/data-tables/123/columns', 'abcXYZ'),
);
test('should reject listing without dataTableColumn:read', async () => {
const dataTable = await createDataTable(ownerPersonalProject, {
name: 'scope-columns-table',
columns: [{ name: 'col1', type: 'string' }],
});
const unscopedAgent = await createUnscopedAgent();
const response = await unscopedAgent.get(`/data-tables/${dataTable.id}/columns`);
expect(response.statusCode).toBe(403);
});
test('should reject a malformed data table id', async () => {
const response = await authOwnerAgent.get('/data-tables/not-a-nanoid/columns');
expect(response.statusCode).toBe(400);
expect(response.body).toHaveProperty('message');
});
test('should list columns for a data table', async () => {
const dataTable = await createDataTable(ownerPersonalProject, {
name: 'columns-test',
@ -2086,19 +2105,54 @@ describe('GET /data-tables/:dataTableId/columns', () => {
{ name: 'name', type: 'string' },
{ name: 'age', type: 'number' },
{ name: 'active', type: 'boolean' },
{ name: 'signedUpAt', type: 'date' },
],
});
const response = await authOwnerAgent.get(`/data-tables/${dataTable.id}/columns`);
expect(response.statusCode).toBe(200);
expect(Array.isArray(response.body)).toBe(true);
expect(response.body).toHaveLength(3);
expect(response.body[0]).toHaveProperty('id');
expect(response.body[0]).toHaveProperty('name');
expect(response.body[0]).toHaveProperty('type');
expect(response.body[0]).toHaveProperty('index');
expect(response.body[0]).toHaveProperty('dataTableId', dataTable.id);
// Columns aren't guaranteed to come back in index order, so sort before comparing.
const columns = [...response.body].sort((a, b) => a.index - b.index);
expect(columns).toStrictEqual([
{
id: expect.any(String),
name: 'name',
type: 'string',
index: 0,
dataTableId: dataTable.id,
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
{
id: expect.any(String),
name: 'age',
type: 'number',
index: 1,
dataTableId: dataTable.id,
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
{
id: expect.any(String),
name: 'active',
type: 'boolean',
index: 2,
dataTableId: dataTable.id,
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
{
id: expect.any(String),
name: 'signedUpAt',
type: 'date',
index: 3,
dataTableId: dataTable.id,
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
]);
});
test('should return 404 for non-existing data table', async () => {