Skip to content

Commit 93911e1

Browse files
committed
support for continuation headers
1 parent 27d7010 commit 93911e1

File tree

10 files changed

+108
-4
lines changed

10 files changed

+108
-4
lines changed

docs/openapi/headers.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,8 @@ location:
88
description: The redirect location
99
schema:
1010
type: string
11+
12+
xDaContinuationToken:
13+
description: Continuation token for fetching the next page of list results.
14+
schema:
15+
type: string

docs/openapi/list-api.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ list:
99
- $ref: "./parameters.yaml#/orgParam"
1010
- $ref: "./parameters.yaml#/repoParam"
1111
- $ref: "./parameters.yaml#/pathParam"
12+
- $ref: "./parameters.yaml#/continuationTokenParam"
1213
responses:
1314
'200':
1415
$ref: "./responses.yaml#/list/200"
1516
'400':
1617
$ref: "./responses.yaml#/400"
1718
'404':
18-
$ref: "./responses.yaml#/404"
19+
$ref: "./responses.yaml#/404"

docs/openapi/parameters.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ pathParam:
2626
required: true
2727
schema:
2828
type: string
29+
continuationTokenParam:
30+
name: continuation-token
31+
in: query
32+
description: Continuation token for paginated list results.
33+
required: false
34+
schema:
35+
type: string
2936
guidParam:
3037
name: guid
3138
in: path

docs/openapi/responses.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ source:
3535
list:
3636
'200':
3737
description: The list of sources
38+
headers:
39+
X-da-continuation-token:
40+
$ref: "./headers.yaml#/xDaContinuationToken"
3841
content:
3942
application/json:
4043
schema:

src/storage/object/list.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,27 @@ import getS3Config from '../utils/config.js';
1818
import formatList from '../utils/list.js';
1919

2020
function buildInput({
21-
bucket, org, key, maxKeys,
21+
bucket, org, key, maxKeys, continuationToken,
2222
}) {
2323
const input = {
2424
Bucket: bucket,
2525
Prefix: key ? `${org}/${key}/` : `${org}/`,
2626
Delimiter: '/',
2727
};
2828
if (maxKeys) input.MaxKeys = maxKeys;
29+
if (continuationToken) input.ContinuationToken = continuationToken;
2930
return input;
3031
}
3132

