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
243 changes: 243 additions & 0 deletions apps/api/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,85 @@
]
}
},
"/v1/items/{id}": {
"get": {
"operationId": "ItemsController_get",
"parameters": [
{
"name": "id",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemDetailDto"
}
}
}
},
"401": {
"description": "unauthenticated",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetailsDto"
}
],
"properties": {
"code": {
"type": "string",
"enum": [
"unauthenticated"
]
}
}
}
}
}
},
"404": {
"description": "not_found",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetailsDto"
}
],
"properties": {
"code": {
"type": "string",
"enum": [
"not_found"
]
}
}
}
}
}
}
},
"security": [
{
"bearer": []
}
],
"tags": [
"items"
]
}
},
"/v1/items/{id}/children": {
"get": {
"operationId": "ItemsController_listChildren",
Expand Down Expand Up @@ -617,6 +696,17 @@
"schema": {
"type": "string"
}
},
{
"name": "type",
"required": false,
"in": "query",
"schema": {
"enum": [
"folder"
],
"type": "string"
}
}
],
"responses": {
Expand Down Expand Up @@ -786,6 +876,109 @@
]
}
},
"/v1/folders": {
"post": {
"operationId": "FoldersController_create",
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/CreateFolderDto"
}
}
}
},
"responses": {
"201": {
"description": "",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ItemDto"
}
}
}
},
"400": {
"description": "validation_error, max_depth_exceeded",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetailsDto"
}
],
"properties": {
"code": {
"type": "string",
"enum": [
"validation_error",
"max_depth_exceeded"
]
}
}
}
}
}
},
"401": {
"description": "unauthenticated",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetailsDto"
}
],
"properties": {
"code": {
"type": "string",
"enum": [
"unauthenticated"
]
}
}
}
}
}
},
"404": {
"description": "not_found",
"content": {
"application/problem+json": {
"schema": {
"allOf": [
{
"$ref": "#/components/schemas/ProblemDetailsDto"
}
],
"properties": {
"code": {
"type": "string",
"enum": [
"not_found"
]
}
}
}
}
}
}
},
"security": [
{
"bearer": []
}
],
"tags": [
"folders"
]
}
},
"/v1/uploads": {
"post": {
"operationId": "UploadsController_create",
Expand Down Expand Up @@ -1424,6 +1617,39 @@
"nextCursor"
]
},
"ItemTrailEntryDto": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
},
"required": [
"id",
"name"
]
},
"ItemDetailDto": {
"type": "object",
"properties": {
"item": {
"$ref": "#/components/schemas/ItemDto"
},
"trail": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ItemTrailEntryDto"
}
}
},
"required": [
"item",
"trail"
]
},
"DownloadUrlDto": {
"type": "object",
"properties": {
Expand All @@ -1440,6 +1666,23 @@
"expiresAt"
]
},
"CreateFolderDto": {
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1,
"maxLength": 255
},
"parentId": {
"type": "string"
}
},
"required": [
"name",
"parentId"
]
},
"CreateUploadDto": {
"type": "object",
"properties": {
Expand Down
20 changes: 20 additions & 0 deletions apps/api/src/modules/items/dto/create-folder.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Transform } from 'class-transformer';
import { IsNotEmpty, IsString, MaxLength, MinLength } from 'class-validator';
import { IsFileName } from '../../../common/validation/file-name.decorator.js';

/** A criação de uma pasta (seção 3.3 do docs/lld.md). */
export class CreateFolderDto {
/** Sem os espaços das pontas: o tamanho vale depois de descartá-los. */
@Transform(({ value }: { value: unknown }) =>
typeof value === 'string' ? value.trim() : value,
)
@MinLength(1)
@MaxLength(255)
@IsFileName()
name: string;

/** `root` ou o id de uma pasta ativa do Usuário. */
@IsString()
@IsNotEmpty()
parentId: string;
}
14 changes: 14 additions & 0 deletions apps/api/src/modules/items/dto/item-detail.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { ItemDetail, ItemTrailEntry } from '../entities/item.entity.js';
import { ItemDto } from './item.dto.js';

/** Um trecho do caminho do topo até a pasta. */
export class ItemTrailEntryDto implements ItemTrailEntry {
id: string;
name: string;
}

/** Uma pasta e o caminho do topo, a Pasta raiz, até ela. */
export class ItemDetailDto implements ItemDetail {
item: ItemDto;
trail: ItemTrailEntryDto[];
}
6 changes: 6 additions & 0 deletions apps/api/src/modules/items/dto/list-children-query.dto.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Type } from 'class-transformer';
import {
IsIn,
IsInt,
IsNotEmpty,
IsOptional,
Expand All @@ -25,4 +26,9 @@ export class ListChildrenQueryDto {
@IsString()
@IsNotEmpty()
cursor?: string;

/** Só `folder`: devolve apenas as pastas. Ausente, devolve pastas e arquivos. */
@IsOptional()
@IsIn(['folder'])
type?: 'folder';
}
34 changes: 34 additions & 0 deletions apps/api/src/modules/items/entities/item.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,40 @@ export interface ItemPage {
*/
export type ItemPosition = Pick<Item, 'type' | 'name' | 'id'>;

/** Um trecho do caminho até uma pasta. */
export interface ItemTrailEntry {
id: string;
name: string;
}

/** Um trecho do caminho, com a data que a lista mostra. */
export interface FolderPathEntry extends ItemTrailEntry {
updatedAt: Date;
}

/** Uma pasta e o caminho do topo, a Pasta raiz, até ela. */
export interface ItemDetail {
item: Item;
trail: ItemTrailEntry[];
}

/** A pasta que a criação grava, já com o id e o destino resolvidos. */
export interface NewFolder {
id: string;
ownerId: string;
parentId: string;
name: string;
}

/** O que a criação de uma pasta resolve: criada, sem destino ou passando do limite. */
export type CreateFolderResult =
| { status: 'created'; folder: Item }
| { status: 'parent_not_found' }
| { status: 'too_deep' };

/** Quantas pastas o caminho de um Item pode ter, sem contar a Pasta raiz. */
export const MAX_FOLDER_DEPTH = 50;

/** Um arquivo de um Usuário que ainda está sendo enviado (`pending`) ou já foi concluído (`active`). */
export interface FileUpload extends Item {
status: 'pending' | 'active';
Expand Down
11 changes: 11 additions & 0 deletions apps/api/src/modules/items/errors/max-depth-exceeded.error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DomainError } from '../../../common/errors/domain-error.js';
import { MAX_FOLDER_DEPTH } from '../entities/item.entity.js';

/** A pasta passaria do 50º nível de profundidade. */
export class MaxDepthExceededError extends DomainError {
readonly code = 'max_depth_exceeded';

constructor() {
super(`As pastas podem ter no máximo ${MAX_FOLDER_DEPTH} níveis.`);
}
}
Loading
Loading