9
9
import { Storage , StoreItems } from 'botbuilder' ;
10
10
import { Container , CosmosClient , CosmosClientOptions } from '@azure/cosmos' ;
11
11
import { CosmosDbKeyEscape } from './cosmosDbKeyEscape' ;
12
- import * as semaphore from 'semaphore ' ;
12
+ import { DoOnce } from './doOnce ' ;
13
13
14
- const _semaphore : semaphore . Semaphore = semaphore ( 1 ) ;
14
+ const _doOnce : DoOnce < Container > = new DoOnce < Container > ( ) ;
15
15
16
16
/**
17
17
* Cosmos DB Partitioned Storage Options.
@@ -41,6 +41,26 @@ export interface CosmosDbPartitionedStorageOptions {
41
41
* The throughput set when creating the Container. Defaults to 400.
42
42
*/
43
43
containerThroughput ?: number ;
44
+ /**
45
+ * The suffix to be added to every key. See cosmosDbKeyEscape.escapeKey
46
+ *
47
+ * Note: compatibilityMode must be set to 'false' to use a KeySuffix.
48
+ * When KeySuffix is used, keys will NOT be truncated but an exception will
49
+ * be thrown if the key length is longer than allowed by CosmosDb.
50
+ *
51
+ * The keySuffix must contain only valid ComosDb key characters.
52
+ * (e.g. not: '\\', '?', '/', '#', '*')
53
+ */
54
+ keySuffix ?: string ;
55
+ /**
56
+ * Early version of CosmosDb had a max key length of 255. Keys longer than
57
+ * this were truncated in cosmosDbKeyEscape.escapeKey. This remains the default
58
+ * behavior of cosmosDbPartitionedStorage, but can be overridden by setting
59
+ * compatibilityMode to false.
60
+ *
61
+ * compatibilityMode cannot be true if keySuffix is used.
62
+ */
63
+ compatibilityMode ?: boolean ;
44
64
}
45
65
46
66
/**
@@ -93,6 +113,7 @@ export class CosmosDbPartitionedStorage implements Storage {
93
113
private container : Container ;
94
114
private readonly cosmosDbStorageOptions : CosmosDbPartitionedStorageOptions ;
95
115
private client : CosmosClient ;
116
+ private compatabilityModePartitionKey : boolean = false ;
96
117
97
118
/**
98
119
* Initializes a new instance of the <see cref="CosmosDbPartitionedStorage"/> class.
@@ -106,6 +127,22 @@ export class CosmosDbPartitionedStorage implements Storage {
106
127
if ( ! cosmosDbStorageOptions . authKey ) { throw new ReferenceError ( 'authKey for CosmosDB is required.' ) ; }
107
128
if ( ! cosmosDbStorageOptions . databaseId ) { throw new ReferenceError ( 'databaseId is for CosmosDB required.' ) ; }
108
129
if ( ! cosmosDbStorageOptions . containerId ) { throw new ReferenceError ( 'containerId for CosmosDB is required.' ) ; }
130
+ // In order to support collections previously restricted to max key length of 255, we default
131
+ // compatabilityMode to 'true'. No compatibilityMode is opt-in only.
132
+ if ( typeof cosmosDbStorageOptions . compatibilityMode === "undefined" ) {
133
+ cosmosDbStorageOptions . compatibilityMode = true ;
134
+ }
135
+ if ( cosmosDbStorageOptions . keySuffix ) {
136
+ if ( cosmosDbStorageOptions . compatibilityMode ) {
137
+ throw new ReferenceError ( 'compatibilityMode cannot be true while using a keySuffix.' ) ;
138
+ }
139
+ // In order to reduce key complexity, we do not allow invalid characters in a KeySuffix
140
+ // If the keySuffix has invalid characters, the escaped key will not match
141
+ const suffixEscaped = CosmosDbKeyEscape . escapeKey ( cosmosDbStorageOptions . keySuffix ) ;
142
+ if ( cosmosDbStorageOptions . keySuffix !== suffixEscaped ) {
143
+ throw new ReferenceError ( `Cannot use invalid Row Key characters: ${ cosmosDbStorageOptions . keySuffix } in keySuffix` ) ;
144
+ }
145
+ }
109
146
110
147
this . cosmosDbStorageOptions = cosmosDbStorageOptions ;
111
148
}
@@ -120,9 +157,9 @@ export class CosmosDbPartitionedStorage implements Storage {
120
157
121
158
await Promise . all ( keys . map ( async ( k : string ) : Promise < void > => {
122
159
try {
123
- const escapedKey = CosmosDbKeyEscape . escapeKey ( k ) ;
160
+ const escapedKey = CosmosDbKeyEscape . escapeKey ( k , this . cosmosDbStorageOptions . keySuffix , this . cosmosDbStorageOptions . compatibilityMode ) ;
124
161
125
- const readItemResponse = await this . container . item ( escapedKey , escapedKey ) . read < DocumentStoreItem > ( ) ;
162
+ const readItemResponse = await this . container . item ( escapedKey , this . getPartitionKey ( escapedKey ) ) . read < DocumentStoreItem > ( ) ;
126
163
const documentStoreItem = readItemResponse . resource ;
127
164
if ( documentStoreItem ) {
128
165
storeItems [ documentStoreItem . realId ] = documentStoreItem . document ;
@@ -159,7 +196,7 @@ export class CosmosDbPartitionedStorage implements Storage {
159
196
// The ETag information is updated as an _etag attribute in the document metadata.
160
197
delete changesCopy . eTag ;
161
198
const documentChange = new DocumentStoreItem ( {
162
- id : CosmosDbKeyEscape . escapeKey ( k ) ,
199
+ id : CosmosDbKeyEscape . escapeKey ( k , this . cosmosDbStorageOptions . keySuffix , this . cosmosDbStorageOptions . compatibilityMode ) ,
163
200
realId : k ,
164
201
document : changesCopy
165
202
} ) ;
@@ -180,10 +217,10 @@ export class CosmosDbPartitionedStorage implements Storage {
180
217
await this . initialize ( ) ;
181
218
182
219
await Promise . all ( keys . map ( async ( k : string ) : Promise < void > => {
183
- const escapedKey = CosmosDbKeyEscape . escapeKey ( k ) ;
220
+ const escapedKey = CosmosDbKeyEscape . escapeKey ( k , this . cosmosDbStorageOptions . keySuffix , this . cosmosDbStorageOptions . compatibilityMode ) ;
184
221
try {
185
222
await this . container
186
- . item ( escapedKey , escapedKey )
223
+ . item ( escapedKey , this . getPartitionKey ( escapedKey ) )
187
224
. delete ( ) ;
188
225
} catch ( err ) {
189
226
// If trying to delete a document that doesn't exist, do nothing. Otherwise, throw
@@ -200,35 +237,63 @@ export class CosmosDbPartitionedStorage implements Storage {
200
237
*/
201
238
public async initialize ( ) : Promise < void > {
202
239
if ( ! this . container ) {
203
-
204
240
if ( ! this . client ) {
205
241
this . client = new CosmosClient ( {
206
242
endpoint : this . cosmosDbStorageOptions . cosmosDbEndpoint ,
207
243
key : this . cosmosDbStorageOptions . authKey ,
208
244
...this . cosmosDbStorageOptions . cosmosClientOptions ,
209
245
} ) ;
210
246
}
247
+ this . container = await _doOnce . waitFor ( async ( ) => await this . getOrCreateContainer ( ) ) ;
248
+ }
249
+ }
250
+
251
+ private async getOrCreateContainer ( ) : Promise < Container > {
252
+ let createIfNotExists = ! this . cosmosDbStorageOptions . compatibilityMode ;
253
+ let container ;
254
+ if ( this . cosmosDbStorageOptions . compatibilityMode ) {
255
+ try {
256
+ container = await this . client
257
+ . database ( this . cosmosDbStorageOptions . databaseId )
258
+ . container ( this . cosmosDbStorageOptions . containerId ) ;
259
+ const partitionKeyPath = await container . readPartitionKeyDefinition ( ) ;
260
+ const paths = partitionKeyPath . resource . paths ;
261
+ if ( paths ) {
262
+ // Containers created with CosmosDbStorage had no partition key set, so the default was '/_partitionKey'.
263
+ if ( paths . indexOf ( '/_partitionKey' ) !== - 1 ) {
264
+ this . compatabilityModePartitionKey = true ;
265
+ }
266
+ else if ( paths . indexOf ( DocumentStoreItem . partitionKeyPath ) === - 1 ) {
267
+ // We are not supporting custom Partition Key Paths.
268
+ new Error ( `Custom Partition Key Paths are not supported. ${ this . cosmosDbStorageOptions . containerId } has a custom Partition Key Path of ${ paths [ 0 ] } .` ) ;
269
+ }
270
+ } else {
271
+ this . compatabilityModePartitionKey = true ;
272
+ }
273
+ return container ;
274
+ } catch ( err ) {
275
+ createIfNotExists = true ;
276
+ }
277
+ }
211
278
212
- if ( ! this . container ) {
213
- this . container = await new Promise ( ( resolve : Function ) : void => {
214
- _semaphore . take ( async ( ) : Promise < void > => {
215
- const result = await this . client
216
- . database ( this . cosmosDbStorageOptions . databaseId )
217
- . containers . createIfNotExists ( {
218
- id : this . cosmosDbStorageOptions . containerId ,
219
- partitionKey : {
220
- paths : [ DocumentStoreItem . partitionKeyPath ]
221
- } ,
222
- throughput : this . cosmosDbStorageOptions . containerThroughput
223
- } ) ;
224
- _semaphore . leave ( ) ;
225
- resolve ( result . container ) ;
226
- } ) ;
279
+ if ( createIfNotExists ) {
280
+ const result = await this . client
281
+ . database ( this . cosmosDbStorageOptions . databaseId )
282
+ . containers . createIfNotExists ( {
283
+ id : this . cosmosDbStorageOptions . containerId ,
284
+ partitionKey : {
285
+ paths : [ DocumentStoreItem . partitionKeyPath ]
286
+ } ,
287
+ throughput : this . cosmosDbStorageOptions . containerThroughput
227
288
} ) ;
228
- }
289
+ return result . container ;
229
290
}
230
291
}
231
292
293
+ private getPartitionKey ( key ) {
294
+ return this . compatabilityModePartitionKey ? undefined : key ;
295
+ }
296
+
232
297
/**
233
298
* The Cosmos JS SDK doesn't return very descriptive errors and not all errors contain a body.
234
299
* This provides more detailed errors and err['message'] prevents ReferenceError
@@ -240,4 +305,4 @@ export class CosmosDbPartitionedStorage implements Storage {
240
305
err [ 'message' ] = `[${ prependedMessage } ] ${ err [ 'message' ] } ` ;
241
306
throw err ;
242
307
}
243
- }
308
+ }
0 commit comments