Skip to content

Commit e1c342f

Browse files
committed
UnexpectedDataAPIResponseError
1 parent 8ee74e2 commit e1c342f

File tree

8 files changed

+472
-55
lines changed

8 files changed

+472
-55
lines changed

src/client/errors.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,55 @@ export class FailedToLoadDefaultClientError extends Error {
3535
this.name = 'FailedToLoadDefaultClientError';
3636
}
3737
}
38+
39+
/**
40+
* ##### Overview
41+
*
42+
* Error thrown when the Data API response is not as expected. Should never be thrown in normal operation.
43+
*
44+
* ##### Possible causes
45+
*
46+
* 1. A `Collection` was used on a table, or vice versa.
47+
*
48+
* 2. New Data API changes occurred that are not yet supported by the client.
49+
*
50+
* 3. There is a bug in the Data API or the client.
51+
*
52+
* ##### Possible solutions
53+
*
54+
* For #1, ensure that you're using the right `Table` or `Collection` class.
55+
*
56+
* If #2 or #3, upgrade your client, and/or open an issue on the [`astra-db-ts` GitHub repository](https://github.com/datastax/astra-db-ts/issues).
57+
* - If you open an issue, please include the full error message and any relevant context.
58+
* - Please do not hesitate to do so, as there is likely a bug somewhere.
59+
*
60+
* @public
61+
*/
62+
export class UnexpectedDataAPIResponseError extends Error {
63+
/**
64+
* The response that was unexpected.
65+
*/
66+
public readonly rawDataAPIResponse?: unknown;
67+
68+
/**
69+
* Should not be instantiated by the user.
70+
*
71+
* @internal
72+
*/
73+
constructor(message: string, rawDataAPIResponse: unknown) {
74+
try {
75+
super(`${message}\n\nRaw Data API response: ${JSON.stringify(rawDataAPIResponse, null, 2)}`);
76+
} catch (_) {
77+
super(`${message}\n\nRaw Data API response: ${rawDataAPIResponse}`);
78+
}
79+
this.rawDataAPIResponse = rawDataAPIResponse;
80+
this.name = 'UnexpectedDataAPIResponseError';
81+
}
82+
83+
public static require<T>(val: T | null | undefined, message: string, rawDataAPIResponse?: unknown): T {
84+
if (val === null || val === undefined) {
85+
throw new UnexpectedDataAPIResponseError(message, rawDataAPIResponse);
86+
}
87+
return val;
88+
}
89+
}

src/documents/tables/ser-des/ser-des.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { $SerializeForTable } from '@/src/documents/tables/ser-des/constants';
2525
import BigNumber from 'bignumber.js';
2626
import { stringArraysEqual } from '@/src/lib/utils';
2727
import { RawCodec } from '@/src/lib/api/ser-des/codecs';
28+
import { UnexpectedDataAPIResponseError } from '@/src/client';
2829

