Skip to content
Draft
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
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel

### Added

- Datasets: `listDatasetTreeNode` use case and repository method backing `GET /datasets/{id}/versions/{versionId}/tree` for paginated, lazy listing of folders/files inside a dataset version. Returns `FileTreePage` with folder-first ordering, opaque keyset cursors, and per-file `downloadUrl`.
- Datasets: `iterateDatasetTreeNode` async generator that walks the cursor chain so callers can consume one folder's children without driving pagination by hand.
- Core: re-export `DataverseApiAuthMechanism` from the public surface so consumers of the standalone reusable bundles (e.g. `dv-tree-view`, `dv-uploader`) can import it without reaching into `core/...`.

### Changed

### Fixed
Expand All @@ -28,6 +32,8 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel
- Templates: Added `setTemplateAsDefault` use case and repository method to support Dataverse endpoint `POST /dataverses/{id}/template/default/{templateId}`.
- Templates: Added `unsetTemplateAsDefault` use case and repository method to support Dataverse endpoint `DELETE /dataverses/{id}/template/default`.
- New Use Case: [Update Terms of Access](./docs/useCases.md#update-terms-of-access).
- Files: Direct uploads now forward the `tagging` value returned by the upload destination response as the `x-amz-tagging` header for single-part uploads.
- Files: Added a `DirectUploadClientConfig` object for configuring multipart upload retries and upload timeout.
- Guestbooks: Added use cases and repository support for guestbook creation, listing, and enabling/disabling.
- Guestbooks: Added dataset-level guestbook assignment and removal support via `assignDatasetGuestbook` (`PUT /api/datasets/{identifier}/guestbook`) and `removeDatasetGuestbook` (`DELETE /api/datasets/{identifier}/guestbook`).
- Datasets/Guestbooks: Added `guestbookId` in `getDataset` responses.
Expand All @@ -38,10 +44,11 @@ This changelog follows the principles of [Keep a Changelog](https://keepachangel

### Changed

- Add pagination query parameters to Dataset Version Summeries and File Version Summaries use cases.
- Add pagination query parameters to Dataset Version Summaries and File Version Summaries use cases.
- Templates: Rename `CreateDatasetTemplateDTO` to `CreateTemplateDTO`.
- Templates: Rename `createDatasetTemplate` repository method to `createTemplate`.
- Templates: Rename `getDatasetTemplates` repository method to `getTemplatesByCollectionId`.
- Files: `DirectUploadClient` constructor now accepts a `DirectUploadClientConfig` object instead of a plain number for `maxMultipartRetries`.

### Fixed

Expand Down
75 changes: 75 additions & 0 deletions docs/useCases.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ The different use cases currently available in the package are classified below,
- [Get Dataset Available Dataset Types](#get-dataset-available-dataset-types)
- [Get Dataset Available Dataset Type](#get-dataset-available-dataset-type)
- [Get Dataset Upload Limits](#get-dataset-upload-limits)
- [List a Folder of a Dataset Version (Tree View)](#list-a-folder-of-a-dataset-version-tree-view)
- [Iterate a Folder of a Dataset Version (Tree View)](#iterate-a-folder-of-a-dataset-version-tree-view)
- [Datasets write use cases](#datasets-write-use-cases)
- [Create a Dataset](#create-a-dataset)
- [Update a Dataset](#update-a-dataset)
Expand Down Expand Up @@ -1619,6 +1621,79 @@ _See [use case](../src/datasets/domain/useCases/GetDatasetUploadLimits.ts) imple

If the backend does not define any quota limits for the dataset, the returned object can be empty (`{}`).

#### List a Folder of a Dataset Version (Tree View)

Returns a [FileTreePage](../src/datasets/domain/models/FileTreePage.ts) for the immediate children (folders and files) inside a folder of a dataset version, intended for lazy tree-view UIs that fetch each folder's children on demand.

Folders come first, then files. Both are name-sorted (case-insensitive); files break ties on data file id for stability. The page carries an opaque `nextCursor` token; clients echo it back to fetch the next page and never construct one themselves.

##### Example call:

```typescript
import { listDatasetTreeNode, FileTreePage } from '@iqss/dataverse-client-javascript'

/* ... */

const datasetId = 'doi:10.77777/FK2/AAAAAA'

listDatasetTreeNode
.execute({
datasetId,
datasetVersionId: '1.0',
path: 'data/raw',
limit: 100
})
.then((page: FileTreePage) => {
/* ... */
})

/* ... */
```

_See [use case](../src/datasets/domain/useCases/ListDatasetTreeNode.ts) implementation_.

`datasetId` can be a numeric id or a persistent identifier string. `datasetVersionId` is optional and defaults to `DatasetNotNumberedVersion.LATEST`.

Other optional parameters: `cursor` (opaque, from a previous response), `include` (`'all' | 'folders' | 'files'`, default `'all'`), `order` (`'NameAZ' | 'NameZA'`, default `'NameAZ'`), `includeDeaccessioned` (default `false`), and `originals` (when `true`, the per-file `downloadUrl` carries `?format=original`).

For published, non-deaccessioned versions the underlying API emits `ETag` + `Cache-Control: public, immutable` headers. Drafts and deaccessioned versions don't.

#### Iterate a Folder of a Dataset Version (Tree View)

Returns an async generator over [FileTreeNode](../src/datasets/domain/models/FileTreeNode.ts) values for one folder, walking the cursor chain so callers can consume the children without driving pagination by hand.

##### Example call:

```typescript
import {
iterateDatasetTreeNode,
FileTreeNode,
isFileTreeFileNode
} from '@iqss/dataverse-client-javascript'

/* ... */

const datasetId = 'doi:10.77777/FK2/AAAAAA'

for await (const node of iterateDatasetTreeNode.execute({
datasetId,
datasetVersionId: '1.0',
path: 'data/raw'
})) {
if (isFileTreeFileNode(node)) {
/* ... */
} else {
/* node is a folder ... */
}
}

/* ... */
```

_See [use case](../src/datasets/domain/useCases/IterateDatasetTreeNode.ts) implementation_.

The generator stops after yielding everything in the requested folder; it does **not** descend into subfolders. Pass each subfolder's `path` back through `iterateDatasetTreeNode` if you want a recursive walk.

## Files

### Files read use cases
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"test:coverage": "jest --coverage -c jest.config.ts",
"test:coverage:check": "jest --coverage --ci --config jest.config.ts",
"lint": "npm run lint:eslint && npm run lint:prettier",
"lint:fix": "eslint --fix --ext .ts ./src --ignore-path .gitignore .",
"lint:eslint": "eslint --ignore-path .gitignore .",
"lint:fix": "eslint --fix --ext .ts --ignore-path .gitignore ./src ./test/unit ./test/integration ./test/functional",
"lint:eslint": "eslint --ignore-path .gitignore ./src ./test/unit ./test/integration ./test/functional",
"lint:prettier": "prettier --check '**/*.(yml|json|md)'",
"format": "prettier --write './**/*.{js,ts,md,json,yml,md}' --config ./.prettierrc",
"format": "prettier --write './src/**/*.{js,ts,md,json,yml,md}' './test/{unit,integration,functional}/**/*.{js,ts,json}' --config ./.prettierrc",
"typecheck": "tsc --noEmit",
"prepare": "husky"
},
Expand Down
2 changes: 1 addition & 1 deletion src/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export { ReadError } from './domain/repositories/ReadError'
export { WriteError } from './domain/repositories/WriteError'
export { ApiConfig } from './infra/repositories/ApiConfig'
export { ApiConfig, DataverseApiAuthMechanism } from './infra/repositories/ApiConfig'
export { DvObjectOwnerNode, DvObjectType } from './domain/models/DvObjectOwnerNode'
export { PublicationStatus } from './domain/models/PublicationStatus'
37 changes: 37 additions & 0 deletions src/datasets/domain/models/FileTreeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export enum FileTreeNodeType {
FOLDER = 'folder',
FILE = 'file'
}

export interface FileTreeFolderNode {
type: FileTreeNodeType.FOLDER
name: string
path: string
counts?: {
files: number
folders: number
}
}

export interface FileTreeFileNode {
type: FileTreeNodeType.FILE
id: number
name: string
path: string
size: number
contentType?: string
access?: 'public' | 'restricted' | 'embargoed'
checksum?: {
type: string
value: string
}
downloadUrl: string
}

export type FileTreeNode = FileTreeFolderNode | FileTreeFileNode

export const isFileTreeFolderNode = (node: FileTreeNode): node is FileTreeFolderNode =>
node.type === FileTreeNodeType.FOLDER

export const isFileTreeFileNode = (node: FileTreeNode): node is FileTreeFileNode =>
node.type === FileTreeNodeType.FILE
22 changes: 22 additions & 0 deletions src/datasets/domain/models/FileTreePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { FileTreeNode } from './FileTreeNode'

export enum FileTreeInclude {
ALL = 'all',
FOLDERS = 'folders',
FILES = 'files'
}

export enum FileTreeOrder {
NAME_AZ = 'NameAZ',
NAME_ZA = 'NameZA'
}

export interface FileTreePage {
path: string
items: FileTreeNode[]
nextCursor: string | null
limit: number
order: FileTreeOrder
include: FileTreeInclude
approximateCount?: number
}
14 changes: 14 additions & 0 deletions src/datasets/domain/repositories/IDatasetsRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,19 @@ import { DatasetLicenseUpdateRequest } from '../dtos/DatasetLicenseUpdateRequest
import { DatasetTypeDTO } from '../dtos/DatasetTypeDTO'
import { StorageDriver } from '../models/StorageDriver'
import { DatasetUploadLimits } from '../models/DatasetUploadLimits'
import { FileTreePage, FileTreeInclude, FileTreeOrder } from '../models/FileTreePage'

export interface ListDatasetTreeNodeParams {
datasetId: number | string
datasetVersionId?: string
path?: string
limit?: number
cursor?: string
include?: FileTreeInclude
order?: FileTreeOrder
includeDeaccessioned?: boolean
originals?: boolean
}

export interface IDatasetsRepository {
getDataset(
Expand Down Expand Up @@ -104,4 +117,5 @@ export interface IDatasetsRepository {
): Promise<void>
getDatasetStorageDriver(datasetId: number | string): Promise<StorageDriver>
getDatasetUploadLimits(datasetId: number | string): Promise<DatasetUploadLimits>
listDatasetTreeNode(params: ListDatasetTreeNodeParams): Promise<FileTreePage>
}
30 changes: 30 additions & 0 deletions src/datasets/domain/useCases/IterateDatasetTreeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { IDatasetsRepository, ListDatasetTreeNodeParams } from '../repositories/IDatasetsRepository'
import { FileTreeNode } from '../models/FileTreeNode'

/**
* Async generator that exhaustively iterates the immediate children of the
* given path inside a dataset version, transparently following the
* `nextCursor` chain.
*
* Use this when you need every direct child of a folder; it does NOT recurse
* into subfolders — that is the caller's responsibility (e.g. pre-download
* enumeration walks the tree by re-invoking this iterator with each folder
* path it discovers).
*/
export class IterateDatasetTreeNode {
constructor(private readonly datasetsRepository: IDatasetsRepository) {}

async *execute(params: ListDatasetTreeNodeParams): AsyncGenerator<FileTreeNode> {
let cursor = params.cursor
do {
const page = await this.datasetsRepository.listDatasetTreeNode({
...params,
cursor
})
for (const item of page.items) {
yield item
}
cursor = page.nextCursor ?? undefined
} while (cursor)
}
}
20 changes: 20 additions & 0 deletions src/datasets/domain/useCases/ListDatasetTreeNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { UseCase } from '../../../core/domain/useCases/UseCase'
import { IDatasetsRepository, ListDatasetTreeNodeParams } from '../repositories/IDatasetsRepository'
import { FileTreePage } from '../models/FileTreePage'

export class ListDatasetTreeNode implements UseCase<FileTreePage> {
constructor(private readonly datasetsRepository: IDatasetsRepository) {}

/**
* Lists the immediate children of the given folder path inside a dataset
* version, returning a single page of folders and files.
*
* Folders are returned first, then files. Both are sorted by name. Use the
* returned `nextCursor` to keep paging the same folder. The cursor is
* opaque to callers and is server-validated; an invalid cursor yields a 400
* from the API.
*/
async execute(params: ListDatasetTreeNodeParams): Promise<FileTreePage> {
return this.datasetsRepository.listDatasetTreeNode(params)
}
}
18 changes: 17 additions & 1 deletion src/datasets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ import { UpdateTermsOfAccess } from './domain/useCases/UpdateTermsOfAccess'
import { UpdateDatasetLicense } from './domain/useCases/UpdateDatasetLicense'
import { GetDatasetStorageDriver } from './domain/useCases/GetDatasetStorageDriver'
import { GetDatasetUploadLimits } from './domain/useCases/GetDatasetUploadLimits'
import { ListDatasetTreeNode } from './domain/useCases/ListDatasetTreeNode'
import { IterateDatasetTreeNode } from './domain/useCases/IterateDatasetTreeNode'

const datasetsRepository = new DatasetsRepository()

Expand Down Expand Up @@ -86,6 +88,8 @@ const updateTermsOfAccess = new UpdateTermsOfAccess(datasetsRepository)
const updateDatasetLicense = new UpdateDatasetLicense(datasetsRepository)
const getDatasetStorageDriver = new GetDatasetStorageDriver(datasetsRepository)
const getDatasetUploadLimits = new GetDatasetUploadLimits(datasetsRepository)
const listDatasetTreeNode = new ListDatasetTreeNode(datasetsRepository)
const iterateDatasetTreeNode = new IterateDatasetTreeNode(datasetsRepository)

export {
getDataset,
Expand Down Expand Up @@ -118,7 +122,9 @@ export {
deleteDatasetType,
updateDatasetLicense,
getDatasetStorageDriver,
getDatasetUploadLimits
getDatasetUploadLimits,
listDatasetTreeNode,
iterateDatasetTreeNode
}
export { DatasetNotNumberedVersion } from './domain/models/DatasetNotNumberedVersion'
export { DatasetUserPermissions } from './domain/models/DatasetUserPermissions'
Expand Down Expand Up @@ -159,3 +165,13 @@ export { DatasetType } from './domain/models/DatasetType'
export { DatasetTypeDTO } from './domain/dtos/DatasetTypeDTO'
export { StorageDriver } from './domain/models/StorageDriver'
export { DatasetUploadLimits } from './domain/models/DatasetUploadLimits'
export {
FileTreeNode,
FileTreeFolderNode,
FileTreeFileNode,
FileTreeNodeType,
isFileTreeFolderNode,
isFileTreeFileNode
} from './domain/models/FileTreeNode'
export { FileTreePage, FileTreeInclude, FileTreeOrder } from './domain/models/FileTreePage'
export { ListDatasetTreeNodeParams } from './domain/repositories/IDatasetsRepository'
37 changes: 36 additions & 1 deletion src/datasets/infra/repositories/DatasetsRepository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import { ApiRepository } from '../../../core/infra/repositories/ApiRepository'
import { IDatasetsRepository } from '../../domain/repositories/IDatasetsRepository'
import {
IDatasetsRepository,
ListDatasetTreeNodeParams
} from '../../domain/repositories/IDatasetsRepository'
import { DatasetNotNumberedVersion } from '../../domain/models/DatasetNotNumberedVersion'
import { FileTreeInclude, FileTreeOrder, FileTreePage } from '../../domain/models/FileTreePage'
import { transformTreeResponseToFileTreePage } from './transformers/fileTreeTransformers'
import { Dataset, VersionUpdateType } from '../../domain/models/Dataset'
import {
transformVersionResponseToDataset,
Expand Down Expand Up @@ -523,4 +529,33 @@ export class DatasetsRepository extends ApiRepository implements IDatasetsReposi
throw error
})
}

public async listDatasetTreeNode(params: ListDatasetTreeNodeParams): Promise<FileTreePage> {
const versionId = params.datasetVersionId ?? DatasetNotNumberedVersion.LATEST
const queryParams: Record<string, string | number | boolean> = {}
if (params.path !== undefined) queryParams.path = params.path
if (params.limit !== undefined) queryParams.limit = params.limit
if (params.cursor !== undefined) queryParams.cursor = params.cursor
queryParams.include = params.include ?? FileTreeInclude.ALL
queryParams.order = params.order ?? FileTreeOrder.NAME_AZ
if (params.includeDeaccessioned !== undefined) {
queryParams.includeDeaccessioned = params.includeDeaccessioned
}
if (params.originals !== undefined) {
queryParams.originals = params.originals
}
return this.doGet(
this.buildApiEndpoint(
this.datasetsResourceName,
`versions/${versionId}/tree`,
params.datasetId
),
true,
queryParams
)
.then((response) => transformTreeResponseToFileTreePage(response))
.catch((error) => {
throw error
})
}
}
Loading
Loading