Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support manually handling continuationTokens #18179

Merged
merged 6 commits into from
Oct 15, 2021
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
3 changes: 2 additions & 1 deletion sdk/tables/data-tables/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Release History

## 12.1.3 (Unreleased)
## 12.2.0 (Unreleased)

### Features Added
- Take `continuationToken` as a `PageSetting` and expose it in the page when listing entities `byPage`. [#](https://github.com/Azure/azure-sdk-for-js/pull/)

### Breaking Changes

Expand Down
2 changes: 1 addition & 1 deletion sdk/tables/data-tables/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/data-tables",
"version": "12.1.3",
"version": "12.2.0",
"description": "An isomorphic client library for the Azure Tables service.",
"sdk-type": "client",
"main": "dist/index.js",
Expand Down
8 changes: 6 additions & 2 deletions sdk/tables/data-tables/review/data-tables.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ export class TableClient {
static fromConnectionString(connectionString: string, tableName: string, options?: TableServiceClientOptions): TableClient;
getAccessPolicy(options?: OperationOptions): Promise<GetAccessPolicyResponse>;
getEntity<T extends object = Record<string, unknown>>(partitionKey: string, rowKey: string, options?: GetTableEntityOptions): Promise<GetTableEntityResponse<TableEntityResult<T>>>;
listEntities<T extends object = Record<string, unknown>>(options?: ListTableEntitiesOptions): PagedAsyncIterableIterator<TableEntityResult<T>, TableEntityResult<T>[]>;
listEntities<T extends object = Record<string, unknown>>(options?: ListTableEntitiesOptions): PagedAsyncIterableIterator<TableEntityResult<T>, TableEntityResultPage<T>>;
joheredi marked this conversation as resolved.
Show resolved Hide resolved
pipeline: Pipeline;
setAccessPolicy(tableAcl: SignedIdentifier[], options?: OperationOptions): Promise<SetAccessPolicyResponse>;
submitTransaction(actions: TransactionAction[]): Promise<TableTransactionResponse>;
Expand Down Expand Up @@ -293,6 +293,11 @@ export type TableEntityResult<T> = T & {
timestamp?: string;
};

// @public
export type TableEntityResultPage<T> = Array<TableEntityResult<T>> & {
continuationToken?: string;
};

// @public
export interface TableGetAccessPolicyHeaders {
clientRequestId?: string;
Expand Down Expand Up @@ -448,7 +453,6 @@ export type UpdateTableEntityOptions = OperationOptions & {
// @public
export type UpsertEntityResponse = TableMergeEntityHeaders;


// (No @packageDocumentation comment for this package)

```
66 changes: 66 additions & 0 deletions sdk/tables/data-tables/samples-dev/manualEntityPageQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

one last thing, can you please rename the sample to something that closely related to what it does now? usingContinuationToken or something?

// Licensed under the MIT license.
/**
* This sample demonstrates how to query entities in a table by page by manually handling the continuation token
*
* @summary queries entities in a table by page manually handling continuation tokens
* @azsdk-weight 50
*/

import { TableClient, AzureSASCredential, TransactionAction } from "@azure/data-tables";

// Load the .env file if it exists
import * as dotenv from "dotenv";
dotenv.config();

const tablesUrl = process.env["TABLES_URL"] || "";
const sasToken = process.env["SAS_TOKEN"] || "";

async function listEntitiesPage() {
const tableName = `manualListByPage`;

// See authenticationMethods sample for other options of creating a new client
const client = new TableClient(tablesUrl, tableName, new AzureSASCredential(sasToken));
// Create the table
await client.createTable();

let actions: TransactionAction[] = [];

// Create 100 entities
for (let i = 0; i < 100; i++) {
actions.push(["create", { partitionKey: `one`, rowKey: `${i}`, foo: i }]);
}
await client.submitTransaction(actions);

// Limit the size to 2 entities by page
let iterator = client.listEntities().byPage({ maxPageSize: 2 });

// Iterating the pages to find the page that contains row key 50
let interestingPage: string | undefined;
for await (const page of iterator) {
if (page.some((p) => p.rowKey === "50")) {
interestingPage = page.continuationToken;
}
}

if (!interestingPage) {
console.error("Didn't find entity with rowKey = 50");
return;
}

// Fetch only the page that contains rowKey 50;
const page = await client
.listEntities()
.byPage({ maxPageSize: 2, continuationToken: interestingPage })
.next();

if (!page.done) {
for (const entity of page.value) {
console.log(entity.rowKey);
}
}
}

listEntitiesPage().catch((err) => {
console.error("The sample encountered an error:", err);
});
63 changes: 40 additions & 23 deletions sdk/tables/data-tables/src/TableClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,19 @@ import {
TransactionAction,
TableTransactionResponse,
SignedIdentifier,
GetAccessPolicyResponse
GetAccessPolicyResponse,
TableEntityResultPage
} from "./models";
import {
UpdateEntityResponse,
UpsertEntityResponse,
DeleteTableEntityResponse,
SetAccessPolicyResponse
} from "./generatedModels";
import { QueryOptions as GeneratedQueryOptions } from "./generated/models";
import {
QueryOptions as GeneratedQueryOptions,
TableQueryEntitiesOptionalParams
} from "./generated/models";
import { getClientParamsFromConnectionString } from "./utils/connectionString";
import {
isNamedKeyCredential,
Expand Down Expand Up @@ -64,6 +68,7 @@ import { isCredential } from "./utils/isCredential";
import { tablesSASTokenPolicy } from "./tablesSASTokenPolicy";
import { isCosmosEndpoint } from "./utils/isCosmosEndpoint";
import { cosmosPatchPolicy } from "./cosmosPathPolicy";
import { decodeContinuationToken, encodeContinuationToken } from "./utils/continuationToken";

/**
* A TableClient represents a Client to the Azure Tables service allowing you
Expand Down Expand Up @@ -438,7 +443,7 @@ export class TableClient {
public listEntities<T extends object = Record<string, unknown>>(
// eslint-disable-next-line @azure/azure-sdk/ts-naming-options
options: ListTableEntitiesOptions = {}
): PagedAsyncIterableIterator<TableEntityResult<T>, TableEntityResult<T>[]> {
): PagedAsyncIterableIterator<TableEntityResult<T>, TableEntityResultPage<T>> {
const tableName = this.tableName;
const iter = this.listEntitiesAll<T>(tableName, options);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: any chance we can rewrite this using getPagedAsyncIterator? :) no pressure though, feel free to ignore.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take on this as a separate PR. But I do agree we should refactor to use getPagedAsyncIterator


Expand All @@ -454,6 +459,11 @@ export class TableClient {
...options,
queryOptions: { ...options.queryOptions, top: settings?.maxPageSize }
};

if (settings?.continuationToken) {
pageOptions.continuationToken = settings.continuationToken;
}

return this.listEntitiesPage(tableName, pageOptions);
}
};
Expand All @@ -464,13 +474,11 @@ export class TableClient {
options?: InternalListTableEntitiesOptions
): AsyncIterableIterator<TableEntityResult<T>> {
const firstPage = await this._listEntities<T>(tableName, options);
const { nextPartitionKey, nextRowKey } = firstPage;
yield* firstPage;
if (nextRowKey && nextPartitionKey) {
if (firstPage.continuationToken) {
const optionsWithContinuation: InternalListTableEntitiesOptions = {
...options,
nextPartitionKey,
nextRowKey
continuationToken: firstPage.continuationToken
};
for await (const page of this.listEntitiesPage<T>(tableName, optionsWithContinuation)) {
yield* page;
Expand All @@ -489,12 +497,12 @@ export class TableClient {

yield result;

while (result.nextPartitionKey && result.nextRowKey) {
while (result.continuationToken) {
const optionsWithContinuation: InternalListTableEntitiesOptions = {
...updatedOptions,
nextPartitionKey: result.nextPartitionKey,
nextRowKey: result.nextRowKey
continuationToken: result.continuationToken
};

result = await this._listEntities(tableName, optionsWithContinuation);

yield result;
Expand All @@ -513,27 +521,40 @@ export class TableClient {
private async _listEntities<T extends object>(
tableName: string,
options: InternalListTableEntitiesOptions = {}
): Promise<ListEntitiesResponse<TableEntityResult<T>>> {
): Promise<TableEntityResultPage<T>> {
const { disableTypeConversion = false } = options;
const queryOptions = this.convertQueryOptions(options.queryOptions || {});
const listEntitiesOptions: TableQueryEntitiesOptionalParams = {
...options,
queryOptions
};

// If a continuation token is used, decode it and set the next row and partition key
if (options.continuationToken) {
const continuationToken = decodeContinuationToken(options.continuationToken);
listEntitiesOptions.nextRowKey = continuationToken.nextRowKey;
listEntitiesOptions.nextPartitionKey = continuationToken.nextPartitionKey;
}

const {
xMsContinuationNextPartitionKey: nextPartitionKey,
xMsContinuationNextRowKey: nextRowKey,
value
} = await this.table.queryEntities(tableName, {
...options,
queryOptions
});
} = await this.table.queryEntities(tableName, listEntitiesOptions);

const tableEntities = deserializeObjectsArray<TableEntityResult<T>>(
value ?? [],
disableTypeConversion
);

return Object.assign([...tableEntities], {
nextPartitionKey,
nextRowKey
// Encode nextPartitionKey and nextRowKey as a single continuation token and add it as a
// property to the page.
const continuationToken = encodeContinuationToken(nextPartitionKey, nextRowKey);
const page: TableEntityResultPage<T> = Object.assign([...tableEntities], {
continuationToken
});

return page;
}

/**
Expand Down Expand Up @@ -945,11 +966,7 @@ interface InternalListTableEntitiesOptions extends ListTableEntitiesOptions {
/**
* An entity query continuation token from a previous call.
*/
nextPartitionKey?: string;
/**
* An entity query continuation token from a previous call.
*/
nextRowKey?: string;
continuationToken?: string;
/**
* If true, automatic type conversion will be disabled and entity properties will
* be represented by full metadata types. For example, an Int32 value will be \{value: "123", type: "Int32"\} instead of 123.
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions sdk/tables/data-tables/src/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,16 @@ export type TableEntityResult<T> = T & {
timestamp?: string;
};

/**
* Output page type for query operations
*/
export type TableEntityResultPage<T> = Array<TableEntityResult<T>> & {
/**
* Continuation token to get the next page
*/
continuationToken?: string;
};

/**
* List entities optional parameters.
*/
Expand Down
12 changes: 8 additions & 4 deletions sdk/tables/data-tables/src/utils/bufferSerializer.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,16 @@

/**
* Encodes a byte array in base64 format.
* @param value - The Uint8Aray to encode
* @param value - The Uint8Aray or string to encode
*/
export function base64Encode(value: Uint8Array): string {
export function base64Encode(value: Uint8Array | string): string {
let str = "";
for (let i = 0; i < value.length; i++) {
str += String.fromCharCode(value[i]);
if (typeof value === "string") {
str = value;
} else {
for (let i = 0; i < value.length; i++) {
str += String.fromCharCode(value[i]);
}
}
return btoa(str);
}
Expand Down
12 changes: 8 additions & 4 deletions sdk/tables/data-tables/src/utils/bufferSerializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@

/**
* Encodes a byte array in base64 format.
* @param value - The Uint8Aray to encode
* @param value - The Uint8Aray or string to encode
*/
export function base64Encode(value: Uint8Array): string {
const bufferValue = value instanceof Buffer ? value : Buffer.from(value.buffer);
return bufferValue.toString("base64");
export function base64Encode(value: Uint8Array | string): string {
if (value instanceof Uint8Array) {
const bufferValue = value instanceof Buffer ? value : Buffer.from(value.buffer);
return bufferValue.toString("base64");
} else {
return Buffer.from(value).toString("base64");
}
}

/**
Expand Down
43 changes: 43 additions & 0 deletions sdk/tables/data-tables/src/utils/continuationToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { base64Encode, base64Decode } from "./bufferSerializer";

export interface ContinuationToken {
nextPartitionKey: string;
nextRowKey: string;
}

/**
* Encodes the nextPartitionKey and nextRowKey into a single continuation token
*/
export function encodeContinuationToken(
nextPartitionKey: string = "",
nextRowKey: string = ""
): string | undefined {
if (!nextPartitionKey && !nextRowKey) {
return undefined;
}

const continuationToken = JSON.stringify({
nextPartitionKey,
nextRowKey
});

return base64Encode(continuationToken);
}

/**
* Decodes a continuationToken into an object containing a nextPartitionKey and nextRowKey
*/
export function decodeContinuationToken(encodedToken: string): ContinuationToken {
const decodedToken = base64Decode(encodedToken);
let tokenStr = "";

for (const byte of decodedToken) {
tokenStr += String.fromCharCode(byte);
}
const continuationToken: ContinuationToken = JSON.parse(tokenStr);

return continuationToken;
}
8 changes: 2 additions & 6 deletions sdk/tables/data-tables/src/utils/internalModels.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,9 @@ export type ListTableItemsResponse = Array<TableItem> & {
*/
export type ListEntitiesResponse<T extends object> = Array<TableEntityResult<T>> & {
/**
* Contains the continuation token value for partition key.
* Contains the continuation token value for the next page.
*/
nextPartitionKey?: string;
/**
* Contains the continuation token value for row key.
*/
nextRowKey?: string;
continuationToken?: string;
};

export interface ClientParamsFromConnectionString {
Expand Down
2 changes: 1 addition & 1 deletion sdk/tables/data-tables/swagger/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

```yaml
v3: true
package-version: 12.1.3
package-version: 12.2.0
package-name: "@azure/data-tables"
title: TablesClient
description: Tables Client
Expand Down