2930
/**
3031
* @public
@@ -71,12 +72,17 @@ export class TableSerDes extends SerDes<TableCodecSerDesFns, TableSerCtx, TableD
7172
}
7273

7374
protected override adaptDesCtx(ctx: TableDesCtx): TableDesCtx {
74-
const status = ctx.rawDataApiResp.status!;
75+
const rawDataApiResp = ctx.rawDataApiResp;
76+
const status = UnexpectedDataAPIResponseError.require(rawDataApiResp.status, 'No `status` found in response.', rawDataApiResp);
7577

7678
if (ctx.parsingInsertedId) {
77-
ctx.tableSchema = status.primaryKeySchema;
79+
ctx.tableSchema = UnexpectedDataAPIResponseError.require(status.primaryKeySchema, 'No `status.primaryKeySchema` found in response.\n\n**Did you accidentally use a `Table` object on a collection?** If so, your document was successfully inserted, but the client cannot properly deserialize the response. Please use a `Collection` object instead.', rawDataApiResp);
80+
81+
ctx.rootObj = Object.fromEntries(Object.keys(ctx.tableSchema).map((key, i) => {
82+
return [key, ctx.rootObj[i]];
83+
}));
7884
} else {
79-
ctx.tableSchema = status.projectionSchema;
85+
ctx.tableSchema = UnexpectedDataAPIResponseError.require(status.projectionSchema, 'No `status.projectionSchema` found in response.\n\n**Did you accidentally use a `Table` object on a collection?** If so, documents may\'ve been found, but the client cannot properly deserialize the response. Please use a `Collection` object instead.', rawDataApiResp);
8086
}
8187

8288
if (ctx.keyTransformer) {
@@ -85,12 +91,6 @@ export class TableSerDes extends SerDes<TableCodecSerDesFns, TableSerCtx, TableD
8591
}));
8692
}
8793

88-
if (ctx.parsingInsertedId) {
89-
ctx.rootObj = Object.fromEntries(Object.entries(status.primaryKeySchema).map(([key], j) => {
90-
return [key, ctx.rootObj[j]];
91-
}));
92-
}
93-
9494
(<any>ctx).recurse = () => { throw new Error('Table deserialization does not recurse normally; please call any necessary codecs manually'); };
9595

9696
ctx.populateSparseData = this._cfg?.sparseData !== true;

src/lib/api/ser-des/key-transformer.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@ export class Camel2SnakeCase extends KeyTransformer {
4545
if (this._cache[snake]) {
4646
return this._cache[snake];
4747
}
48+
if (snake === '_id') {
49+
return snake;
50+
}
4851
return this._cache[snake] = snake.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
4952
}
5053
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
// noinspection DuplicatedCode
15+
16+
import { describe, it, useSuiteResources } from '@/tests/testlib';
17+
import { Camel2SnakeCase } from '@/src/lib';
18+
import assert from 'assert';
19+
import {
20+
$DeserializeForCollection,
21+
$SerializeForCollection,
22+
CollCodec,
23+
CollCodecs,
24+
CollDesCtx,
25+
CollSerCtx,
26+
uuid,
27+
UUID,
28+
} from '@/src/index';
29+
30+
describe('integration.documents.collections.ser-des.key-transformer', ({ db }) => {
31+
class Newtype implements CollCodec<typeof Newtype> {
32+
constructor(public dontChange_me: string) {}
33+
34+
[$SerializeForCollection](ctx: CollSerCtx) {
35+
return ctx.done(this.dontChange_me);
36+
}
37+
38+
static [$DeserializeForCollection](_: unknown, value: string, ctx: CollDesCtx) {
39+
return ctx.done(new Newtype(value));
40+
}
41+
}
42+
43+
describe('Camel2SnakeCase', { drop: 'colls:after' }, () => {
44+
interface SnakeCaseTest {
45+
_id: UUID,
46+
camelCase1: string,
47+
camelCaseName2: Newtype,
48+
CamelCaseName3: string[],
49+
_CamelCaseName4: Record<string, string>,
50+
camelCaseName5_: bigint,
51+
name: string[],
52+
}
53+
54+
const coll = useSuiteResources(() => ({
55+
ref: db.createCollection<SnakeCaseTest>('test_camel_snake_case_coll', {
56+
serdes: {
57+
keyTransformer: new Camel2SnakeCase(),
58+
codecs: [CollCodecs.forName('camelCaseName2', Newtype)],
59+
enableBigNumbers: true,
60+
},
61+
}),
62+
}));
63+
64+
it('should work', async () => {
65+
const id = uuid(4);
66+
67+
const { insertedId } = await coll.ref.insertOne({
68+
_id: id,
69+
camelCase1: 'dontChange_me',
70+
camelCaseName2: new Newtype('dontChange_me'),
71+
CamelCaseName3: ['dontChange_me'],
72+
_CamelCaseName4: { dontChange_me: 'dontChange_me' },
73+
camelCaseName5_: 123n,
74+
name: ['dontChange_me'],
75+
});
76+
77+
assert.deepStrictEqual(insertedId, id);
78+
79+
const result = await coll.ref.findOne({ _id: insertedId });
80+
81+
assert.deepStrictEqual(result, {
82+
_id: id,
83+
camelCase1: 'dontChange_me',
84+
camelCaseName2: new Newtype('dontChange_me'),
85+
CamelCaseName3: ['dontChange_me'],
86+
_CamelCaseName4: { dontChange_me: 'dontChange_me' },
87+
camelCaseName5_: 123,
88+
name: ['dontChange_me'],
89+
});
90+
});
91+
});
92+
});
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
// noinspection DuplicatedCode
15+
16+
import { describe, it, useSuiteResources } from '@/tests/testlib';
17+
import { Camel2SnakeCase } from '@/src/lib';
18+
import assert from 'assert';
19+
import {
20+
$DeserializeForTable,
21+
$SerializeForTable,
22+
TableCodec,
23+
TableCodecs,
24+
TableDesCtx,
25+
TableSerCtx,
26+
} from '@/src/index';
27+
28+
describe('integration.documents.tables.ser-des.key-transformer', ({ db }) => {
29+
class Newtype implements TableCodec<typeof Newtype> {
30+
constructor(public dontChange_me: string) {}
31+
32+
[$SerializeForTable](ctx: TableSerCtx) {
33+
return ctx.done(this.dontChange_me);
34+
}
35+
36+
static [$DeserializeForTable](_: unknown, value: string, ctx: TableDesCtx) {
37+
return ctx.done(new Newtype(value));
38+
}
39+
}
40+
41+
describe('Camel2SnakeCase', { drop: 'tables:after' }, () => {
42+
interface SnakeCaseTest {
43+
camelCase1: string,
44+
camelCaseName2: Newtype,
45+
CamelCaseName3: string[],
46+
_CamelCaseName4: Map<string, string>,
47+
camelCaseName5_: bigint,
48+
name: Set<string>,
49+
}
50+
51+
const table = useSuiteResources(() => ({
52+
ref: db.createTable<SnakeCaseTest>('test_camel_snake_case_table', {
53+
definition: {
54+
columns: {
55+
camel_case1: 'text',
56+
camel_case_name2: 'text',
57+
_camel_case_name3: { type: 'list', valueType: 'text' },
58+
__camel_case_name4: { type: 'map', keyType: 'text', valueType: 'text' },
59+
camel_case_name5_: 'varint',
60+
name: { type: 'set', valueType: 'text' },
61+
never_set: 'text',
62+
},
63+
primaryKey: {
64+
partitionBy: ['camel_case1', 'camel_case_name2', 'camel_case_name5_'],
65+
},
66+
},
67+
serdes: {
68+
keyTransformer: new Camel2SnakeCase(),
69+
codecs: [TableCodecs.forName('camelCaseName2', Newtype)],
70+
},
71+
}),
72+
}));
73+
74+
it('should work', async () => {
75+
const { insertedId } = await table.ref.insertOne({
76+
camelCase1: 'dontChange_me',
77+
camelCaseName2: new Newtype('dontChange_me'),
78+
CamelCaseName3: ['dontChange_me'],
79+
_CamelCaseName4: new Map([['dontChange_me', 'dontChange_me']]),
80+
camelCaseName5_: 123n,
81+
name: new Set(['dontChange_me']),
82+
});
83+
84+
assert.deepStrictEqual(insertedId, {
85+
camelCase1: 'dontChange_me',
86+
camelCaseName2: new Newtype('dontChange_me'),
87+
camelCaseName5_: 123n,
88+
});
89+
90+
const result = await table.ref.findOne(insertedId);
91+
92+
assert.deepStrictEqual(result, {
93+
camelCase1: 'dontChange_me',
94+
camelCaseName2: new Newtype('dontChange_me'),
95+
CamelCaseName3: ['dontChange_me'],
96+
_CamelCaseName4: new Map([['dontChange_me', 'dontChange_me']]),
97+
camelCaseName5_: 123n,
98+
name: new Set(['dontChange_me']),
99+
neverSet: null,
100+
});
101+
});
102+
});
103+
});
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright DataStax, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
// noinspection DuplicatedCode
15+
16+
import { describe, it } from '@/tests/testlib';
17+
import { Camel2SnakeCase } from '@/src/lib';
18+
import assert from 'assert';
19+
import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des';
20+
21+
describe('unit.documents.collections.ser-des.key-transformer', () => {
22+
describe('Camel2SnakeCase', () => {
23+
const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase() });
24+
25+
it('should serialize top-level keys to snake_case for collections', () => {
26+
const [obj] = serdes.serialize({
27+
camelCaseName1: 'dontChangeMe',
28+
CamelCaseName2: ['dontChangeMe'],
29+
_camelCaseName3: { dontChangeMe: 'dontChangeMe' },
30+
_CamelCaseName4: { dontChangeMe: 'dontChangeMe' },
31+
camelCaseName_5: 1n,
32+
car: ['dontChangeMe'],
33+
});
34+
35+
assert.deepStrictEqual(obj, {
36+
camel_case_name1: 'dontChangeMe',
37+
_camel_case_name2: ['dontChangeMe'],
38+
_camel_case_name3: { dontChangeMe: 'dontChangeMe' },
39+
__camel_case_name4: { dontChangeMe: 'dontChangeMe' },
40+
camel_case_name_5: 1n,
41+
car: ['dontChangeMe'],
42+
});
43+
});
44+
45+
it('should deserialize top-level keys to camelCase for collections', () => {
46+
const obj = serdes.deserialize({
47+
camel_case_name1: 'dontChangeMe',
48+
__camel_case_name2: { dontChangeMe: 'dontChangeMe' },
49+
_camel_case_name3: ['dontChangeMe'],
50+
camel_case_name_4: 1n,
51+
car: [['dontChangeMe']],
52+
}, {});
53+
54+
assert.deepStrictEqual(obj, {
55+
camelCaseName1: 'dontChangeMe',
56+
_CamelCaseName2: { dontChangeMe: 'dontChangeMe' },
57+
CamelCaseName3: ['dontChangeMe'],
58+
camelCaseName_4: 1n,
59+
car: [['dontChangeMe']],
60+
});
61+
});
62+
});
63+
});

0 commit comments

Comments
 (0)