3233
export default async function listObjects(env, daCtx, maxKeys) {
3334
const config = getS3Config(env);
3435
const client = new S3Client(config);
3536

36-
const input = buildInput({ ...daCtx, maxKeys });
37+
const input = buildInput({
38+
...daCtx,
39+
maxKeys,
40+
continuationToken: daCtx.continuationToken,
41+
});
3742
const command = new ListObjectsV2Command(input);
3843
try {
3944
const resp = await client.send(command);
@@ -43,6 +48,7 @@ export default async function listObjects(env, daCtx, maxKeys) {
4348
body: JSON.stringify(body),
4449
status: resp.$metadata.httpStatusCode,
4550
contentType: resp.ContentType,
51+
continuationToken: resp.NextContinuationToken,
4652
};
4753
} catch (e) {
4854
return { body: '', status: 404 };

src/utils/daCtx.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import { getAclCtx, getUsers } from './auth.js';
1818
* @returns {DaCtx} The Dark Alley Context.
1919
*/
2020
export default async function getDaCtx(req, env) {
21-
let { pathname } = new URL(req.url);
21+
const url = new URL(req.url);
22+
let { pathname } = url;
2223
// Remove proxied api route
2324
if (pathname.startsWith('/api')) pathname = pathname.replace('/api', '');
2425

@@ -38,6 +39,9 @@ export default async function getDaCtx(req, env) {
3839
// Extract conditional headers
3940
const ifMatch = req.headers?.get('if-match') || null;
4041
const ifNoneMatch = req.headers?.get('if-none-match') || null;
42+
const continuationToken = url.searchParams.get('continuation-token')
43+
|| url.searchParams.get('continuationToken')
44+
|| null;
4145

4246
// Set base details
4347
const daCtx = {
@@ -53,6 +57,7 @@ export default async function getDaCtx(req, env) {
5357
ifMatch,
5458
ifNoneMatch,
5559
},
60+
continuationToken,
5661
};
5762

5863
// Sanitize the remaining path parts

src/utils/daResp.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default function daResp({
2727
contentLength,
2828
metadata,
2929
etag,
30+
continuationToken,
3031
}, ctx = null) {
3132
const headers = new Headers();
3233
headers.append('Access-Control-Allow-Origin', '*');
@@ -48,6 +49,9 @@ export default function daResp({
4849
if (etag) {
4950
headers.append('ETag', etag);
5051
}
52+
if (continuationToken) {
53+
headers.append('X-da-continuation-token', continuationToken);
54+
}
5155

5256
if (ctx?.aclCtx && status < 500) {
5357
headers.append('X-da-actions', `/${ctx.key}=${[...ctx.aclCtx.actionSet]}`);

test/index.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,31 @@ describe('fetch', () => {
9090
const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/source/org/repo/file.html' }, {});
9191
assert.strictEqual(resp.status, 500);
9292
});
93+
94+
it('should expose continuation token header for list responses', async () => {
95+
const hnd = await esmock('../src/index.js', {
96+
'../src/utils/daCtx.js': {
97+
default: async () => ({
98+
authorized: true,
99+
users: [{ email: 'test@example.com' }],
100+
path: '/list/org/repo/path',
101+
key: 'repo/path',
102+
}),
103+
},
104+
'../src/handlers/get.js': {
105+
default: async () => ({
106+
status: 200,
107+
body: '[]',
108+
contentType: 'application/json',
109+
continuationToken: 'next-token',
110+
}),
111+
},
112+
});
113+
114+
const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/list/org/repo/path' }, {});
115+
assert.strictEqual(resp.status, 200);
116+
assert.strictEqual(resp.headers.get('X-da-continuation-token'), 'next-token');
117+
});
93118
});
94119

95120
describe('invalid routes', () => {

test/storage/object/list.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,28 @@ describe('List Objects', () => {
5656
const data = JSON.parse(resp.body);
5757
assert.strictEqual(data.length, 2, 'Should only return 2 items');
5858
});
59+
60+
it('passes continuation token and returns next token', async () => {
61+
s3Mock.on(ListObjectsV2Command, {
62+
Bucket: 'rt-bkt',
63+
Prefix: 'acme/wknd/',
64+
Delimiter: '/',
65+
ContinuationToken: 'prev-token',
66+
}).resolves({
67+
$metadata: { httpStatusCode: 200 },
68+
Contents: [Contents[0]],
69+
NextContinuationToken: 'next-token',
70+
});
71+
72+
const daCtx = {
73+
bucket: 'rt-bkt',
74+
org: 'acme',
75+
key: 'wknd',
76+
continuationToken: 'prev-token',
77+
};
78+
const resp = await listObjects({}, daCtx);
79+
const data = JSON.parse(resp.body);
80+
assert.strictEqual(data.length, 1, 'Should only return 1 item');
81+
assert.strictEqual(resp.continuationToken, 'next-token');
82+
});
5983
});

test/utils/daCtx.test.js

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,28 @@ describe('DA context', () => {
217217
assert.strictEqual(daCtx.conditionalHeaders.ifNoneMatch, null);
218218
});
219219
});
220+
221+
describe('Continuation token', async () => {
222+
it('should extract continuation-token query param', async () => {
223+
const req = {
224+
url: 'http://localhost:8787/list/org/site/path?continuation-token=token123',
225+
headers: {
226+
get: () => null,
227+
},
228+
};
229+
const daCtx = await getDaCtx(req, env);
230+
assert.strictEqual(daCtx.continuationToken, 'token123');
231+
});
232+
233+
it('should default continuation token to null', async () => {
234+
const req = {
235+
url: 'http://localhost:8787/list/org/site/path',
236+
headers: {
237+
get: () => null,
238+
},
239+
};
240+
const daCtx = await getDaCtx(req, env);
241+
assert.strictEqual(daCtx.continuationToken, null);
242+
});
243+
});
220244
});

0 commit comments

Comments
 (0)