From e1c342f7e5b5e30aaf2eff29e6adf230f9ff4075 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 25 Dec 2024 19:55:12 +0530 Subject: [PATCH 01/44] UnexpectedDataAPIResponseError --- src/client/errors.ts | 52 +++++++++ src/documents/tables/ser-des/ser-des.ts | 18 +-- src/lib/api/ser-des/key-transformer.ts | 3 + .../ser-des/key-transformer.test.ts | 92 ++++++++++++++++ .../tables/ser-des/key-transformer.test.ts | 103 ++++++++++++++++++ .../ser-des/key-transformer.test.ts | 63 +++++++++++ .../tables/ser-des/key-transformer.test.ts | 102 +++++++++++++++++ .../lib/api/ser-des/key-transformer.test.ts | 94 ++++++++-------- 8 files changed, 472 insertions(+), 55 deletions(-) create mode 100644 tests/integration/documents/collections/ser-des/key-transformer.test.ts create mode 100644 tests/integration/documents/tables/ser-des/key-transformer.test.ts create mode 100644 tests/unit/documents/collections/ser-des/key-transformer.test.ts create mode 100644 tests/unit/documents/tables/ser-des/key-transformer.test.ts diff --git a/src/client/errors.ts b/src/client/errors.ts index 64f4f2c1..82d2fb9a 100644 --- a/src/client/errors.ts +++ b/src/client/errors.ts @@ -35,3 +35,55 @@ export class FailedToLoadDefaultClientError extends Error { this.name = 'FailedToLoadDefaultClientError'; } } + +/** + * ##### Overview + * + * Error thrown when the Data API response is not as expected. Should never be thrown in normal operation. + * + * ##### Possible causes + * + * 1. A `Collection` was used on a table, or vice versa. + * + * 2. New Data API changes occurred that are not yet supported by the client. + * + * 3. There is a bug in the Data API or the client. + * + * ##### Possible solutions + * + * For #1, ensure that you're using the right `Table` or `Collection` class. + * + * 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). + * - If you open an issue, please include the full error message and any relevant context. + * - Please do not hesitate to do so, as there is likely a bug somewhere. + * + * @public + */ +export class UnexpectedDataAPIResponseError extends Error { + /** + * The response that was unexpected. + */ + public readonly rawDataAPIResponse?: unknown; + + /** + * Should not be instantiated by the user. + * + * @internal + */ + constructor(message: string, rawDataAPIResponse: unknown) { + try { + super(`${message}\n\nRaw Data API response: ${JSON.stringify(rawDataAPIResponse, null, 2)}`); + } catch (_) { + super(`${message}\n\nRaw Data API response: ${rawDataAPIResponse}`); + } + this.rawDataAPIResponse = rawDataAPIResponse; + this.name = 'UnexpectedDataAPIResponseError'; + } + + public static require(val: T | null | undefined, message: string, rawDataAPIResponse?: unknown): T { + if (val === null || val === undefined) { + throw new UnexpectedDataAPIResponseError(message, rawDataAPIResponse); + } + return val; + } +} diff --git a/src/documents/tables/ser-des/ser-des.ts b/src/documents/tables/ser-des/ser-des.ts index 52a81349..def69ab7 100644 --- a/src/documents/tables/ser-des/ser-des.ts +++ b/src/documents/tables/ser-des/ser-des.ts @@ -25,6 +25,7 @@ import { $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; import BigNumber from 'bignumber.js'; import { stringArraysEqual } from '@/src/lib/utils'; import { RawCodec } from '@/src/lib/api/ser-des/codecs'; +import { UnexpectedDataAPIResponseError } from '@/src/client'; /** * @public @@ -71,12 +72,17 @@ export class TableSerDes extends SerDes { + return [key, ctx.rootObj[i]]; + })); } else { - ctx.tableSchema = status.projectionSchema; + 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); } if (ctx.keyTransformer) { @@ -85,12 +91,6 @@ export class TableSerDes extends SerDes { - return [key, ctx.rootObj[j]]; - })); - } - (ctx).recurse = () => { throw new Error('Table deserialization does not recurse normally; please call any necessary codecs manually'); }; ctx.populateSparseData = this._cfg?.sparseData !== true; diff --git a/src/lib/api/ser-des/key-transformer.ts b/src/lib/api/ser-des/key-transformer.ts index 0e2e4c4e..08f51411 100644 --- a/src/lib/api/ser-des/key-transformer.ts +++ b/src/lib/api/ser-des/key-transformer.ts @@ -45,6 +45,9 @@ export class Camel2SnakeCase extends KeyTransformer { if (this._cache[snake]) { return this._cache[snake]; } + if (snake === '_id') { + return snake; + } return this._cache[snake] = snake.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } } diff --git a/tests/integration/documents/collections/ser-des/key-transformer.test.ts b/tests/integration/documents/collections/ser-des/key-transformer.test.ts new file mode 100644 index 00000000..237965eb --- /dev/null +++ b/tests/integration/documents/collections/ser-des/key-transformer.test.ts @@ -0,0 +1,92 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it, useSuiteResources } from '@/tests/testlib'; +import { Camel2SnakeCase } from '@/src/lib'; +import assert from 'assert'; +import { + $DeserializeForCollection, + $SerializeForCollection, + CollCodec, + CollCodecs, + CollDesCtx, + CollSerCtx, + uuid, + UUID, +} from '@/src/index'; + +describe('integration.documents.collections.ser-des.key-transformer', ({ db }) => { + class Newtype implements CollCodec { + constructor(public dontChange_me: string) {} + + [$SerializeForCollection](ctx: CollSerCtx) { + return ctx.done(this.dontChange_me); + } + + static [$DeserializeForCollection](_: unknown, value: string, ctx: CollDesCtx) { + return ctx.done(new Newtype(value)); + } + } + + describe('Camel2SnakeCase', { drop: 'colls:after' }, () => { + interface SnakeCaseTest { + _id: UUID, + camelCase1: string, + camelCaseName2: Newtype, + CamelCaseName3: string[], + _CamelCaseName4: Record, + camelCaseName5_: bigint, + name: string[], + } + + const coll = useSuiteResources(() => ({ + ref: db.createCollection('test_camel_snake_case_coll', { + serdes: { + keyTransformer: new Camel2SnakeCase(), + codecs: [CollCodecs.forName('camelCaseName2', Newtype)], + enableBigNumbers: true, + }, + }), + })); + + it('should work', async () => { + const id = uuid(4); + + const { insertedId } = await coll.ref.insertOne({ + _id: id, + camelCase1: 'dontChange_me', + camelCaseName2: new Newtype('dontChange_me'), + CamelCaseName3: ['dontChange_me'], + _CamelCaseName4: { dontChange_me: 'dontChange_me' }, + camelCaseName5_: 123n, + name: ['dontChange_me'], + }); + + assert.deepStrictEqual(insertedId, id); + + const result = await coll.ref.findOne({ _id: insertedId }); + + assert.deepStrictEqual(result, { + _id: id, + camelCase1: 'dontChange_me', + camelCaseName2: new Newtype('dontChange_me'), + CamelCaseName3: ['dontChange_me'], + _CamelCaseName4: { dontChange_me: 'dontChange_me' }, + camelCaseName5_: 123, + name: ['dontChange_me'], + }); + }); + }); +}); diff --git a/tests/integration/documents/tables/ser-des/key-transformer.test.ts b/tests/integration/documents/tables/ser-des/key-transformer.test.ts new file mode 100644 index 00000000..e4ca3e71 --- /dev/null +++ b/tests/integration/documents/tables/ser-des/key-transformer.test.ts @@ -0,0 +1,103 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it, useSuiteResources } from '@/tests/testlib'; +import { Camel2SnakeCase } from '@/src/lib'; +import assert from 'assert'; +import { + $DeserializeForTable, + $SerializeForTable, + TableCodec, + TableCodecs, + TableDesCtx, + TableSerCtx, +} from '@/src/index'; + +describe('integration.documents.tables.ser-des.key-transformer', ({ db }) => { + class Newtype implements TableCodec { + constructor(public dontChange_me: string) {} + + [$SerializeForTable](ctx: TableSerCtx) { + return ctx.done(this.dontChange_me); + } + + static [$DeserializeForTable](_: unknown, value: string, ctx: TableDesCtx) { + return ctx.done(new Newtype(value)); + } + } + + describe('Camel2SnakeCase', { drop: 'tables:after' }, () => { + interface SnakeCaseTest { + camelCase1: string, + camelCaseName2: Newtype, + CamelCaseName3: string[], + _CamelCaseName4: Map, + camelCaseName5_: bigint, + name: Set, + } + + const table = useSuiteResources(() => ({ + ref: db.createTable('test_camel_snake_case_table', { + definition: { + columns: { + camel_case1: 'text', + camel_case_name2: 'text', + _camel_case_name3: { type: 'list', valueType: 'text' }, + __camel_case_name4: { type: 'map', keyType: 'text', valueType: 'text' }, + camel_case_name5_: 'varint', + name: { type: 'set', valueType: 'text' }, + never_set: 'text', + }, + primaryKey: { + partitionBy: ['camel_case1', 'camel_case_name2', 'camel_case_name5_'], + }, + }, + serdes: { + keyTransformer: new Camel2SnakeCase(), + codecs: [TableCodecs.forName('camelCaseName2', Newtype)], + }, + }), + })); + + it('should work', async () => { + const { insertedId } = await table.ref.insertOne({ + camelCase1: 'dontChange_me', + camelCaseName2: new Newtype('dontChange_me'), + CamelCaseName3: ['dontChange_me'], + _CamelCaseName4: new Map([['dontChange_me', 'dontChange_me']]), + camelCaseName5_: 123n, + name: new Set(['dontChange_me']), + }); + + assert.deepStrictEqual(insertedId, { + camelCase1: 'dontChange_me', + camelCaseName2: new Newtype('dontChange_me'), + camelCaseName5_: 123n, + }); + + const result = await table.ref.findOne(insertedId); + + assert.deepStrictEqual(result, { + camelCase1: 'dontChange_me', + camelCaseName2: new Newtype('dontChange_me'), + CamelCaseName3: ['dontChange_me'], + _CamelCaseName4: new Map([['dontChange_me', 'dontChange_me']]), + camelCaseName5_: 123n, + name: new Set(['dontChange_me']), + neverSet: null, + }); + }); + }); +}); diff --git a/tests/unit/documents/collections/ser-des/key-transformer.test.ts b/tests/unit/documents/collections/ser-des/key-transformer.test.ts new file mode 100644 index 00000000..ab332cd6 --- /dev/null +++ b/tests/unit/documents/collections/ser-des/key-transformer.test.ts @@ -0,0 +1,63 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it } from '@/tests/testlib'; +import { Camel2SnakeCase } from '@/src/lib'; +import assert from 'assert'; +import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; + +describe('unit.documents.collections.ser-des.key-transformer', () => { + describe('Camel2SnakeCase', () => { + const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase() }); + + it('should serialize top-level keys to snake_case for collections', () => { + const [obj] = serdes.serialize({ + camelCaseName1: 'dontChangeMe', + CamelCaseName2: ['dontChangeMe'], + _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, + _CamelCaseName4: { dontChangeMe: 'dontChangeMe' }, + camelCaseName_5: 1n, + car: ['dontChangeMe'], + }); + + assert.deepStrictEqual(obj, { + camel_case_name1: 'dontChangeMe', + _camel_case_name2: ['dontChangeMe'], + _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, + __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, + camel_case_name_5: 1n, + car: ['dontChangeMe'], + }); + }); + + it('should deserialize top-level keys to camelCase for collections', () => { + const obj = serdes.deserialize({ + camel_case_name1: 'dontChangeMe', + __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, + _camel_case_name3: ['dontChangeMe'], + camel_case_name_4: 1n, + car: [['dontChangeMe']], + }, {}); + + assert.deepStrictEqual(obj, { + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: { dontChangeMe: 'dontChangeMe' }, + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, + car: [['dontChangeMe']], + }); + }); + }); +}); diff --git a/tests/unit/documents/tables/ser-des/key-transformer.test.ts b/tests/unit/documents/tables/ser-des/key-transformer.test.ts new file mode 100644 index 00000000..5609435d --- /dev/null +++ b/tests/unit/documents/tables/ser-des/key-transformer.test.ts @@ -0,0 +1,102 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it } from '@/tests/testlib'; +import { Camel2SnakeCase } from '@/src/lib'; +import assert from 'assert'; +import { TableSerDes } from '@/src/documents/tables/ser-des/ser-des'; + +describe('unit.documents.tables.ser-des.key-transformer', () => { + describe('Camel2SnakeCase', () => { + const serdes = new TableSerDes({ keyTransformer: new Camel2SnakeCase() }); + + it('should serialize top-level keys to snake_case for tables', () => { + const [obj, bigNumPresent] = serdes.serialize({ + camelCaseName1: 'dontChangeMe', + CamelCaseName2: ['dontChangeMe'], + _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, + _CamelCaseName4: new Map([['dontChangeMe', 'dontChangeMe']]), + camelCaseName_5: 1n, + car: new Set(['dontChangeMe']), + }); + + assert.deepStrictEqual(obj, { + camel_case_name1: 'dontChangeMe', + _camel_case_name2: ['dontChangeMe'], + _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, + __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, + camel_case_name_5: 1n, + car: ['dontChangeMe'], + }); + assert.strictEqual(bigNumPresent, true); + }); + + it('should deserialize top-level keys to camelCase for tables', () => { + const obj = serdes.deserialize({ + camel_case_name1: 'dontChangeMe', + __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, + _camel_case_name3: ['dontChangeMe'], + camel_case_name_4: 1n, + car: ['dontChangeMe'], + }, { + status: { + projectionSchema: { + camel_case_name1: { type: 'text' }, + __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, + _camel_case_name3: { type: 'list', valueType: 'text' }, + camel_case_name_4: { type: 'varint' }, + car: { type: 'set', valueType: 'text' }, + }, + }, + }); + + assert.deepStrictEqual(obj, { + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, + car: new Set(['dontChangeMe']), + }); + }); + + it('should deserialize top-level keys to camelCase for tables for primary keys', () => { + const obj = serdes.deserialize([ + 'dontChangeMe', + { dontChangeMe: 'dontChangeMe' }, + ['dontChangeMe'], + 1n, + ['dontChangeMe'], + ], { + status: { + primaryKeySchema: { + camel_case_name1: { type: 'text' }, + __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, + _camel_case_name3: { type: 'list', valueType: 'text' }, + camel_case_name_4: { type: 'varint' }, + car: { type: 'set', valueType: 'text' }, + }, + }, + }, true); + + assert.deepStrictEqual(obj, { + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, + car: new Set(['dontChangeMe']), + }); + }); + }); +}); diff --git a/tests/unit/lib/api/ser-des/key-transformer.test.ts b/tests/unit/lib/api/ser-des/key-transformer.test.ts index 15869c77..11bfe93a 100644 --- a/tests/unit/lib/api/ser-des/key-transformer.test.ts +++ b/tests/unit/lib/api/ser-des/key-transformer.test.ts @@ -46,19 +46,20 @@ describe('unit.lib.api.ser-des.key-transformer', () => { it('should serialize top-level keys to snake_case for tables', () => { const [obj, bigNumPresent] = tableSerdes.serialize({ - camelCaseName: 'dontChangeMe', - CamelCaseName: ['dontChangeMe'], - _camelCaseName: { dontChangeMe: 'dontChangeMe' }, - _CamelCaseName: new Map([['dontChangeMe', 'dontChangeMe']]), - camelCaseName_: 1n, + camelCaseName1: 'dontChangeMe', + CamelCaseName2: ['dontChangeMe'], + _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, + _CamelCaseName4: new Map([['dontChangeMe', 'dontChangeMe']]), + camelCaseName_5: 1n, car: new Set(['dontChangeMe']), }); assert.deepStrictEqual(obj, { - camel_case_name: 'dontChangeMe', - __camel_case_name: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name: ['dontChangeMe'], - camel_case_name_: 1n, + camel_case_name1: 'dontChangeMe', + _camel_case_name2: ['dontChangeMe'], + _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, + __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, + camel_case_name_5: 1n, car: ['dontChangeMe'], }); assert.strictEqual(bigNumPresent, true); @@ -66,28 +67,28 @@ describe('unit.lib.api.ser-des.key-transformer', () => { it('should deserialize top-level keys to camelCase for tables', () => { const obj = tableSerdes.deserialize({ - camel_case_name: 'dontChangeMe', - __camel_case_name: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name: ['dontChangeMe'], - camel_case_name_: 1n, + camel_case_name1: 'dontChangeMe', + __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, + _camel_case_name3: ['dontChangeMe'], + camel_case_name_4: 1n, car: ['dontChangeMe'], }, { status: { projectionSchema: { - camel_case_name: { type: 'text' }, - __camel_case_name: { type: 'map', keyType: 'text', valueType: 'text' }, - _camel_case_name: { type: 'list', valueType: 'text' }, - camel_case_name_: { type: 'varint' }, + camel_case_name1: { type: 'text' }, + __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, + _camel_case_name3: { type: 'list', valueType: 'text' }, + camel_case_name_4: { type: 'varint' }, car: { type: 'set', valueType: 'text' }, }, }, }); assert.deepStrictEqual(obj, { - camelCaseName: 'dontChangeMe', - CamelCaseName: ['dontChangeMe'], - _CamelCaseName: new Map([['dontChangeMe', 'dontChangeMe']]), - camelCaseName_: 1n, + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, car: new Set(['dontChangeMe']), }); }); @@ -102,57 +103,58 @@ describe('unit.lib.api.ser-des.key-transformer', () => { ], { status: { primaryKeySchema: { - camel_case_name: { type: 'text' }, - __camel_case_name: { type: 'map', keyType: 'text', valueType: 'text' }, - _camel_case_name: { type: 'list', valueType: 'text' }, - camel_case_name_: { type: 'varint' }, + camel_case_name1: { type: 'text' }, + __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, + _camel_case_name3: { type: 'list', valueType: 'text' }, + camel_case_name_4: { type: 'varint' }, car: { type: 'set', valueType: 'text' }, }, }, }, true); assert.deepStrictEqual(obj, { - camelCaseName: 'dontChangeMe', - CamelCaseName: ['dontChangeMe'], - _CamelCaseName: new Map([['dontChangeMe', 'dontChangeMe']]), - camelCaseName_: 1n, + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, car: new Set(['dontChangeMe']), }); }); it('should serialize top-level keys to snake_case for collections', () => { const [obj] = collSerdes.serialize({ - camelCaseName: 'dontChangeMe', - CamelCaseName: ['dontChangeMe'], - _camelCaseName: { dontChangeMe: 'dontChangeMe' }, - _CamelCaseName: { dontChangeMe: 'dontChangeMe' }, - camelCaseName_: 1n, + camelCaseName1: 'dontChangeMe', + CamelCaseName2: ['dontChangeMe'], + _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, + _CamelCaseName4: { dontChangeMe: 'dontChangeMe' }, + camelCaseName_5: 1n, car: ['dontChangeMe'], }); assert.deepStrictEqual(obj, { - camel_case_name: 'dontChangeMe', - __camel_case_name: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name: ['dontChangeMe'], - camel_case_name_: 1n, + camel_case_name1: 'dontChangeMe', + _camel_case_name2: ['dontChangeMe'], + _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, + __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, + camel_case_name_5: 1n, car: ['dontChangeMe'], }); }); it('should deserialize top-level keys to camelCase for collections', () => { const obj = collSerdes.deserialize({ - camel_case_name: 'dontChangeMe', - __camel_case_name: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name: ['dontChangeMe'], - camel_case_name_: 1n, + camel_case_name1: 'dontChangeMe', + __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, + _camel_case_name3: ['dontChangeMe'], + camel_case_name_4: 1n, car: [['dontChangeMe']], }, {}); assert.deepStrictEqual(obj, { - camelCaseName: 'dontChangeMe', - CamelCaseName: ['dontChangeMe'], - _CamelCaseName: { dontChangeMe: 'dontChangeMe' }, - camelCaseName_: 1n, + camelCaseName1: 'dontChangeMe', + _CamelCaseName2: { dontChangeMe: 'dontChangeMe' }, + CamelCaseName3: ['dontChangeMe'], + camelCaseName_4: 1n, car: [['dontChangeMe']], }); }); From 8c7872b57b3c75bf00641547bfc36e75e4b7e9b0 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 25 Dec 2024 20:33:42 +0530 Subject: [PATCH 02/44] made column definition optional in tableserdesfns --- src/documents/tables/ser-des/codecs.ts | 10 +++++----- src/documents/tables/ser-des/ser-des.ts | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index 63611e04..8134cfa5 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -29,7 +29,7 @@ import { CodecOpts, RawCodec } from '@/src/lib/api/ser-des/codecs'; */ export interface TableCodecSerDesFns { serialize: SerDesFn, - deserialize: (key: string | undefined, val: any, ctx: TableDesCtx, definition: SomeDoc) => ReturnType>, + deserialize: (key: string | undefined, val: any, ctx: TableDesCtx, definition?: SomeDoc) => ReturnType>, } /** @@ -94,8 +94,8 @@ export class TableCodecs { for (let i = 0, n = entries.length; i < n; i++) { const [key, value] = entries[i]; - const keyParser = ctx.codecs.type[def.keyType]; - const valueParser = ctx.codecs.type[def.valueType]; + const keyParser = ctx.codecs.type[def!.keyType]; + const valueParser = ctx.codecs.type[def!.valueType]; entries[i] = [ keyParser ? keyParser.deserialize(undefined, key, ctx, def)[1] : key, @@ -109,7 +109,7 @@ export class TableCodecs { list: TableCodecs.forType('list', { deserialize(_, list, ctx, def) { for (let i = 0, n = list.length; i < n; i++) { - const elemParser = ctx.codecs.type[def.valueType]; + const elemParser = ctx.codecs.type[def!.valueType]; list[i] = elemParser ? elemParser.deserialize(undefined, list[i], ctx, def)[1] : list[i]; } return ctx.done(list); @@ -122,7 +122,7 @@ export class TableCodecs { }, deserialize(_, list, ctx, def) { for (let i = 0, n = list.length; i < n; i++) { - const elemParser = ctx.codecs.type[def.valueType]; + const elemParser = ctx.codecs.type[def!.valueType]; list[i] = elemParser ? elemParser.deserialize(undefined, list[i], ctx, def)[1] : list[i]; } return ctx.done(new Set(list)); diff --git a/src/documents/tables/ser-des/ser-des.ts b/src/documents/tables/ser-des/ser-des.ts index def69ab7..84dc92be 100644 --- a/src/documents/tables/ser-des/ser-des.ts +++ b/src/documents/tables/ser-des/ser-des.ts @@ -195,15 +195,15 @@ const DefaultTableSerDesCfg = { ctx.populateSparseData = false; } - const type = resolveType(column); - if (key in codecs.name) { if ((resp = codecs.name[key].deserialize(key, ctx.rootObj[key], ctx, column))[0] !== CONTINUE) { return resp; } } - if (type in codecs.type) { + const type = resolveType(column); + + if (type && type in codecs.type) { if ((resp = codecs.type[type].deserialize(key, ctx.rootObj[key], ctx, column))[0] !== CONTINUE) { return resp; } From 4e31bb003eec7fe38424387d6e32677f04e50967 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 25 Dec 2024 20:37:10 +0530 Subject: [PATCH 03/44] fixes serdes of datapitimestamp for collections --- src/documents/datatypes/dates.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/documents/datatypes/dates.ts b/src/documents/datatypes/dates.ts index b5386f14..2b75072d 100644 --- a/src/documents/datatypes/dates.ts +++ b/src/documents/datatypes/dates.ts @@ -412,14 +412,14 @@ export class DataAPITimestamp implements CollCodec, Tab * Implementation of `$SerializeForTable` for {@link TableCodec} */ public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.toString()); + return ctx.done(this.#timestamp); }; /** * Implementation of `$SerializeForCollection` for {@link TableCodec} */ public [$SerializeForCollection](ctx: CollSerCtx) { - return ctx.done({ $date: this.toString() }); + return ctx.done({ $date: new Date(this.#timestamp).valueOf() }); }; /** @@ -433,7 +433,7 @@ export class DataAPITimestamp implements CollCodec, Tab * Implementation of `$DeserializeForCollection` for {@link TableCodec} */ public static [$DeserializeForCollection](_: string, value: any, ctx: CollDesCtx) { - return ctx.done(new DataAPITimestamp(value.$date)); + return ctx.done(new DataAPITimestamp(new Date(value.$date).toISOString())); } /** From 2f0d42de3c59499ee925fad203b6c8141c579f70 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 25 Dec 2024 20:46:19 +0530 Subject: [PATCH 04/44] Love coding? here's the secret reason why --- src/documents/commands/helpers/insertion.ts | 9 +++++---- tests/integration/documents/tables/indexes.ts | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/documents/commands/helpers/insertion.ts b/src/documents/commands/helpers/insertion.ts index 6dcfcb45..2d26360f 100644 --- a/src/documents/commands/helpers/insertion.ts +++ b/src/documents/commands/helpers/insertion.ts @@ -22,6 +22,7 @@ import { } from '@/src/documents/errors'; import { GenericInsertManyDocumentResponse, SomeDoc, SomeId } from '@/src/documents'; import { TimeoutManager } from '@/src/lib/api/timeouts'; +import { RawDataAPIResponse } from '@/src/lib'; /** * @internal @@ -34,7 +35,7 @@ export const insertManyOrdered = async ( timeoutManager: TimeoutManager, err: new (descs: DataAPIDetailedErrorDescriptor[]) => DataAPIResponseError, ): Promise => { - const insertedIds: any = []; + const insertedIds: ID[] = []; for (let i = 0, n = documents.length; i < n; i += chunkSize) { const slice = documents.slice(i, i + chunkSize); @@ -69,11 +70,11 @@ export const insertManyUnordered = async ( timeoutManager: TimeoutManager, err: new (descs: DataAPIDetailedErrorDescriptor[]) => DataAPIResponseError, ): Promise => { - const insertedIds: any = []; + const insertedIds: ID[] = []; let masterIndex = 0; - const failCommands = [] as Record[]; - const failRaw = [] as Record[]; + const failCommands = [] as Record[]; + const failRaw = [] as RawDataAPIResponse[]; const docResps = [] as GenericInsertManyDocumentResponse[]; const promises = Array.from({ length: concurrency }, async () => { diff --git a/tests/integration/documents/tables/indexes.ts b/tests/integration/documents/tables/indexes.ts index a7b7aa76..7adb3422 100644 --- a/tests/integration/documents/tables/indexes.ts +++ b/tests/integration/documents/tables/indexes.ts @@ -55,7 +55,7 @@ parallel('integration.documents.tables.indexes', { drop: 'colls:after' }, ({ db definition: { columns: { pkey: 'text', - vec: { type: 'vector' }, + vec: { type: 'vector', dimension: 3 }, }, primaryKey: 'pkey', }, From bafd8b1f4e84482457e004665549479c60569119 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 1 Jan 2025 20:03:56 +0530 Subject: [PATCH 05/44] move test script documentation to test script itself. --- .env.example | 5 -- scripts/check.sh | 5 ++ scripts/test.sh | 146 +++++++++++++++++++++++++++++++++-------------- 3 files changed, 107 insertions(+), 49 deletions(-) create mode 100644 scripts/check.sh diff --git a/.env.example b/.env.example index 6b76fd7c..d8001e3d 100644 --- a/.env.example +++ b/.env.example @@ -10,8 +10,3 @@ CLIENT_DB_TOKEN= # Backend for the Data API (astra | dse | hcd | cassandra | other). Defaults to 'astra'. # CLIENT_DB_ENVIRONMENT= - -# Uncomment to enable running all (or specific) types of test by default -# CLIENT_RUN_VECTORIZE_TESTS=1 -# CLIENT_RUN_LONG_TESTS=1 -# CLIENT_RUN_ADMIN_TESTS=1 diff --git a/scripts/check.sh b/scripts/check.sh new file mode 100644 index 00000000..05ea51b0 --- /dev/null +++ b/scripts/check.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh + +npx tsc --noEmit --skipLibCheck + +npm run lint -- --no-warn-ignored diff --git a/scripts/test.sh b/scripts/test.sh index b304e5f7..3d7f011d 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh +set -e + # Properly sources the .env file to bring env variables into scope if [ -f .env ]; then eval "$(tr -d '\r' < .env)" @@ -8,10 +10,6 @@ fi # Define necessary commands test_cmd="ts-mocha --paths -p tsconfig.json --recursive tests/prelude.test.ts tests/unit tests/integration tests/postlude.test.ts --extension .test.ts -t 0 --reporter tests/errors-reporter.cjs" -run_lint_cmd="npm run lint -- --no-warn-ignored" - -run_tsc_cmd="npx tsc --noEmit --skipLibCheck" - # Counter to make sure test type isn't set multiple times test_type_set=0 @@ -37,14 +35,6 @@ while [ $# -gt 0 ]; do test_type="coverage" bail_early=1 ;; - "-lint") - test_type="code" - run_linting=1 - ;; - "-tc") - test_type="code" - run_typechecking=1 - ;; "-f" | "-F") [ "$1" = "-F" ] && filter_type='i' || filter_type='n' shift @@ -83,7 +73,7 @@ while [ $# -gt 0 ]; do "-l" | "-logging") logging="!isGlobal" ;; - "-L" | "-logging-pred") + "-L" | "-logging-with-pred") shift logging="$1" ;; @@ -94,35 +84,113 @@ while [ $# -gt 0 ]; do local=1 ;; *) - echo "Invalid flag $1" - echo "" + if [ "$1" != "--help" ] && [ "$1" != "-help" ] && [ "$1" != "-h" ]; then + echo "Invalid flag $1" + echo + fi echo "Usage:" - echo "scripts/test.sh [-all | -light | -coverage] [-for] [-f/F ]+ [-g/G ]+ [-w/W ] [-b | -bail] [-R | -no-report] [-c ] [-e ] [-local] ([-l | -logging] | [-L | -logging-pred]) [-P | -skip-prelude]" - echo "or" - echo "scripts/test.sh [-lint] [-tc]" + echo + echo "$0" + echo " (1) [-all | -light | -coverage]" + echo " (2) [-f/F ]+" + echo " (3) [-g/G ]+" + echo " (4) [-for]" + echo " (5) [-w/W ]" + echo " (6) [-b | -bail]" + echo " (7) [-R | -no-report]" + echo " (8) [-c ]" + echo " (9) [-e ]" + echo " (10) [-local]" + echo " (11) [(-l | -logging) | (-L | -logging-with-pred)]" + echo " (12) [-P | -skip-prelude]" + echo + echo " $(tput setaf 4)(1)$(tput setaf 9) $(tput bold)Either run all tests, light tests, or coverage tests (defaults to 'all')$(tput sgr0)" + echo + echo " '-all' runs all tests; '-light' runs tests without the LONG, ADMIN, or VECTORIZE tags; '-coverage' runs all tests with nyc coverage, and with '-bail' enabled." + echo + echo " $(tput setaf 4)(2), (3)$(tput setaf 9) $(tput bold)Filter tests by substring or regex match$(tput sgr0)" + echo + echo " A custom, more powerful implementation of Mocha's -f & -g flags." + echo + echo " Requires a test's name, or any of its parent suites' names, to either contain the given text, or match the given regex (depending on the filter used)." + echo + echo " No '-i' flag present; use '-F' or '-R' to invert any individual filter." + echo + echo " May use each multiple times, intermixing the two types of filters." + echo + echo " $(tput setaf 4)(4)$(tput setaf 9) $(tput bold)Use 'or' instead of 'and' for filter flags$(tput sgr0)" + echo + echo " By default, when you run something like '$0 -f unit. -f http-client', it will run tests that match both 'unit.' and 'table.'. Use the '-for' flag to instead run all tests that match *either* filter." + echo + echo " $(tput setaf 4)(5)$(tput setaf 9) $(tput bold)Filter which vectorize tests to run (defaults to '\$limit-per-model:1')$(tput sgr0)" + echo + echo " There's a special filtering system just for vectorize tests, called the \"vectorize whitelist\", of which there are two different types." + echo + echo " $(tput smul)* Regex filtering:$(tput rmul)" + echo + echo " Every vectorize test is given a test name representing every branch it took to become that specific test. It is of the following format:" + echo + echo " > 'providerName@modelName@authType@dimension'" + echo " > where dimension := 'specified' | 'default' | " + echo " > where authType := 'header' | 'providerKey' | 'none'" + echo + echo " Again, the regex only needs to match part of each test's name to succeed, so use '^$' as necessary." + echo + echo " $(tput smul) Filter operators:$(tput rmul)" + echo + echo " The vectorize test suite also defines some custom \"filter operators\" to provide filtering that can't be done through basic regex." + echo + echo " They come of the format '-w \$:':" + echo + echo " > '\$limit:' - This is a limit over the total number of vectorize tests, only running up to the specified amount" + echo " > '\$provider-limit:' - This limits the amount of vectorize tests that can be run per provider" + echo " > '\$model-limit:' - Akin to the above, but limits per model." + echo + echo " $(tput setaf 4)(6)$(tput setaf 9) $(tput bold)Bail early on first test failure$(tput sgr0)" + echo + echo " Simply sets the bail flag, as it does in Mocha. Forces the test script to exit after a single test failure." + echo + echo " $(tput setaf 4)(7)$(tput setaf 9) $(tput bold)Disable test error reporting to \`./etc/test-reports$(tput sgr0)\`" + echo + echo " By default, the test suite logs the complete error objects of any that may've been thrown during your tests to the \`./etc/test-reports\` directory for greatest debuggability. However, this can be disabled for a single test run using this flag." + echo + echo " $(tput setaf 4)(8)$(tput setaf 9) $(tput bold)Set the http client to use for tests (defaults to 'default:http2')$(tput sgr0)" + echo + echo " By default, tests are run w/ \`fetch-h2\` using HTTP/2, but you can specify a specific HTTP client, which is one of 'default:http1', 'default:http2', or 'fetch'." + echo + echo " $(tput setaf 4)(9)$(tput setaf 9) $(tput bold)Set the database used for tests (defaults to 'astra')$(tput sgr0)" + echo + echo " By default, the test suite assumes you're running on Astra, but you can specify the Data API environment through this flag, which should be one of 'dse', 'hcd', 'cassandra', or 'other'." + echo + echo " You can also provide 'astra', but it wouldn't really do anything. But I'm not your boss or your mother; you can make your own big-boy/girl/other decisions, if you really want to." + echo + echo " Not necessary if '-local' is set." + echo + echo " $(tput setaf 4)(10)$(tput setaf 9) $(tput bold)Use local stargate for tests$(tput sgr0)" + echo + echo " If you're running the tests on a local Stargate instance, you can use this flag to set the CLIENT_DB_URL to 'http://localhost:8080' and the CLIENT_DB_TOKEN to 'cassandra:cassandra' without needing to modify your .env file." + echo + echo " Note that you'll still need to run stargate yourself. See \`scripts/start-stargate-4-tests.sh\`." + echo + echo " $(tput setaf 4)(11)$(tput setaf 9) $(tput bold)Enable verbose logging for tests$(tput sgr0)" + echo + echo " (\`-l\` is equal to \`-L '!isGlobal'\`)" + echo + echo " Documentation TODO." + echo + echo " $(tput setaf 4)(12)$(tput setaf 9) $(tput bold)Skip tests setup to save time (prelude.test.ts)$(tput sgr0)" + echo + echo " By default, the test script will run a \"prelude\" script that sets up the database for the tests. This can be skipped to save some time, using this flag, if the DB is already setup (enough), and you just want to run some tests really quickly." + echo exit ;; esac shift done -# Ensure the flags are compatible with each other -if [ "$test_type" = "code" ] && { [ -n "$bail_early" ] || [ -n "$filter" ] || [ -n "$filter_combinator" ] || [ -n "$whitelist" ] || [ -n "$no_err_report" ] || [ -n "$http_client" ] || [ -n "$environment" ] || [ -n "$logging" ] || [ -n "$skip_prelude" ]; }; then - echo "Can't use a filter, bail, whitelist flags when typechecking/linting" - exit 1 -fi - -if [ "$test_type_set" -gt 0 ] && { [ -n "$run_linting" ] || [ -n "$run_typecheking" ]; }; then - echo "Conflicting flags; -all/-light/-coverage and -tc/-lint present at the same time" - exit 1 -fi - # Build the actual command to run case "$test_type" in - "") - cmd_to_run="npx $test_cmd" - ;; - "all") + "" | "all") export CLIENT_RUN_VECTORIZE_TESTS=1 CLIENT_RUN_LONG_TESTS=1 CLIENT_RUN_ADMIN_TESTS=1 cmd_to_run="npx $test_cmd" ;; @@ -134,17 +202,6 @@ case "$test_type" in export CLIENT_RUN_VECTORIZE_TESTS=1 CLIENT_RUN_LONG_TESTS=1 CLIENT_RUN_ADMIN_TESTS=1 cmd_to_run="npx nyc $test_cmd" ;; - "code") - if [ -n "$run_linting" ]; then - cmd_to_run="$run_lint_cmd; $cmd_to_run" - fi - - if [ -n "$run_typechecking" ]; then - cmd_to_run="$run_tsc_cmd; $cmd_to_run" - fi - - cmd_to_run="${cmd_to_run%; }" - ;; esac if [ -n "$filter" ]; then @@ -175,6 +232,7 @@ fi if [ -n "$local" ]; then export USING_LOCAL_STARGATE=1 fi + if [ -n "$logging" ]; then export LOGGING_PRED="$logging" fi From 3fc4f337750668b0c892661d56590b07c54c3099 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 1 Jan 2025 20:16:42 +0530 Subject: [PATCH 06/44] separate scripts/check.sh file for typechecking/linting --- scripts/check-licensing.sh | 4 --- scripts/check.sh | 55 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 53 insertions(+), 6 deletions(-) delete mode 100755 scripts/check-licensing.sh mode change 100644 => 100755 scripts/check.sh diff --git a/scripts/check-licensing.sh b/scripts/check-licensing.sh deleted file mode 100755 index f4cb8a9e..00000000 --- a/scripts/check-licensing.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh - -# Lists out all the files which don't contain the necessary license notice -find tests/ src/ -type f -exec grep -L "^// Copyright DataStax, Inc." {} + diff --git a/scripts/check.sh b/scripts/check.sh old mode 100644 new mode 100755 index 05ea51b0..bc513f85 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -1,5 +1,56 @@ #!/usr/bin/env sh -npx tsc --noEmit --skipLibCheck +while [ $# -gt 0 ]; do + case "$1" in + "tc") + check_types="$check_types tc" + ;; + "lint") + check_types="$check_types lint" + ;; + "licensing") + check_types="$check_types licensing" + ;; + *) + if [ "$1" != "--help" ] && [ "$1" != "-help" ] && [ "$1" != "-h" ]; then + echo "Invalid flag $1" + echo + fi + echo "Usage:" + echo + echo "$0 [tc] [lint] [licensing]" + echo + echo "* tc: runs the type-checker" + echo "* lint: checks for linting errors" + echo "* licensing: checks for missing licensing headers" + echo + echo "Defaults to running all checks if no specific checks are specified." + exit + esac + shift +done -npm run lint -- --no-warn-ignored +if [ -z "$check_types" ]; then + check_types="tc lint licensing" +fi + +for check_type in $check_types; do + case $check_type in + "tc") + echo "Running type-checker..." + npx tsc --noEmit --skipLibCheck || exit 10 + ;; + "lint") + echo "Running linter..." + npm run lint -- --no-warn-ignored || exit 20 + ;; + "licensing") + echo "Checking for missing licensing headers..." + find tests/ src/ -type f -exec grep -L "^// Copyright DataStax, Inc." {} + || exit 30 + ;; + "*") + echo "Invalid check type '$check_type'" + exit 1 + ;; + esac +done From 6aaf4573b99152f3cb3ea6544c660cbcc71cfd87 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 1 Jan 2025 23:27:37 +0530 Subject: [PATCH 07/44] som eoverhaul of unit tests for datatypes --- src/lib/api/fetch/fetch-h2.ts | 2 +- src/lib/api/ser-des/key-transformer.ts | 5 +- src/lib/api/ser-des/ser-des.ts | 2 +- .../administration/astra-admin.test.ts | 26 +++ .../administration/db-admin.test.ts | 2 +- .../documents/collections/misc.test.ts | 43 +---- .../documents/collections/options.test.ts | 5 + .../documents/tables/definition.test.ts | 7 +- .../integration/documents/tables/misc.test.ts | 38 ++++ .../ser-des/key-transformer.test.ts | 4 + tests/unit/documents/datatypes/blob.test.ts | 121 ++++--------- tests/unit/documents/datatypes/inet.test.ts | 69 ++++++++ tests/unit/documents/datatypes/vector.test.ts | 85 +++------ .../lib/api/ser-des/key-transformer.test.ts | 162 ------------------ tests/unit/lib/logging/logger.test.ts | 6 +- 15 files changed, 223 insertions(+), 354 deletions(-) create mode 100644 tests/integration/administration/astra-admin.test.ts create mode 100644 tests/integration/documents/tables/misc.test.ts create mode 100644 tests/unit/documents/datatypes/inet.test.ts delete mode 100644 tests/unit/lib/api/ser-des/key-transformer.test.ts diff --git a/src/lib/api/fetch/fetch-h2.ts b/src/lib/api/fetch/fetch-h2.ts index 6f566fb8..d489f7c5 100644 --- a/src/lib/api/fetch/fetch-h2.ts +++ b/src/lib/api/fetch/fetch-h2.ts @@ -32,7 +32,7 @@ export class FetchH2 implements Fetcher { constructor(options: DefaultHttpClientOptions | undefined, preferHttp2: boolean) { try { // Complicated expression to stop Next.js and such from tracing require and trying to load the fetch-h2 client - const [indirectRequire] = [require].map(x => isNaN(Math.random()) ? null! : x); + const [indirectRequire] = [require].map(x => x); const fetchH2 = validateFetchH2(options?.fetchH2) ?? indirectRequire('fetch-h2') as typeof import('fetch-h2'); diff --git a/src/lib/api/ser-des/key-transformer.ts b/src/lib/api/ser-des/key-transformer.ts index 08f51411..13fad941 100644 --- a/src/lib/api/ser-des/key-transformer.ts +++ b/src/lib/api/ser-des/key-transformer.ts @@ -26,7 +26,7 @@ export abstract class KeyTransformer { * @public */ export class Camel2SnakeCase extends KeyTransformer { - private _cache: Record = {}; + private _cache: Record = { _id: '_id' }; public override serializeKey(camel: string, ctx: BaseSerCtx): string { if (ctx.path.length > 1 || !camel) { @@ -45,9 +45,6 @@ export class Camel2SnakeCase extends KeyTransformer { if (this._cache[snake]) { return this._cache[snake]; } - if (snake === '_id') { - return snake; - } return this._cache[snake] = snake.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); } } diff --git a/src/lib/api/ser-des/ser-des.ts b/src/lib/api/ser-des/ser-des.ts index 60046eea..5e11e953 100644 --- a/src/lib/api/ser-des/ser-des.ts +++ b/src/lib/api/ser-des/ser-des.ts @@ -53,7 +53,7 @@ export abstract class SerDes(obj: S): [S, boolean] { - if (obj === null || obj === undefined) { + if (!obj) { return [obj, false]; } const ctx = this.adaptSerCtx(this._mkCtx(obj, { mutatingInPlace: this._cfg.mutateInPlace === true })); diff --git a/tests/integration/administration/astra-admin.test.ts b/tests/integration/administration/astra-admin.test.ts new file mode 100644 index 00000000..4ddf418f --- /dev/null +++ b/tests/integration/administration/astra-admin.test.ts @@ -0,0 +1,26 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it } from '@/tests/testlib'; +import { DataAPIClient, DevOpsAPIResponseError } from '@/src//index'; +import assert from 'assert'; + +describe('integration.administration.astra-admin', () => { + it('should not stop you from creating an AstraAdmin without a token', async () => { + const client = new DataAPIClient(); + const admin = client.admin(); + await assert.rejects(() => admin.listDatabases(), DevOpsAPIResponseError); + }); +}); diff --git a/tests/integration/administration/db-admin.test.ts b/tests/integration/administration/db-admin.test.ts index de5b0218..e221a0ef 100644 --- a/tests/integration/administration/db-admin.test.ts +++ b/tests/integration/administration/db-admin.test.ts @@ -53,7 +53,7 @@ describe('integration.administration.db-admin', ({ client, dbAdmin }) => { assert.strictEqual(succeeded, 5); assert.strictEqual(warnings, 0); }); - + it('should findEmbeddingProviders', async () => { const { embeddingProviders } = await dbAdmin.findEmbeddingProviders(); assert.ok(typeof embeddingProviders === 'object'); diff --git a/tests/integration/documents/collections/misc.test.ts b/tests/integration/documents/collections/misc.test.ts index 6fb3bc7a..a74f484f 100644 --- a/tests/integration/documents/collections/misc.test.ts +++ b/tests/integration/documents/collections/misc.test.ts @@ -12,50 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DataAPIResponseError, DataAPITimeoutError } from '@/src/documents'; -import { DEFAULT_COLLECTION_NAME, initTestObjects, it, parallel } from '@/tests/testlib'; +import { DataAPIResponseError } from '@/src/documents'; +import { it, parallel } from '@/tests/testlib'; import assert from 'assert'; parallel('integration.documents.collections.misc', ({ db }) => { - it('times out on http2', async () => { - const { db: newDb } = initTestObjects({ httpClient: 'default:http2' }); - - try { - await newDb.collection(DEFAULT_COLLECTION_NAME).insertOne({ username: 'test' }, { timeout: 10 }); - } catch (e) { - assert.ok(e instanceof DataAPITimeoutError); - assert.strictEqual(e.message, 'Command timed out after 10ms (The timeout provided via `{ timeout: }` timed out)'); - } - }); - - it('times out on http1', async () => { - const { db: newDb } = initTestObjects({ httpClient: 'default:http1' }); - - try { - await newDb.collection(DEFAULT_COLLECTION_NAME).insertOne({ username: 'test' }, { timeout: 11 }); - } catch (e) { - assert.ok(e instanceof DataAPITimeoutError); - assert.strictEqual(e.message, 'Command timed out after 11ms (The timeout provided via `{ timeout: }` timed out)'); - } - }); - it('DataAPIResponseError is thrown when doing data api operation on non-existent collections', async () => { const collection = db.collection('non_existent_collection'); - - try { - await collection.insertOne({ username: 'test' }); - } catch (e) { - assert.ok(e instanceof DataAPIResponseError); - } - }); - - it('error is thrown when doing .options() on non-existent collections', async () => { - const collection = db.collection('non_existent_collection'); - - try { - await collection.options(); - } catch (e) { - assert.ok(e instanceof Error); - } + await assert.rejects(() => collection.insertOne({ username: 'test' }), DataAPIResponseError); }); }); diff --git a/tests/integration/documents/collections/options.test.ts b/tests/integration/documents/collections/options.test.ts index b6d5b018..880c7d9b 100644 --- a/tests/integration/documents/collections/options.test.ts +++ b/tests/integration/documents/collections/options.test.ts @@ -28,4 +28,9 @@ parallel('integration.documents.collections.options', { drop: 'colls:after' }, ( assert.deepStrictEqual(res, {}); await db.dropCollection('test_db_collection_empty_opts'); }); + + it('error is thrown when doing .options() on non-existent collections', async () => { + const collection = db.collection('non_existent_collection'); + await assert.rejects(() => collection.options(), Error); + }); }); diff --git a/tests/integration/documents/tables/definition.test.ts b/tests/integration/documents/tables/definition.test.ts index b3eac37f..d35b2010 100644 --- a/tests/integration/documents/tables/definition.test.ts +++ b/tests/integration/documents/tables/definition.test.ts @@ -15,10 +15,15 @@ import { it, parallel } from '@/tests/testlib'; import assert from 'assert'; -parallel('integration.documents.tables.definition', ({ table }) => { +parallel('integration.documents.tables.definition', ({ db, table }) => { it('lists its own definition', async () => { const res = await table.definition(); assert.ok(res.columns); assert.ok(res.primaryKey); }); + + it('error is thrown when doing .definition() on non-existent tables', async () => { + const table = db.table('non_existent_collection'); + await assert.rejects(() => table.definition(), Error); + }); }); diff --git a/tests/integration/documents/tables/misc.test.ts b/tests/integration/documents/tables/misc.test.ts new file mode 100644 index 00000000..dedbe9fb --- /dev/null +++ b/tests/integration/documents/tables/misc.test.ts @@ -0,0 +1,38 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { DataAPIResponseError, timestamp } from '@/src/documents'; +import { it, parallel } from '@/tests/testlib'; +import assert from 'assert'; + +parallel('integration.documents.tables.misc', ({ db, table }) => { + it('DataAPIResponseError is thrown when doing data api operation on non-existent tables', async () => { + const table = db.table('non_existent_collection'); + await assert.rejects(() => table.insertOne({ text: 'test' }), DataAPIResponseError); + }); + + it('handles timestamps properly', async () => { + const ts1 = timestamp(); + await table.insertOne({ text: '123', int: 0, timestamp: ts1 }); + const row1 = await table.findOne({ text: '123' }); + assert.deepStrictEqual(row1?.timestamp, ts1); + + const ts2 = timestamp(new Date('2021-01-01')); + await table.insertOne({ text: '123', int: 0, timestamp: ts2 }); + const row2 = await table.findOne({ text: '123' }); + assert.deepStrictEqual(row2?.timestamp, ts2); + + console.log(row1.timestamp, row2.timestamp); + }); +}); diff --git a/tests/unit/documents/collections/ser-des/key-transformer.test.ts b/tests/unit/documents/collections/ser-des/key-transformer.test.ts index ab332cd6..bea57a58 100644 --- a/tests/unit/documents/collections/ser-des/key-transformer.test.ts +++ b/tests/unit/documents/collections/ser-des/key-transformer.test.ts @@ -24,6 +24,7 @@ describe('unit.documents.collections.ser-des.key-transformer', () => { it('should serialize top-level keys to snake_case for collections', () => { const [obj] = serdes.serialize({ + _id: 'dontChangeMe', camelCaseName1: 'dontChangeMe', CamelCaseName2: ['dontChangeMe'], _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, @@ -33,6 +34,7 @@ describe('unit.documents.collections.ser-des.key-transformer', () => { }); assert.deepStrictEqual(obj, { + _id: 'dontChangeMe', camel_case_name1: 'dontChangeMe', _camel_case_name2: ['dontChangeMe'], _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, @@ -44,6 +46,7 @@ describe('unit.documents.collections.ser-des.key-transformer', () => { it('should deserialize top-level keys to camelCase for collections', () => { const obj = serdes.deserialize({ + _id: 'dontChangeMe', camel_case_name1: 'dontChangeMe', __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, _camel_case_name3: ['dontChangeMe'], @@ -52,6 +55,7 @@ describe('unit.documents.collections.ser-des.key-transformer', () => { }, {}); assert.deepStrictEqual(obj, { + _id: 'dontChangeMe', camelCaseName1: 'dontChangeMe', _CamelCaseName2: { dontChangeMe: 'dontChangeMe' }, CamelCaseName3: ['dontChangeMe'], diff --git a/tests/unit/documents/datatypes/blob.test.ts b/tests/unit/documents/datatypes/blob.test.ts index b2f39c84..bf78bdfb 100644 --- a/tests/unit/documents/datatypes/blob.test.ts +++ b/tests/unit/documents/datatypes/blob.test.ts @@ -17,35 +17,32 @@ import assert from 'assert'; import { blob, DataAPIBlob } from '@/src/documents'; import { describe, it } from '@/tests/testlib'; +const BUFF = Buffer.from([0x0, 0x1, 0x2]); +const ARR_BUFF = new Uint8Array(BUFF).buffer; +const BINARY = { $binary: 'AAEC' }; + describe('unit.documents.datatypes.blob', () => { describe('construction', () => { - it('should properly construct a DataAPIBlob', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]).buffer; - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.raw(), buff); - }); - - it('should properly construct a DataAPIBlob using the shorthand', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]).buffer; - const blb = blob(buff); - assert.strictEqual(blb.raw(), buff); - }); + it('should create blobs of each type', () => { + const blobLikes = [BUFF, ARR_BUFF, BINARY]; - it('should properly construct a DataAPIBlob using ArrayBuffer', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]).buffer; - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.raw(), buff); + for (const blobLike of blobLikes) { + const full = new DataAPIBlob(blobLike); + const shorthand = blob(blobLike); + assert.deepStrictEqual(full.raw(), blobLike); + assert.deepStrictEqual(shorthand.raw(), blobLike); + assert.deepStrictEqual(full.raw(), shorthand.raw()); + } }); - it('should properly construct a DataAPIBlob using $binary', () => { - const blb = new DataAPIBlob({ $binary: 'AAEC' }); - assert.strictEqual(blb.asBase64(), 'AAEC'); - }); + it('should create a blob from another blob', () => { + const blobLikes = [BUFF, ARR_BUFF, BINARY]; - it('should properly construct a DataAPIBlob using another DataAPIBlob', () => { - const blb = new DataAPIBlob({ $binary: 'AAEC' }); - const blb2 = new DataAPIBlob(blb); - assert.strictEqual(blb2.asBase64(), 'AAEC'); + for (const blobLike of blobLikes) { + const original = new DataAPIBlob(blobLike); + const copy = blob(original); + assert.deepStrictEqual(original.raw(), copy.raw()); + } }); it('should error on invalid type', () => { @@ -58,71 +55,29 @@ describe('unit.documents.datatypes.blob', () => { }); }); - describe('byteLength', () => { - it('should return the byte length of the buffer from Buffer', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]); - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.byteLength, 3); - }); - - it('should return the byte length of the buffer from ArrayBuffer', () => { - const buff = new ArrayBuffer(3); - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.byteLength, 3); - }); + it('should get the byte length of all types', () => { + const blobs = [blob(BUFF), blob(ARR_BUFF), blob(BINARY), blob(blob(BUFF))]; - it('should return the byte length of the buffer from $binary', () => { - const blb = new DataAPIBlob({ $binary: 'AAEC' }); + for (const blb of blobs) { assert.strictEqual(blb.byteLength, 3); - }); + } }); - describe('coercion', () => { - it('should return Buffer as ArrayBuffer', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]); - const blb = new DataAPIBlob(buff); - assert.ok(blb.asArrayBuffer() instanceof ArrayBuffer); - assert.deepStrictEqual(new Uint8Array(blb.asArrayBuffer()), new Uint8Array([0x0, 0x1, 0x2])); - }); + it('should convert between all types', () => { + const blobs = [blob(BUFF), blob(ARR_BUFF), blob(BINARY), blob(blob(BUFF))]; - it('should return $binary as ArrayBuffer', () => { - const blb = new DataAPIBlob({ $binary: 'AAEC' }); - assert.ok(blb.asArrayBuffer() instanceof ArrayBuffer); - assert.deepStrictEqual(new Uint8Array(blb.asArrayBuffer()), new Uint8Array([0x0, 0x1, 0x2])); - }); - - it('should return ArrayBuffer as Buffer', () => { - const buff = new Uint8Array([0x0, 0x1, 0x2]).buffer; - const blb = new DataAPIBlob(buff); - assert.ok(blb.asBuffer() instanceof Buffer); - assert.deepStrictEqual(blb.asBuffer(), Buffer.from([0x0, 0x1, 0x2])); - }); - - it('should return $binary as Buffer', () => { - const blb = new DataAPIBlob({ $binary: 'AAEC' }); - assert.ok(blb.asBuffer() instanceof Buffer); - assert.deepStrictEqual(blb.asBuffer(), Buffer.from([0x0, 0x1, 0x2])); - }); - - it('should throw on Buffer not available', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]); - const blb = new DataAPIBlob(buff); - const origBuffer = globalThis.Buffer; - delete (globalThis).Buffer; - assert.throws(() => blb.asBuffer()); - globalThis.Buffer = origBuffer; - }); - - it('should return Buffer as $binary', () => { - const buff = Buffer.from([0x0, 0x1, 0x2]); - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.asBase64(), 'AAEC'); - }); + for (const blb of blobs) { + assert.strictEqual(blb.asBase64(), BINARY.$binary); + assert.deepStrictEqual(blb.asBuffer(), BUFF); + assert.deepStrictEqual(blb.asArrayBuffer(), ARR_BUFF); + } + }); - it('should return ArrayBuffer as $binary', () => { - const buff = new Uint8Array([0x0, 0x1, 0x2]).buffer; - const blb = new DataAPIBlob(buff); - assert.strictEqual(blb.asBase64(), 'AAEC'); - }); + it('should throw on Buffer not available', () => { + const blb = new DataAPIBlob(BUFF); + const origBuffer = globalThis.Buffer; + delete (globalThis).Buffer; + assert.throws(() => blb.asBuffer()); + globalThis.Buffer = origBuffer; }); }); diff --git a/tests/unit/documents/datatypes/inet.test.ts b/tests/unit/documents/datatypes/inet.test.ts new file mode 100644 index 00000000..d9b748d2 --- /dev/null +++ b/tests/unit/documents/datatypes/inet.test.ts @@ -0,0 +1,69 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import assert from 'assert'; +import { InetAddress } from '@/src/documents'; +import { describe, it } from '@/tests/testlib'; + +const IPV4 = '127.0.0.1'; +const IPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; + +describe('unit.documents.datatypes.inet', () => { + describe('construction', () => { + it('should properly construct an IPv4 address', () => { + const explicit = new InetAddress(IPV4, 4); + assert.strictEqual(explicit.toString(), IPV4); + assert.strictEqual(explicit.version, 4); + + const implicit = new InetAddress(IPV4); + assert.strictEqual(implicit.toString(), IPV4); + assert.strictEqual(implicit.version, 4); + }); + + it('should properly construct an IPv6 address', () => { + const explicit = new InetAddress(IPV6, 6); + assert.strictEqual(explicit.toString(), IPV6); + assert.strictEqual(explicit.version, 6); + + const implicit = new InetAddress(IPV6); + assert.strictEqual(implicit.toString(), IPV6); + assert.strictEqual(implicit.version, 6); + }); + }); + + describe('validation', () => { + it('should error on invalid IPv4', () => { + assert.throws(() => new InetAddress(IPV6, 4)); + }); + + it('should error on invalid IPv6', () => { + assert.throws(() => new InetAddress(IPV4, 6)); + }); + + it('should error on invalid IP', () => { + assert.throws(() => new InetAddress('i like dogs')); + }); + + it('should error on invalid type', () => { + assert.throws(() => new InetAddress({} as any), Error); + }); + + it('should allow force creation of invalid values', () => { + assert.strictEqual(new InetAddress('abc', 4, false).version, 4); + assert.strictEqual(new InetAddress(IPV6, 4, false).version, 4); + assert.throws(() => new InetAddress({} as any, 4, false).version, TypeError); + }); + }); +}); diff --git a/tests/unit/documents/datatypes/vector.test.ts b/tests/unit/documents/datatypes/vector.test.ts index 6be5e340..49b0f41a 100644 --- a/tests/unit/documents/datatypes/vector.test.ts +++ b/tests/unit/documents/datatypes/vector.test.ts @@ -23,30 +23,26 @@ const BINARY = { $binary: 'PwAAAD8AAAA/AAAA' }; describe('unit.documents.datatypes.vector', () => { describe('construction', () => { - it('should properly construct a DataAPIVector', () => { - const vec = new DataAPIVector(ARR); - assert.strictEqual(vec.raw(), ARR); - }); - - it('should properly construct a DataAPIVector using the shorthand', () => { - const vec = vector(ARR); - assert.strictEqual(vec.raw(), ARR); - }); + it('should create vectors of each type', () => { + const vectorLikes = [ARR, F32ARR, BINARY]; - it('should properly construct a DataAPIVector using a Float32Array', () => { - const vec = new DataAPIVector(F32ARR); - assert.strictEqual(vec.raw(), F32ARR); + for (const vectorLike of vectorLikes) { + const full = new DataAPIVector(vectorLike); + const shorthand = vector(vectorLike); + assert.deepStrictEqual(full.raw(), vectorLike); + assert.deepStrictEqual(shorthand.raw(), vectorLike); + assert.deepStrictEqual(full.raw(), shorthand.raw()); + } }); - it('should properly construct a DataAPIVector using $binary', () => { - const vec = new DataAPIVector(BINARY); - assert.strictEqual(vec.raw(), BINARY); - }); + it('should create a vector from another vector', () => { + const vectorLikes = [ARR, F32ARR, BINARY]; - it('should properly construct a DataAPIVector using another DataAPIVector', () => { - const vec = new DataAPIVector(BINARY); - const vec2 = new DataAPIVector(vec); - assert.strictEqual(vec2.raw(), BINARY); + for (const vectorLike of vectorLikes) { + const original = new DataAPIVector(vectorLike); + const copy = vector(original); + assert.deepStrictEqual(original.raw(), copy.raw()); + } }); it('should error on invalid type', () => { @@ -59,52 +55,21 @@ describe('unit.documents.datatypes.vector', () => { }); }); - describe('length', () => { - it('should return the length of the vector from number[]', () => { - const vec = new DataAPIVector(ARR); - assert.strictEqual(vec.length, 3); - }); + it('should get length of all types', () => { + const vectors = [vector(ARR), vector(F32ARR), vector(BINARY), vector(vector(ARR))]; - it('should return the length of the vector from Float32Array', () => { - const vec = new DataAPIVector(F32ARR); + for (const vec of vectors) { assert.strictEqual(vec.length, 3); - }); - - it('should return the length of the vector from $binary', () => { - const vec = new DataAPIVector(BINARY); - assert.strictEqual(vec.length, 3); - }); + } }); - describe('coercion', () => { - it('should return number[] as $binary', () => { - const vec = new DataAPIVector(ARR); - assert.deepStrictEqual(vec.asBase64(), BINARY.$binary); - }); - - it('should return Float32Array as $binary', () => { - const vec = new DataAPIVector(F32ARR); - assert.deepStrictEqual(vec.asBase64(), BINARY.$binary); - }); - - it('should return Float32Array as number[]', () => { - const vec = new DataAPIVector(F32ARR); - assert.deepStrictEqual(vec.asArray(), ARR); - }); + it('should convert between all types', () => { + const vectors = [vector(ARR), vector(F32ARR), vector(BINARY), vector(vector(ARR))]; - it('should return $binary as number[]', () => { - const vec = new DataAPIVector(BINARY); + for (const vec of vectors) { + assert.strictEqual(vec.asBase64(), BINARY.$binary); assert.deepStrictEqual(vec.asArray(), ARR); - }); - - it('should return $binary as Float32Array', () => { - const vec = new DataAPIVector(BINARY); assert.deepStrictEqual(vec.asFloat32Array(), F32ARR); - }); - - it('should return number[] as Float32Array', () => { - const vec = new DataAPIVector(ARR); - assert.deepStrictEqual(vec.asFloat32Array(), F32ARR); - }); + } }); }); diff --git a/tests/unit/lib/api/ser-des/key-transformer.test.ts b/tests/unit/lib/api/ser-des/key-transformer.test.ts deleted file mode 100644 index 11bfe93a..00000000 --- a/tests/unit/lib/api/ser-des/key-transformer.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// noinspection DuplicatedCode - -import { describe, it } from '@/tests/testlib'; -import { Camel2SnakeCase } from '@/src/lib'; -import assert from 'assert'; -import { TableSerDes } from '@/src/documents/tables/ser-des/ser-des'; -import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; - -describe('unit.lib.api.ser-des.key-transformer', () => { - const ctx = { path: [''] } as any; - - describe('Camel2SnakeCase', () => { - const snakeCase = new Camel2SnakeCase(); - const tableSerdes = new TableSerDes({ keyTransformer: snakeCase }); - const collSerdes = new CollectionSerDes({ keyTransformer: snakeCase }); - - it('should serialize strings to snake_case', () => { - assert.strictEqual(snakeCase.serializeKey('camelCaseName', ctx), 'camel_case_name'); - assert.strictEqual(snakeCase.serializeKey('CamelCaseName', ctx), '_camel_case_name'); - assert.strictEqual(snakeCase.serializeKey('_camelCaseName', ctx), '_camel_case_name'); - assert.strictEqual(snakeCase.serializeKey('_CamelCaseName', ctx), '__camel_case_name'); - assert.strictEqual(snakeCase.serializeKey('camelCaseName_', ctx), 'camel_case_name_'); - assert.strictEqual(snakeCase.serializeKey('car', ctx), 'car'); - }); - - it('should deserialize strings to camelCase', () => { - assert.strictEqual(snakeCase.deserializeKey('snake_case_name', ctx), 'snakeCaseName'); - assert.strictEqual(snakeCase.deserializeKey('_snake_case_name', ctx), 'SnakeCaseName'); - assert.strictEqual(snakeCase.deserializeKey('__snake_case_name', ctx), '_SnakeCaseName'); - assert.strictEqual(snakeCase.deserializeKey('snake_case_name_', ctx), 'snakeCaseName_'); - assert.strictEqual(snakeCase.deserializeKey('car', ctx), 'car'); - }); - - it('should serialize top-level keys to snake_case for tables', () => { - const [obj, bigNumPresent] = tableSerdes.serialize({ - camelCaseName1: 'dontChangeMe', - CamelCaseName2: ['dontChangeMe'], - _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, - _CamelCaseName4: new Map([['dontChangeMe', 'dontChangeMe']]), - camelCaseName_5: 1n, - car: new Set(['dontChangeMe']), - }); - - assert.deepStrictEqual(obj, { - camel_case_name1: 'dontChangeMe', - _camel_case_name2: ['dontChangeMe'], - _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, - __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, - camel_case_name_5: 1n, - car: ['dontChangeMe'], - }); - assert.strictEqual(bigNumPresent, true); - }); - - it('should deserialize top-level keys to camelCase for tables', () => { - const obj = tableSerdes.deserialize({ - camel_case_name1: 'dontChangeMe', - __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name3: ['dontChangeMe'], - camel_case_name_4: 1n, - car: ['dontChangeMe'], - }, { - status: { - projectionSchema: { - camel_case_name1: { type: 'text' }, - __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, - _camel_case_name3: { type: 'list', valueType: 'text' }, - camel_case_name_4: { type: 'varint' }, - car: { type: 'set', valueType: 'text' }, - }, - }, - }); - - assert.deepStrictEqual(obj, { - camelCaseName1: 'dontChangeMe', - _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), - CamelCaseName3: ['dontChangeMe'], - camelCaseName_4: 1n, - car: new Set(['dontChangeMe']), - }); - }); - - it('should deserialize top-level keys to camelCase for tables for primary keys', () => { - const obj = tableSerdes.deserialize([ - 'dontChangeMe', - { dontChangeMe: 'dontChangeMe' }, - ['dontChangeMe'], - 1n, - ['dontChangeMe'], - ], { - status: { - primaryKeySchema: { - camel_case_name1: { type: 'text' }, - __camel_case_name2: { type: 'map', keyType: 'text', valueType: 'text' }, - _camel_case_name3: { type: 'list', valueType: 'text' }, - camel_case_name_4: { type: 'varint' }, - car: { type: 'set', valueType: 'text' }, - }, - }, - }, true); - - assert.deepStrictEqual(obj, { - camelCaseName1: 'dontChangeMe', - _CamelCaseName2: new Map([['dontChangeMe', 'dontChangeMe']]), - CamelCaseName3: ['dontChangeMe'], - camelCaseName_4: 1n, - car: new Set(['dontChangeMe']), - }); - }); - - it('should serialize top-level keys to snake_case for collections', () => { - const [obj] = collSerdes.serialize({ - camelCaseName1: 'dontChangeMe', - CamelCaseName2: ['dontChangeMe'], - _camelCaseName3: { dontChangeMe: 'dontChangeMe' }, - _CamelCaseName4: { dontChangeMe: 'dontChangeMe' }, - camelCaseName_5: 1n, - car: ['dontChangeMe'], - }); - - assert.deepStrictEqual(obj, { - camel_case_name1: 'dontChangeMe', - _camel_case_name2: ['dontChangeMe'], - _camel_case_name3: { dontChangeMe: 'dontChangeMe' }, - __camel_case_name4: { dontChangeMe: 'dontChangeMe' }, - camel_case_name_5: 1n, - car: ['dontChangeMe'], - }); - }); - - it('should deserialize top-level keys to camelCase for collections', () => { - const obj = collSerdes.deserialize({ - camel_case_name1: 'dontChangeMe', - __camel_case_name2: { dontChangeMe: 'dontChangeMe' }, - _camel_case_name3: ['dontChangeMe'], - camel_case_name_4: 1n, - car: [['dontChangeMe']], - }, {}); - - assert.deepStrictEqual(obj, { - camelCaseName1: 'dontChangeMe', - _CamelCaseName2: { dontChangeMe: 'dontChangeMe' }, - CamelCaseName3: ['dontChangeMe'], - camelCaseName_4: 1n, - car: [['dontChangeMe']], - }); - }); - }); -}); diff --git a/tests/unit/lib/logging/logger.test.ts b/tests/unit/lib/logging/logger.test.ts index 4da8aa9b..5916cb24 100644 --- a/tests/unit/lib/logging/logger.test.ts +++ b/tests/unit/lib/logging/logger.test.ts @@ -78,9 +78,11 @@ describe('unit.lib.logging.logger', () => { it('should handle absolute madness', () => { const config: DataAPILoggingConfig = [ 'commandSucceeded', - { events: 'all', emits: 'stdout' }, + { events: 'all', emits: ['stderr'] }, + { events: ['all'], emits: 'stdout' }, 'commandFailed', { events: ['commandFailed', 'commandSucceeded'], emits: [] }, + { events: ['commandSucceeded', 'commandFailed'], emits: 'stdout' }, { events: ['commandSucceeded', 'commandFailed'], emits: ['event', 'stderr'] }, 'all', 'commandSucceeded', @@ -88,9 +90,11 @@ describe('unit.lib.logging.logger', () => { const expected: NormalizedLoggingConfig[] = [ 3 as any, { events: ['commandSucceeded'], emits: ['event'] }, + { events: LoggingEventsWithoutAll, emits: ['stderr'] }, { events: LoggingEventsWithoutAll, emits: ['stdout'] }, { events: ['commandFailed'], emits: ['event', 'stderr'] }, { events: ['commandFailed', 'commandSucceeded'], emits: [] }, + { events: ['commandSucceeded', 'commandFailed'], emits: ['stdout'] }, { events: ['commandSucceeded', 'commandFailed'], emits: ['event', 'stderr'] }, ...DataAPILoggingDefaults, { events: ['commandSucceeded'], emits: ['event'] }, From 18b866d9eed9f0cf1e075d463287aa7f258efb8b Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 10:09:08 +0530 Subject: [PATCH 08/44] forJSEnv utility --- src/documents/datatypes/blob.ts | 83 +++++++---------- src/documents/datatypes/vector.ts | 91 ++++++++++--------- .../userpass-token-providers.ts | 25 ++--- src/lib/utils.ts | 17 ++++ tests/unit/common/token-providers.test.ts | 24 +---- tests/unit/lib/logging/logger.test.ts | 8 ++ 6 files changed, 123 insertions(+), 125 deletions(-) diff --git a/src/documents/datatypes/blob.ts b/src/documents/datatypes/blob.ts index 3ea25206..3b8d7a6b 100644 --- a/src/documents/datatypes/blob.ts +++ b/src/documents/datatypes/blob.ts @@ -15,6 +15,7 @@ import { $CustomInspect } from '@/src/lib/constants'; import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; +import { forJSEnv } from '@/src/lib/utils'; /** * Represents any type that can be converted into a {@link DataAPIBlob} @@ -183,54 +184,40 @@ export class DataAPIBlob implements TableCodec { } } -const base64ToArrayBuffer = - (typeof Buffer !== 'undefined') - ? nodeBase64ToArrayBuffer : - (typeof window !== 'undefined') - ? webBase64ToArrayBuffer - : panicBase64ToBuffer; - -function webBase64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = window.atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - return bytes.buffer; -} - -function nodeBase64ToArrayBuffer(base64: string): ArrayBuffer { - return bufferToArrayBuffer(Buffer.from(base64, 'base64')); -} - -function panicBase64ToBuffer(): ArrayBuffer { - throw new Error("Cannot convert base64 to Buffer/ArrayBuffer in this environment; please do so manually"); -} - -const arrayBufferToBase64 = - (typeof Buffer !== 'undefined') - ? nodeArrayBufferToBase64 : - (typeof window !== 'undefined') - ? webArrayBufferToBase64 - : panicBufferToBase64; - -function webArrayBufferToBase64(buffer: ArrayBuffer): string { - let binary = ''; - const bytes = new Uint8Array(buffer); - for (let i = 0; i < bytes.length; i++) { - binary += String.fromCharCode(bytes[i]); - } - return window.btoa(binary); -} - -function nodeArrayBufferToBase64(buffer: ArrayBuffer): string { - return Buffer.from(buffer).toString('base64'); -} - -function panicBufferToBase64(): string { - throw new Error("Cannot convert Buffer/ArrayBuffer to base64 in this environment; please do so manually"); -} +const base64ToArrayBuffer = forJSEnv<(base64: string) => ArrayBuffer>({ + server: (base64) => { + return bufferToArrayBuffer(Buffer.from(base64, 'base64')); + }, + browser: (base64) => { + const binaryString = window.atob(base64); + const len = binaryString.length; + const bytes = new Uint8Array(len); + for (let i = 0; i < len; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + return bytes.buffer; + }, + unknown: () => { + throw new Error("Cannot convert base64 to Buffer/ArrayBuffer in this environment; please do so manually"); + }, +}); + +const arrayBufferToBase64 = forJSEnv<(buffer: ArrayBuffer) => string>({ + server: (buffer) => { + return Buffer.from(buffer).toString('base64'); + }, + browser: (buffer) => { + let binary = ''; + const bytes = new Uint8Array(buffer); + for (let i = 0; i < bytes.length; i++) { + binary += String.fromCharCode(bytes[i]); + } + return window.btoa(binary); + }, + unknown: () => { + throw new Error("Cannot convert Buffer/ArrayBuffer to base64 in this environment; please do so manually"); + }, +}); function bufferToArrayBuffer(b: Buffer): ArrayBuffer { return b.buffer.slice(b.byteOffset, b.byteOffset + b.byteLength); diff --git a/src/documents/datatypes/vector.ts b/src/documents/datatypes/vector.ts index 31138948..dd921d2f 100644 --- a/src/documents/datatypes/vector.ts +++ b/src/documents/datatypes/vector.ts @@ -22,6 +22,7 @@ import { } from '@/src/documents'; import { $DeserializeForCollection, $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; +import { forJSEnv } from '@/src/lib/utils'; /** * Represents any type that can be converted into a {@link DataAPIVector} @@ -57,7 +58,7 @@ export class DataAPIVector implements CollCodec, TableCode public [$SerializeForTable](ctx: TableSerCtx) { return ctx.done(serialize(this.#vector)); }; - + /** * Implementation of `$SerializeForCollection` for {@link TableCodec} */ @@ -110,7 +111,7 @@ export class DataAPIVector implements CollCodec, TableCode */ public get length(): number { if ('$binary' in this.#vector) { - return ~~((this.#vector.$binary.replace(/=+$/, "").length * 3) / 4 / 4); + return ~~((this.#vector.$binary.replace(/=+$/, '').length * 3) / 4 / 4); } return this.#vector.length; } @@ -123,7 +124,7 @@ export class DataAPIVector implements CollCodec, TableCode public raw(): Exclude { return this.#vector; } - + /** * Returns the vector as a `number[]`, converting between types if necessary. * @@ -149,7 +150,7 @@ export class DataAPIVector implements CollCodec, TableCode /** * Returns the vector as a `Float32Array`, converting between types if necessary. - * + * * @returns The vector as a `Float32Array` */ public asFloat32Array(): Float32Array { @@ -158,7 +159,7 @@ export class DataAPIVector implements CollCodec, TableCode } if ('$binary' in this.#vector) { - const deserialized = deserializeToF32Array(this.#vector.$binary); + const deserialized = deserializeToF32Array(this.#vector.$binary); if (!deserialized) { throw new Error('Could not to deserialize vector from base64 => Float32Array; unknown environment. Please manually deserialize the binary from `vector.getAsBase64()`'); @@ -172,7 +173,7 @@ export class DataAPIVector implements CollCodec, TableCode /** * Returns the vector as a base64 string, converting between types if necessary. - * + * * @returns The vector as a base64 string */ public asBase64(): string { @@ -180,9 +181,9 @@ export class DataAPIVector implements CollCodec, TableCode if (!('$binary' in serialized)) { if (Array.isArray(this.#vector)) { - throw new Error('Could not serialize vector from number[] => base64; unknown environment. Please manually serialize the binary from `vector.getRaw()`/`vector.getAsArray()`'); + throw new Error('Could not convert vector from number[] => base64; unknown environment. Please manually serialize the binary from `vector.raw()`/`vector.getAsArray()`'); } else { - throw new Error('Could not serialize vector from Float32Array => base64; unknown environment. Please manually serialize the binary from `vector.getRaw()`/`vector.getAsFloat32Array()`'); + throw new Error('Could not convert vector from Float32Array => base64; unknown environment. Please manually serialize the binary from `vector.raw()`/`vector.getAsFloat32Array()`'); } } @@ -201,7 +202,7 @@ export class DataAPIVector implements CollCodec, TableCode return `DataAPIVector<${this.length}>(typeof raw=${type}, preview=${partial})`; } - + /** * Determines whether the given value is a vector-like value (i.e. it's {@link DataAPIVectorLike}. * @@ -214,12 +215,15 @@ export class DataAPIVector implements CollCodec, TableCode } } -const serialize = (vector: Exclude): { $binary: string } | number[] => { +const serialize = (vector: Exclude) => { if ('$binary' in vector) { return vector; } + return serializeFromArray(vector); +}; - if (typeof Buffer !== 'undefined') { +const serializeFromArray = forJSEnv<(vector: number[] | Float32Array) => number[] | { $binary: string }>({ + server: (vector) => { const buffer = Buffer.allocUnsafe(vector.length * 4); for (let i = 0; i < vector.length; i++) { @@ -227,9 +231,8 @@ const serialize = (vector: Exclude): { $binary } return { $binary: buffer.toString('base64') }; - } - - if (typeof window !== 'undefined' && window.btoa) { + }, + browser: (vector) => { const buffer = new Uint8Array(vector.length * 4); const view = new DataView(buffer.buffer); @@ -243,17 +246,17 @@ const serialize = (vector: Exclude): { $binary } return { $binary: window.btoa(binary) }; - } - - if (vector instanceof Float32Array) { - return Array.from(vector); - } - - return vector; -}; + }, + unknown: (vector) => { + if (vector instanceof Float32Array) { + return Array.from(vector); + } + return vector; + }, +}); -const deserializeToNumberArray = (serialized: string): number[] | undefined => { - if (typeof Buffer !== 'undefined') { +const deserializeToNumberArray = forJSEnv<(serialized: string) => number[] | undefined>({ + server: (serialized) => { const buffer = Buffer.from(serialized, 'base64'); const vector = Array.from({ length: buffer.length / 4 }); @@ -262,19 +265,21 @@ const deserializeToNumberArray = (serialized: string): number[] | undefined => { } return vector; - } - - const deserialized = deserializeToF32Array(serialized); - - if (deserialized) { - return Array.from(deserialized); - } - - return undefined; -}; + }, + browser: (serialized) => { + const deserialized = deserializeToF32Array(serialized); -const deserializeToF32Array = (serialized: string): Float32Array | undefined => { - if (typeof Buffer !== 'undefined') { + if (deserialized) { + return Array.from(deserialized); + } + }, + unknown: () => { + return undefined; + }, +}); + +const deserializeToF32Array = forJSEnv<(serialized: string) => Float32Array | undefined>({ + server: (serialized) => { const buffer = Buffer.from(serialized, 'base64'); const vector = new Float32Array(buffer.length / 4); @@ -283,9 +288,8 @@ const deserializeToF32Array = (serialized: string): Float32Array | undefined => } return vector; - } - - if (typeof window !== 'undefined') { + }, + browser: (serialized) => { const binary = window.atob(serialized); const buffer = new Uint8Array(binary.length); @@ -294,7 +298,8 @@ const deserializeToF32Array = (serialized: string): Float32Array | undefined => } return new Float32Array(buffer.buffer); - } - - return undefined; -}; + }, + unknown: () => { + return undefined; + }, +}); diff --git a/src/lib/token-providers/userpass-token-providers.ts b/src/lib/token-providers/userpass-token-providers.ts index 532136f6..04072415 100644 --- a/src/lib/token-providers/userpass-token-providers.ts +++ b/src/lib/token-providers/userpass-token-providers.ts @@ -13,6 +13,7 @@ // limitations under the License. import { TokenProvider } from '@/src/lib/token-providers/token-provider'; +import { forJSEnv } from '@/src/lib/utils'; /** * A token provider which translates a username-password pair into the appropriate authentication token for DSE, HCD. @@ -40,7 +41,7 @@ export class UsernamePasswordTokenProvider extends TokenProvider { */ constructor(username: string, password: string) { super(); - this.#token = `Cassandra:${this.#encodeB64(username)}:${this.#encodeB64(password)}`; + this.#token = `Cassandra:${encodeB64(username)}:${encodeB64(password)}`; } /** @@ -51,14 +52,16 @@ export class UsernamePasswordTokenProvider extends TokenProvider { override getToken(): string { return this.#token; } - - #encodeB64(input: string) { - if (typeof window !== 'undefined' && typeof window.btoa === 'function') { - return window.btoa(input); - } else if (typeof Buffer === 'function') { - return Buffer.from(input, 'utf-8').toString('base64'); - } else { - throw new Error('Unable to encode username/password to base64... please provide the "Cassandra:[username_b64]:[password_b64]" token manually'); - } - } } + +const encodeB64 = forJSEnv<(input: string) => string>({ + server: (input) => { + return Buffer.from(input, 'utf-8').toString('base64'); + }, + browser: (input) => { + return window.btoa(input); + }, + unknown: () => { + throw new Error('Unable to encode username/password to base64... please provide the "Cassandra:[username_b64]:[password_b64]" token manually'); + }, +}); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 646604cb..c17a0cd1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -101,3 +101,20 @@ export function stringArraysEqual(a: readonly string[], b: readonly string[]): b return true; } + +interface JSEnvs { + server: T, + browser: T, + unknown: T, +} + +export function forJSEnv any>(fns: JSEnvs) { + const env = + (typeof globalThis.Buffer !== 'undefined') + ? 'server' : + (typeof globalThis.window !== 'undefined') + ? 'browser' + : 'unknown'; + + return fns[env]; +} diff --git a/tests/unit/common/token-providers.test.ts b/tests/unit/common/token-providers.test.ts index 8439ec4d..5d80f782 100644 --- a/tests/unit/common/token-providers.test.ts +++ b/tests/unit/common/token-providers.test.ts @@ -18,8 +18,6 @@ import { describe, it } from '@/tests/testlib'; import assert from 'assert'; describe('unit.common.token-providers', () => { - const anyGlobalThis = globalThis as any; - describe('StaticTokenProvider', () => { it('should provide the token it was given', () => { const tp = new StaticTokenProvider('token'); @@ -28,29 +26,9 @@ describe('unit.common.token-providers', () => { }); describe('UsernamePasswordTokenProvider', () => { - it('should provide the properly encoded cassandra token in node', () => { - const tp = new UsernamePasswordTokenProvider('username', 'password'); - assert.strictEqual(tp.getToken(), 'Cassandra:dXNlcm5hbWU=:cGFzc3dvcmQ='); - }); - - it('should provide the properly encoded cassandra token in the browser', () => { - const [window, buffer] = [anyGlobalThis.window, anyGlobalThis.Buffer]; - - anyGlobalThis.window = { btoa: anyGlobalThis.btoa }; - anyGlobalThis.Buffer = null!; + it('should provide the properly encoded cassandra token', () => { const tp = new UsernamePasswordTokenProvider('username', 'password'); assert.strictEqual(tp.getToken(), 'Cassandra:dXNlcm5hbWU=:cGFzc3dvcmQ='); - - [anyGlobalThis.window, anyGlobalThis.Buffer] = [window, buffer]; - }); - - it('should throw an error if invalid environment', () => { - const buffer = globalThis.Buffer; - - anyGlobalThis.Buffer = null!; - assert.throws(() => new UsernamePasswordTokenProvider('username', 'password')); - - anyGlobalThis.Buffer = buffer; }); }); }); diff --git a/tests/unit/lib/logging/logger.test.ts b/tests/unit/lib/logging/logger.test.ts index 5916cb24..fa5dc3bf 100644 --- a/tests/unit/lib/logging/logger.test.ts +++ b/tests/unit/lib/logging/logger.test.ts @@ -151,5 +151,13 @@ describe('unit.lib.logging.logger', () => { assert.strictEqual(stdout.length, 1); assert.strictEqual(stderr.length, 0); }); + + it('should not log events if not enabled', () => { + const logger = new Logger([{ events: ['commandStarted'], emits: ['stdout'] }], emitter, console); + logger.commandStarted?.({ timeoutManager: { initial: () => ({}) }, command: {} } as any); + assert.strictEqual(events.length, 0); + assert.strictEqual(stdout.length, 1); + assert.strictEqual(stderr.length, 0); + }); }); }); From 2e670bbc5b6b1e9ffd4eaa266bdaa728c49ecbbc Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 11:31:19 +0530 Subject: [PATCH 09/44] more work on blob & vector unit tests --- src/documents/datatypes/blob.ts | 6 +-- src/documents/datatypes/vector.ts | 10 ++++- src/lib/utils.ts | 22 +++++++--- .../documents/tables/insert-one.test.ts | 13 ++++-- tests/integration/misc/quickstart.test.ts | 5 ++- tests/testlib/config.ts | 2 + tests/testlib/test-fns/it.ts | 44 ++++++++++++++++++- tests/unit/common/token-providers.test.ts | 13 +++++- tests/unit/documents/datatypes/blob.test.ts | 28 +++++++++++- tests/unit/documents/datatypes/vector.test.ts | 30 ++++++++++++- 10 files changed, 150 insertions(+), 23 deletions(-) diff --git a/src/documents/datatypes/blob.ts b/src/documents/datatypes/blob.ts index 3b8d7a6b..08a1b61f 100644 --- a/src/documents/datatypes/blob.ts +++ b/src/documents/datatypes/blob.ts @@ -157,11 +157,11 @@ export class DataAPIBlob implements TableCodec { return arrayBufferToBase64(this.#raw); } - if (this.#raw instanceof Buffer) { - return this.#raw.toString('base64'); + if ('$binary' in this.#raw) { + return this.#raw.$binary; } - return this.#raw.$binary; + return this.#raw.toString('base64'); } /** diff --git a/src/documents/datatypes/vector.ts b/src/documents/datatypes/vector.ts index dd921d2f..44f05559 100644 --- a/src/documents/datatypes/vector.ts +++ b/src/documents/datatypes/vector.ts @@ -268,7 +268,6 @@ const deserializeToNumberArray = forJSEnv<(serialized: string) => number[] | und }, browser: (serialized) => { const deserialized = deserializeToF32Array(serialized); - if (deserialized) { return Array.from(deserialized); } @@ -297,7 +296,14 @@ const deserializeToF32Array = forJSEnv<(serialized: string) => Float32Array | un buffer[i] = binary.charCodeAt(i); } - return new Float32Array(buffer.buffer); + const vector = new Float32Array(buffer.buffer); + const view = new DataView(buffer.buffer); + + for (let i = 0; i < vector.length; i++) { + vector[i] = view.getFloat32(i * 4, false); + } + + return vector; }, unknown: () => { return undefined; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c17a0cd1..234bf298 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -16,6 +16,7 @@ import { DataAPIEnvironment, nullish } from '@/src/lib/types'; import { DataAPIEnvironments } from '@/src/lib/constants'; import JBI from 'json-bigint'; import { SomeDoc } from '@/src/documents'; +import process from 'node:process'; /** * @internal @@ -108,13 +109,20 @@ interface JSEnvs { unknown: T, } -export function forJSEnv any>(fns: JSEnvs) { - const env = - (typeof globalThis.Buffer !== 'undefined') - ? 'server' : - (typeof globalThis.window !== 'undefined') - ? 'browser' - : 'unknown'; +const getJSEnv = () => + (typeof globalThis.window !== 'undefined') + ? 'browser' : + (typeof globalThis.Buffer !== 'undefined') + ? 'server' + : 'unknown'; + +const env = getJSEnv(); +export function forJSEnv any>(fns: JSEnvs) { + if (process.env.CLIENT_DYNAMIC_JS_ENV_CHECK) { + return (...args: Parameters) => { + return fns[getJSEnv()](...args); + }; + } return fns[env]; } diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index de079415..6acfa8da 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -24,11 +24,11 @@ import { InetAddress, UUID, } from '@/src/documents'; -import { describe, it, parallel } from '@/tests/testlib'; +import { it, parallel, describe } from '@/tests/testlib'; import assert from 'assert'; import BigNumber from 'bignumber.js'; -parallel('integration.documents.tables.insert-one', { truncate: 'colls:before' }, ({ table, table_ }) => { +parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' }, ({ table, table_ }) => { it('should insert one partial row', async (key) => { const inserted = await table.insertOne({ text: key, @@ -171,7 +171,9 @@ parallel('integration.documents.tables.insert-one', { truncate: 'colls:before' } await table.drop(); })); }); + }); + describe('scalar inserts (group #2)', ({ db }) => { it('should handle different ascii insertion cases', async () => { const table = await db.createTable('temp_ascii', { definition: { columns: { ascii: 'ascii' }, primaryKey: 'ascii' } }); @@ -202,9 +204,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'colls:before' } await table.drop(); }); - }); - describe('scalar inserts (group #2)', ({ db }) => { it('should handle different blob insertion cases', async () => { const table = await db.createTable('temp_blob', { definition: { columns: { blob: 'blob' }, primaryKey: 'blob' } }); const buffer = Buffer.from([0x0, 0x1]); @@ -366,6 +366,11 @@ parallel('integration.documents.tables.insert-one', { truncate: 'colls:before' } await table.drop(); })); }); + + // it('should handle different duration insertion cases', () => { + // const table = db.createTable('temp_duration', { definition: { columns: { duration: 'duration' }, primaryKey: 'duration' } }); + // + // }); }); // describe('scalar inserts (group #3)', ({ db }) => { diff --git a/tests/integration/misc/quickstart.test.ts b/tests/integration/misc/quickstart.test.ts index 878eb3a3..75185365 100644 --- a/tests/integration/misc/quickstart.test.ts +++ b/tests/integration/misc/quickstart.test.ts @@ -95,7 +95,10 @@ parallel('integration.misc.quickstart', { drop: 'colls:after' }, () => { const dbInfo = databases.find(db => db.id === id); assert.ok(dbInfo?.name); assert.strictEqual(dbInfo.id, id); - assert.deepStrictEqual(dbInfo.regions, [{ name: region, apiEndpoint: TEST_APPLICATION_URI }]); + assert.strictEqual(dbInfo.regions.length, 1); + assert.strictEqual(dbInfo.regions[0].name, region); + assert.strictEqual(dbInfo.regions[0].apiEndpoint, region); + assert.ok(dbInfo.regions[0].createdAt as unknown instanceof Date); const dbAdmin = admin.dbAdmin(dbInfo.id, dbInfo.regions[0].name); const keyspaces = await dbAdmin.listKeyspaces(); diff --git a/tests/testlib/config.ts b/tests/testlib/config.ts index f96d2ac0..e23fd324 100644 --- a/tests/testlib/config.ts +++ b/tests/testlib/config.ts @@ -62,3 +62,5 @@ export const LOGGING_PRED: (e: DataAPIClientEvent, isGlobal: boolean) => boolean : () => false; export const SKIP_PRELUDE = !!process.env.SKIP_PRELUDE || false; + +process.env.CLIENT_DYNAMIC_JS_ENV_CHECK = 'true'; diff --git a/tests/testlib/test-fns/it.ts b/tests/testlib/test-fns/it.ts index 9e462cd1..72ed5c59 100644 --- a/tests/testlib/test-fns/it.ts +++ b/tests/testlib/test-fns/it.ts @@ -25,9 +25,15 @@ export type TestFn = SyncTestFn | AsyncTestFn; type SyncTestFn = (...keys: string[]) => void; type AsyncTestFn = (...keys: string[]) => Promise; +interface TestOptions { + pretendEnv?: 'server' | 'browser' | 'unknown'; +} + interface TaggableTestFunction { (name: string, fn: SyncTestFn): Mocha.Test | null; + (name: string, options: TestOptions, fn: SyncTestFn): Mocha.Test | null; (name: string, fn: AsyncTestFn): Mocha.Test | null; + (name: string, options: TestOptions, fn: AsyncTestFn): Mocha.Test | null; } export let it: TaggableTestFunction; @@ -36,7 +42,15 @@ export let it: TaggableTestFunction; // get() { throw new Error('Can not use return type of `it` when in a parallel/background block') }, // }); -it = function (name: string, testFn: TestFn) { +it = function (name: string, optsOrFn: TestOptions | TestFn, maybeFn?: TestFn) { + const testFn = (!maybeFn) + ? optsOrFn as TestFn + : maybeFn; + + const opts = (maybeFn) + ? optsOrFn as TestOptions + : undefined; + const skipped = !checkTestsEnabled(name); for (const asyncTestState of [parallelTestState, backgroundTestState]) { @@ -57,8 +71,34 @@ it = function (name: string, testFn: TestFn) { this.timeout(DEFAULT_TEST_TIMEOUT); const keys = Array.from({ length: testFn.length }, () => UUID.v4().toString()); - return testFn(...keys); + return pretendingEnv(opts?.pretendEnv ?? 'server', testFn)(...keys); } return global.it(name, modifiedFn); }; + +function pretendingEnv(env: 'server' | 'browser' | 'unknown', fn: TestFn): TestFn { + if (env === 'server') { + return fn; + } + + return (...args: string[]) => { + const anyGlobalThis = globalThis as any; + const [window, buffer] = [anyGlobalThis.window, anyGlobalThis.Buffer]; + + anyGlobalThis.window = env === 'browser' ? globalThis : undefined; + anyGlobalThis.Buffer = env === 'unknown' ? undefined : buffer; + + const res = fn(...args); + + if (res instanceof Promise) { + res.finally(() => { + [anyGlobalThis.window, anyGlobalThis.Buffer] = [window, buffer]; + }); + } else { + [anyGlobalThis.window, anyGlobalThis.Buffer] = [window, buffer]; + } + + return res; + }; +} diff --git a/tests/unit/common/token-providers.test.ts b/tests/unit/common/token-providers.test.ts index 5d80f782..669d3870 100644 --- a/tests/unit/common/token-providers.test.ts +++ b/tests/unit/common/token-providers.test.ts @@ -13,7 +13,7 @@ // limitations under the License. // noinspection DuplicatedCode -import { UsernamePasswordTokenProvider, StaticTokenProvider } from '@/src/lib'; +import { StaticTokenProvider, UsernamePasswordTokenProvider } from '@/src/lib'; import { describe, it } from '@/tests/testlib'; import assert from 'assert'; @@ -26,9 +26,18 @@ describe('unit.common.token-providers', () => { }); describe('UsernamePasswordTokenProvider', () => { - it('should provide the properly encoded cassandra token', () => { + it('should provide the properly encoded cassandra token on the server', () => { const tp = new UsernamePasswordTokenProvider('username', 'password'); assert.strictEqual(tp.getToken(), 'Cassandra:dXNlcm5hbWU=:cGFzc3dvcmQ='); }); + + it('should provide the properly encoded cassandra token in the browser', { pretendEnv: 'browser' }, () => { + const tp = new UsernamePasswordTokenProvider('username', 'password'); + assert.strictEqual(tp.getToken(), 'Cassandra:dXNlcm5hbWU=:cGFzc3dvcmQ='); + }); + + it('should error in unknown environment', { pretendEnv: 'unknown' }, () => { + assert.throws(() => new UsernamePasswordTokenProvider('username', 'password'), { message: 'Unable to encode username/password to base64... please provide the "Cassandra:[username_b64]:[password_b64]" token manually' }); + }); }); }); diff --git a/tests/unit/documents/datatypes/blob.test.ts b/tests/unit/documents/datatypes/blob.test.ts index bf78bdfb..33beaa1f 100644 --- a/tests/unit/documents/datatypes/blob.test.ts +++ b/tests/unit/documents/datatypes/blob.test.ts @@ -63,7 +63,7 @@ describe('unit.documents.datatypes.blob', () => { } }); - it('should convert between all types', () => { + it('should convert between all types on the server', () => { const blobs = [blob(BUFF), blob(ARR_BUFF), blob(BINARY), blob(blob(BUFF))]; for (const blb of blobs) { @@ -73,6 +73,32 @@ describe('unit.documents.datatypes.blob', () => { } }); + // Technically the browser doesn't have the Buffer, but the "absent Buffer" case is tested in the next test anyways + it('should convert between compatible types in the browser', { pretendEnv: 'browser' }, () => { + const blobs = [blob(BUFF), blob(ARR_BUFF), blob(BINARY), blob(blob(ARR_BUFF))]; + + for (const blb of blobs) { + assert.strictEqual(blb.asBase64(), BINARY.$binary); + assert.deepStrictEqual(blb.asBuffer(), BUFF); + assert.deepStrictEqual(blb.asArrayBuffer(), ARR_BUFF); + } + }); + + // This is mainly just testing to see what happens if both Buffer and window are undefined + it('should throw various conversion errors in unknown environments', { pretendEnv: 'unknown' }, () => { + assert.throws(() => blob(BUFF).asBase64()); + assert.throws(() => blob(BUFF).asBuffer()); + assert.throws(() => blob(BUFF).asArrayBuffer()); + + assert.throws(() => blob(ARR_BUFF).asBase64()); + assert.throws(() => blob(ARR_BUFF).asBuffer()); + assert.ok(blob(ARR_BUFF).asArrayBuffer()); + + assert.ok(blob(BINARY).asBase64()); + assert.throws(() => blob(BINARY).asBuffer()); + assert.throws(() => blob(BINARY).asArrayBuffer()); + }); + it('should throw on Buffer not available', () => { const blb = new DataAPIBlob(BUFF); const origBuffer = globalThis.Buffer; diff --git a/tests/unit/documents/datatypes/vector.test.ts b/tests/unit/documents/datatypes/vector.test.ts index 49b0f41a..12a1538c 100644 --- a/tests/unit/documents/datatypes/vector.test.ts +++ b/tests/unit/documents/datatypes/vector.test.ts @@ -63,7 +63,7 @@ describe('unit.documents.datatypes.vector', () => { } }); - it('should convert between all types', () => { + it('should convert between all types on the server', () => { const vectors = [vector(ARR), vector(F32ARR), vector(BINARY), vector(vector(ARR))]; for (const vec of vectors) { @@ -72,4 +72,32 @@ describe('unit.documents.datatypes.vector', () => { assert.deepStrictEqual(vec.asFloat32Array(), F32ARR); } }); + + it('should convert between all types in the browser', { pretendEnv: 'browser' }, () => { + const vectors = [vector(ARR), vector(F32ARR), vector(BINARY), vector(vector(ARR))]; + + for (const vec of vectors) { + assert.strictEqual(vec.asBase64(), BINARY.$binary); + assert.deepStrictEqual(vec.asArray(), ARR); + assert.deepStrictEqual(vec.asFloat32Array(), F32ARR); + } + }); + + it('should throw various conversion errors in unknown environments', { pretendEnv: 'unknown' }, () => { + assert.throws(() => vector(ARR).asBase64()); + assert.ok(vector(ARR).asArray()); + assert.ok(vector(ARR).asFloat32Array()); + + assert.throws(() => vector(F32ARR).asBase64()); + assert.ok(vector(F32ARR).asArray()); + assert.ok(vector(F32ARR).asFloat32Array()); + + assert.ok(vector(BINARY).asBase64()); + assert.throws(() => vector(BINARY).asArray()); + assert.throws(() => vector(BINARY).asFloat32Array()); + + assert.throws(() => vector(vector(ARR)).asBase64()); + assert.ok(vector(vector(ARR)).asArray()); + assert.ok(vector(vector(ARR)).asFloat32Array()); + }); }); From eea4db08ef008c6a44197787e736a7080cbed4a4 Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 22:48:43 +0530 Subject: [PATCH 10/44] proper checking for Bignumbers --- src/documents/collections/ser-des/ser-des.ts | 13 +++++++++---- src/documents/tables/ser-des/ser-des.ts | 5 ++--- src/lib/utils.ts | 5 +++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/documents/collections/ser-des/ser-des.ts b/src/documents/collections/ser-des/ser-des.ts index f97e9f31..977b2ac7 100644 --- a/src/documents/collections/ser-des/ser-des.ts +++ b/src/documents/collections/ser-des/ser-des.ts @@ -16,8 +16,7 @@ import { SerDes, BaseSerDesConfig } from '@/src/lib/api/ser-des/ser-des'; import { BaseDesCtx, BaseSerCtx, CONTINUE } from '@/src/lib/api/ser-des/ctx'; import { CollCodecs, CollCodecSerDesFns } from '@/src/documents/collections/ser-des/codecs'; import { $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; -import { stringArraysEqual } from '@/src/lib/utils'; -import BigNumber from 'bignumber.js'; +import { isBigNumber, stringArraysEqual } from '@/src/lib/utils'; /** * @public @@ -93,7 +92,6 @@ const DefaultCollectionSerDesCfg = { } } - if (typeof value === 'object' && value !== null) { if (value[$SerializeForCollection]) { if ((resp = value[$SerializeForCollection](ctx))[0] !== CONTINUE) { @@ -109,9 +107,16 @@ const DefaultCollectionSerDesCfg = { } } - if (ctx.bigNumsEnabled && value instanceof BigNumber) { + if (isBigNumber(value)) { + if (!ctx.bigNumsEnabled) { + throw new Error('BigNumber serialization must be enabled through serdes.enableBigNumbers in CollectionSerDesConfig'); + } return ctx.done(); } + } else if (typeof value === 'bigint') { + if (!ctx.bigNumsEnabled) { + throw new Error('BigNumber serialization must be enabled through serdes.enableBigNumbers in CollectionSerDesConfig'); + } } for (const codec of codecs.customGuard) { diff --git a/src/documents/tables/ser-des/ser-des.ts b/src/documents/tables/ser-des/ser-des.ts index 84dc92be..987d4f3d 100644 --- a/src/documents/tables/ser-des/ser-des.ts +++ b/src/documents/tables/ser-des/ser-des.ts @@ -22,8 +22,7 @@ import { import { TableCodecs, TableCodecSerDesFns } from '@/src/documents/tables/ser-des/codecs'; import { BaseDesCtx, BaseSerCtx, CONTINUE } from '@/src/lib/api/ser-des/ctx'; import { $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; -import BigNumber from 'bignumber.js'; -import { stringArraysEqual } from '@/src/lib/utils'; +import { isBigNumber, stringArraysEqual } from '@/src/lib/utils'; import { RawCodec } from '@/src/lib/api/ser-des/codecs'; import { UnexpectedDataAPIResponseError } from '@/src/client'; @@ -149,7 +148,7 @@ const DefaultTableSerDesCfg = { } } - if (value instanceof BigNumber) { + if (isBigNumber(value)) { ctx.bigNumsPresent = true; return ctx.done(); } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 234bf298..521bacc2 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -17,6 +17,7 @@ import { DataAPIEnvironments } from '@/src/lib/constants'; import JBI from 'json-bigint'; import { SomeDoc } from '@/src/documents'; import process from 'node:process'; +import BigNumber from 'bignumber.js'; /** * @internal @@ -126,3 +127,7 @@ export function forJSEnv any>(fns: JSEnvs) { } return fns[env]; } + +export function isBigNumber(value: object): value is BigNumber { + return BigNumber.isBigNumber(value) && value.constructor?.name === 'BigNumber'; +} From 344b10a687fc127a71c4057585d048b3fffd3449 Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 23:01:40 +0530 Subject: [PATCH 11/44] killed DataAPITimestamp --- etc/astra-db-ts.api.md | 53 ++---- etc/docs/DATATYPES.md | 87 ++++------ src/db/types/tables/table-schema.ts | 8 +- src/documents/collections/ser-des/codecs.ts | 3 - src/documents/datatypes/dates.ts | 158 +----------------- src/documents/tables/ser-des/codecs.ts | 12 +- .../documents/tables/find-one.test.ts | 5 +- .../documents/tables/insert-one.test.ts | 3 +- .../integration/documents/tables/misc.test.ts | 18 +- .../documents/tables/update-one.test.ts | 5 +- 10 files changed, 73 insertions(+), 279 deletions(-) diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index c112fd9c..817eed80 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -370,8 +370,6 @@ export class CollCodecs { static forPath(path: string[], optsOrClass: CodecOpts | CollCodecClass): RawCodec; // (undocumented) static forType(type: string, optsOrClass: CodecOpts | CollCodecClass): RawCodec; - // (undocumented) - static USE_DATA_API_TIMESTAMPS_FOR_DATES: RawCodec; } // @public (undocumented) @@ -861,7 +859,7 @@ export class DataAPIDate implements TableCodec { [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; constructor(input?: string | Date | DataAPIDateComponents); components(): DataAPIDateComponents; - toDate(base?: Date | DataAPITime | DataAPITimestamp): Date; + toDate(base?: Date | DataAPITime): Date; toString(): string; } @@ -968,7 +966,7 @@ export class DataAPITime implements TableCodec { nanoseconds?: number; })); components(): DataAPITimeComponents; - toDate(base?: Date | DataAPIDate | DataAPITimestamp): Date; + toDate(base?: Date | DataAPIDate): Date; toString(): string; } @@ -993,41 +991,12 @@ export class DataAPITimeoutError extends DataAPIError { readonly timeout: Partial; } -// @public -export class DataAPITimestamp implements CollCodec, TableCodec { - static [$DeserializeForCollection](_: string, value: any, ctx: CollDesCtx): readonly [0, (DataAPITimestamp | undefined)?]; - static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (DataAPITimestamp | undefined)?]; - [$SerializeForCollection](ctx: CollSerCtx): readonly [0, ({ - $date: string; - } | undefined)?]; - [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; - constructor(input?: string | Date | Partial); - components(): DataAPITimestampComponents; - toDate(): Date; - toString(): string; -} - -// @public -export interface DataAPITimestampComponents { - date: number; - hours: number; - minutes: number; - month: number; - nanoseconds: number; - seconds: number; - year: number; -} - // @public export class DataAPIVector implements CollCodec, TableCodec { static [$DeserializeForCollection](_: string, value: any, ctx: CollDesCtx): readonly [0, (DataAPIVector | undefined)?]; static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (DataAPIVector | undefined)?]; - [$SerializeForCollection](ctx: CollSerCtx): readonly [0, (number[] | { - $binary: string; - } | undefined)?]; - [$SerializeForTable](ctx: TableSerCtx): readonly [0, (number[] | { - $binary: string; - } | undefined)?]; + [$SerializeForCollection](ctx: CollSerCtx): readonly [0, any?]; + [$SerializeForTable](ctx: TableSerCtx): readonly [0, any?]; constructor(vector: DataAPIVectorLike, validate?: boolean); asArray(): number[]; asBase64(): string; @@ -1853,7 +1822,7 @@ export class TableCodecs { // @public (undocumented) export interface TableCodecSerDesFns { // (undocumented) - deserialize: (key: string | undefined, val: any, ctx: TableDesCtx, definition: SomeDoc) => ReturnType>; + deserialize: (key: string | undefined, val: any, ctx: TableDesCtx, definition?: SomeDoc) => ReturnType>; // (undocumented) serialize: SerDesFn; } @@ -2001,9 +1970,6 @@ export interface TimeoutDescriptor { tableAdminTimeoutMs: number; } -// @public -export const timestamp: (timestamp?: string | Date | DataAPITimestampComponents) => DataAPITimestamp; - // Warning: (ae-forgotten-export) The symbol "Merge" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "_ToDotNotation" needs to be exported by the entry point index.d.ts // @@ -2038,6 +2004,15 @@ export type TypeCodec = Pick & { type: string; }; +// @public +export class UnexpectedDataAPIResponseError extends Error { + // @internal + constructor(message: string, rawDataAPIResponse: unknown); + readonly rawDataAPIResponse?: unknown; + // (undocumented) + static require(val: T | null | undefined, message: string, rawDataAPIResponse?: unknown): T; +} + // @public export type UpdateFilter = Record; diff --git a/etc/docs/DATATYPES.md b/etc/docs/DATATYPES.md index b74d2f3c..aca650ab 100644 --- a/etc/docs/DATATYPES.md +++ b/etc/docs/DATATYPES.md @@ -140,25 +140,6 @@ const doc = await collection.findOne(); console.log(doc.date instanceof Date); // true ``` -If you prefer to use `DataAPITimestamp`s for interop with tables, that's also allowed, though you'll need to enable a certain codec if you want to read the date back as a `DataAPITimestamp`. - -- `DataAPITimestamp`s will still serialize to a `$date` by default, even if you don't set the necessary codec. - -```typescript -import { CollCodecs, DataAPITimestamp, timestamp } from '@datastax/astra-db-ts'; - -const collection = db.collection('my_coll', { - serdes: { codecs: [CollCodecs.USE_DATA_API_TIMESTAMPS_FOR_DATES] }, -}); - -await collection.insertOne({ - date: timestamp(), // Equivalent to `new DataAPITimestamp()` -}); - -const doc = await collection.findOne(); -console.log(doc.date instanceof DataAPITimestamp); // true -``` - ### ObjectIds You can use objectIds in collections using the `ObjectId` class (or the `oid` shorthand). Make sure you're importing this from `'@datastax/astra-db-ts'`, and _not_ from `'bson'`. @@ -336,32 +317,34 @@ console.log(row.list[0]); // 'value' ### Dates & times -Due to the variety of date & time classes available through the Data API, four custom classes are provided to represent them in the client. +Due to the variety of date & time classes available through the Data API, some custom classes are provided to represent them in the client. + +Only the `timestamp` type is represented by the native JavaScript `Date` object. ```typescript -import { date, duration, time, timestamp, ... } from '@datastax/astra-db-ts'; +import { date, duration, time, ...g } from '@datastax/astra-db-ts'; await table.insertOne({ date: date(), // Equivalent to `new DataAPIDate()` time: time(), // Equivalent to `new DataAPITime()` - timestamp: timestamp(), // Equivalent to `new DataAPITimestamp()` + timestamp: new Date(), // Uses the native `Date` object duration: duration('P5DT30M'), // Equivalent to `new DataAPIDuration(...)` }); const row = await table.findOne(); console.log(row.date instanceof DataAPIDate); // true console.log(row.time instanceof DataAPITime); // true -console.log(row.timestamp instanceof DataAPITimestamp); // true +console.log(row.timestamp instanceof Date); // true console.log(row.duration instanceof DataAPIDuration); // true ``` -You can create these classes through the constructor function, or through the respective shorthand, by providing the date/time/duration in a few different ways: +You can create the custom classes through the constructor function, or through their respective shorthands, by providing the date/time/duration in a few different ways: 1. As a raw string formatted as it would be stored in the database (`'1992-05-28'`, `'12:34:56'`, `'2021-09-30T12:34:56.789Z'`, `'P5DT30M'`) 2. As a `Date` object (`new Date(1734070574056)`) - - Durations are the exception here, as they doesn't have a direct `Date` equivalent + - Durations are the exception here, as they don't have a direct `Date` equivalent 3. As the `*Components` object for that respective class (e.g. `{ year: 1992, month: 5, day: 28 }`) -From each class, you can generally: +From each custom class, you can generally: - Get the string representation of the date/time/duration using `.toString()` - Get the date/time as a `Date` object using `.toDate()` - Get the individual components of the date/time using `.components()` @@ -498,29 +481,29 @@ If you really want to change the behavior of how a certain type is deserialized, ### Tables -| Type | Type | Shorthand | Examples | -|-------------|--------------------|-------------|--------------------------------------------------------------------------------------| -| `ascii` | `string` | - | `'Hello!'` | -| `bigint` | `number` | - | `42` | -| `blob` | `DataAPIBlob` | `blob` | `new DataAPIBlob(Buffer.from(...))`, `blob({ $binary: '' })` | -| `boolean` | `boolean` | - | `true` | -| `date` | `DataAPIDate` | `date` | `new DataAPIDate()`, `date(new Date(1734070574056))`, `date('1992-05-28')`, `date()` | -| `decimal` | `BigNumber` | - | `new BigNumber(123.4567)`, `BigNumber('123456.7e-3')` | -| `double` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | -| `duration` | `DataAPIDuration` | `duration` | `new DataAPIDuration('3w')`, `duration('P5DT30M')` | -| `float` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | -| `inet`. | `InetAddress` | `inet` | `new InetAddress('::1')`, `inet('127.0.0.1')` | -| `int` | `number` | - | `42` | -| `list` | `Array` | - | `['value']` | -| `map` | `Map` | - | `new Map([['key', 'value']])` | -| `set` | `Set` | - | `new Set(['value'])` | -| `smallint` | `number` | - | `42` | -| `text` | `string` | - | `'Hello!'` | -| `time` | `DataAPITime` | `time` | `new DataAPITime()`, `time(new Date(1734070574056))`, `time('12:34:56')`, `time()` | -| `timestamp` | `DataAPITimestamp` | `timestamp` | `new DataAPITimestamp('...')`, `timestamp(new Date(1734070574056))`, `timestamp()` | -| `timeuuid` | `UUID` | `timeuuid` | `new UUID('...')`, `UUID.v1()`, `uuid('...')`, `uuid(1)` | -| `tinyint` | `number` | - | `42` | -| `uuid` | `UUID` | `uuid` | `new UUID('...')`, `UUID.v4()`, `uuid('...')`, `uuid(7)` | -| `varchar` | `string` | - | `'Hello!'` | -| `varint` | `bigint` | - | `BigInt('42')`, `42n` | -| `vector` | `DataAPIVector` | `vector` | `new DataAPIVector([.1, .2, .3])`, `vector([.1, .2, .3])` | +| Type | Type | Shorthand | Examples | +|-------------|-------------------|------------|--------------------------------------------------------------------------------------| +| `ascii` | `string` | - | `'Hello!'` | +| `bigint` | `number` | - | `42` | +| `blob` | `DataAPIBlob` | `blob` | `new DataAPIBlob(Buffer.from(...))`, `blob({ $binary: '' })` | +| `boolean` | `boolean` | - | `true` | +| `date` | `DataAPIDate` | `date` | `new DataAPIDate()`, `date(new Date(1734070574056))`, `date('1992-05-28')`, `date()` | +| `decimal` | `BigNumber` | - | `new BigNumber(123.4567)`, `BigNumber('123456.7e-3')` | +| `double` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | +| `duration` | `DataAPIDuration` | `duration` | `new DataAPIDuration('3w')`, `duration('P5DT30M')` | +| `float` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | +| `inet`. | `InetAddress` | `inet` | `new InetAddress('::1')`, `inet('127.0.0.1')` | +| `int` | `number` | - | `42` | +| `list` | `Array` | - | `['value']` | +| `map` | `Map` | - | `new Map([['key', 'value']])` | +| `set` | `Set` | - | `new Set(['value'])` | +| `smallint` | `number` | - | `42` | +| `text` | `string` | - | `'Hello!'` | +| `time` | `DataAPITime` | `time` | `new DataAPITime()`, `time(new Date(1734070574056))`, `time('12:34:56')`, `time()` | +| `timestamp` | `Date` | - | `new Date()`, `new Date(1734070574056)`, `new Date('...')` | +| `timeuuid` | `UUID` | `timeuuid` | `new UUID('...')`, `UUID.v1()`, `uuid('...')`, `uuid(1)` | +| `tinyint` | `number` | - | `42` | +| `uuid` | `UUID` | `uuid` | `new UUID('...')`, `UUID.v4()`, `uuid('...')`, `uuid(7)` | +| `varchar` | `string` | - | `'Hello!'` | +| `varint` | `bigint` | - | `BigInt('42')`, `42n` | +| `vector` | `DataAPIVector` | `vector` | `new DataAPIVector([.1, .2, .3])`, `vector([.1, .2, .3])` | diff --git a/src/db/types/tables/table-schema.ts b/src/db/types/tables/table-schema.ts index 1899294c..6a150d0c 100644 --- a/src/db/types/tables/table-schema.ts +++ b/src/db/types/tables/table-schema.ts @@ -25,7 +25,7 @@ import { DataAPIDate, DataAPIDuration, DataAPITime, - DataAPITimestamp, FoundRow, + FoundRow, InetAddress, SomeRow, UUID, @@ -100,7 +100,7 @@ export type InferrableTable = * // for the table's schema * type _Proof = Equal, * }>; * @@ -109,7 +109,7 @@ export type InferrableTable = * type _ProofPK = Equal, - * dob: DataAPITimestamp, + * dob: Date, * }>; * * // And now `User` can be used wherever. @@ -267,7 +267,7 @@ interface CqlNonGenericType2TSTypeDict { smallint: number | null, text: string | null; time: DataAPITime | null, - timestamp: DataAPITimestamp | null, + timestamp: Date | null, tinyint: number | null, uuid: UUID | null, varchar: string | null, diff --git a/src/documents/collections/ser-des/codecs.ts b/src/documents/collections/ser-des/codecs.ts index 9a77c590..cab6b7d2 100644 --- a/src/documents/collections/ser-des/codecs.ts +++ b/src/documents/collections/ser-des/codecs.ts @@ -16,7 +16,6 @@ import { UUID } from '@/src/documents/datatypes/uuid'; import { ObjectId } from '@/src/documents/datatypes/object-id'; import { DataAPIVector } from '@/src/documents/datatypes/vector'; -import { DataAPITimestamp } from '@/src/documents/datatypes/dates'; import { CollDesCtx, CollSerCtx } from '@/src/documents'; import { EmptyObj, SerDesFn } from '@/src/lib'; import { CodecOpts, RawCodec } from '@/src/lib/api/ser-des/codecs'; @@ -62,8 +61,6 @@ export class CollCodecs { $objectId: CollCodecs.forType('$objectId', ObjectId), }; - public static USE_DATA_API_TIMESTAMPS_FOR_DATES = CollCodecs.forType('$date', DataAPITimestamp); - public static forPath(path: string[], optsOrClass: CodecOpts | CollCodecClass): RawCodec { return { path, diff --git a/src/documents/datatypes/dates.ts b/src/documents/datatypes/dates.ts index 2b75072d..eeb212a3 100644 --- a/src/documents/datatypes/dates.ts +++ b/src/documents/datatypes/dates.ts @@ -14,8 +14,7 @@ import { isNullish } from '@/src/lib/utils'; import { $CustomInspect } from '@/src/lib/constants'; -import { CollCodec, CollDesCtx, CollSerCtx, TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; -import { $DeserializeForCollection, $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; +import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; /** @@ -121,11 +120,7 @@ export class DataAPIDate implements TableCodec { * * @returns The `Date` object representing this `DataAPIDate` */ - public toDate(base?: Date | DataAPITime | DataAPITimestamp): Date { - if (base instanceof DataAPITimestamp) { - base = base.toDate(); - } - + public toDate(base?: Date | DataAPITime): Date { if (!base) { base = new Date(); } @@ -319,11 +314,7 @@ export class DataAPITime implements TableCodec { * * @returns The `Date` object representing this `DataAPITime` */ - public toDate(base?: Date | DataAPIDate | DataAPITimestamp): Date { - if (base instanceof DataAPITimestamp) { - base = base.toDate(); - } - + public toDate(base?: Date | DataAPIDate): Date { if (!base) { base = new Date(); } @@ -350,146 +341,3 @@ export class DataAPITime implements TableCodec { return this.#time; } } - -/** - * A shorthand function for `new DataAPITimestamp(timestamp?)` - * - * If no timestamp is provided, it defaults to the current timestamp. - * - * @public - */ -export const timestamp = (timestamp?: string | Date | DataAPITimestampComponents) => new DataAPITimestamp(timestamp); - -/** - * Represents the time components that make up a `DataAPITimestamp` - * - * @public - */ -export interface DataAPITimestampComponents { - /** - * The year of the timestamp - */ - year: number, - /** - * The month of the timestamp (should be between 1 and 12) - */ - month: number, - /** - * The day of the month - */ - date: number, - /** - * The hour of the timestamp - */ - hours: number, - /** - * The minute of the timestamp - */ - minutes: number, - /** - * The second of the timestamp - */ - seconds: number, - /** - * The nanosecond of the timestamp - */ - nanoseconds: number, -} - -/** - * Represents a `timestamp` column for Data API tables. - * - * You may use the {@link timestamp} function as a shorthand for creating a new `DataAPITimestamp`. - * - * See the official DataStax documentation for more information. - * - * @public - */ -export class DataAPITimestamp implements CollCodec, TableCodec { - readonly #timestamp: string; - - /** - * Implementation of `$SerializeForTable` for {@link TableCodec} - */ - public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.#timestamp); - }; - - /** - * Implementation of `$SerializeForCollection` for {@link TableCodec} - */ - public [$SerializeForCollection](ctx: CollSerCtx) { - return ctx.done({ $date: new Date(this.#timestamp).valueOf() }); - }; - - /** - * Implementation of `$DeserializeForTable` for {@link TableCodec} - */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new DataAPITimestamp(value)); - } - - /** - * Implementation of `$DeserializeForCollection` for {@link TableCodec} - */ - public static [$DeserializeForCollection](_: string, value: any, ctx: CollDesCtx) { - return ctx.done(new DataAPITimestamp(new Date(value.$date).toISOString())); - } - - /** - * Creates a new `DataAPITimestamp` instance from various formats. - * - * @param input - The input to create the `DataAPITimestamp` from - */ - public constructor(input?: string | Date | Partial) { - input ||= new Date(); - - if (typeof input === 'string') { - this.#timestamp = input; - } else if (input instanceof Date) { - this.#timestamp = input.toISOString(); - } else { - this.#timestamp = new Date(input.year ?? 0, input.month ?? 1 - 1, input.date, input.hours, input.minutes, input.seconds, input.nanoseconds ?? 0 / 1_000_000).toISOString(); - } - - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPITimestamp("${this.#timestamp}")`, - }); - } - - /** - * Returns the {@link DataAPITimestampComponents} that make up this `DataAPITimestamp` - * - * @returns The components of the timestamp - */ - public components(): DataAPITimestampComponents { - const date = this.toDate(); - return { - year: date.getFullYear(), - month: date.getMonth() + 1, - date: date.getDate(), - hours: date.getHours(), - minutes: date.getMinutes(), - seconds: date.getSeconds(), - nanoseconds: date.getMilliseconds() * 1_000_000, - }; - } - - /** - * Converts this `DataAPITimestamp` to a `Date` object - * - * @returns The `Date` object representing this `DataAPITimestamp` - */ - public toDate(): Date { - return new Date(this.#timestamp); - } - - /** - * Returns the string representation of this `DataAPITimestamp` - * - * @returns The string representation of this `DataAPITimestamp` - */ - public toString() { - return this.#timestamp; - } -} diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index 8134cfa5..21fb25f4 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -14,7 +14,7 @@ // Important to import from specific paths here to avoid circular dependencies import { DataAPIBlob } from '@/src/documents/datatypes/blob'; -import { DataAPIDate, DataAPIDuration, DataAPITime, DataAPITimestamp } from '@/src/documents/datatypes/dates'; +import { DataAPIDate, DataAPIDuration, DataAPITime } from '@/src/documents/datatypes/dates'; import { InetAddress } from '@/src/documents/datatypes/inet-address'; import { UUID } from '@/src/documents/datatypes/uuid'; import { DataAPIVector } from '@/src/documents/datatypes/vector'; @@ -73,7 +73,15 @@ export class TableCodecs { deserialize: (_, value, ctx) => ctx.done(parseInt(value)), }), time: TableCodecs.forType('time', DataAPITime), - timestamp: TableCodecs.forType('timestamp', DataAPITimestamp), + timestamp: TableCodecs.forType('timestamp', { + serializeClass: Date, + serialize(_, value, ctx) { + return ctx.done(value.toISOString()); + }, + deserialize(_, value, ctx) { + return ctx.done(new Date(value)); + }, + }), timeuuid: TableCodecs.forType('timeuuid', UUID), tinyint: TableCodecs.forType('tinyint', { deserialize: (_, value, ctx) => ctx.done(parseInt(value)), diff --git a/tests/integration/documents/tables/find-one.test.ts b/tests/integration/documents/tables/find-one.test.ts index 184aefd6..30842916 100644 --- a/tests/integration/documents/tables/find-one.test.ts +++ b/tests/integration/documents/tables/find-one.test.ts @@ -18,7 +18,6 @@ import { DataAPIDate, DataAPIDuration, DataAPITime, - DataAPITimestamp, DataAPIVector, InetAddress, UUID, @@ -90,7 +89,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr set: new Set([uuid, uuid, uuid]), smallint: 123, time: new DataAPITime(), - timestamp: new DataAPITimestamp(), + timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), varint: 12312312312312312312312312312312n, @@ -144,7 +143,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr assert.deepStrictEqual(found.time.components(), doc.time.components()); assert.ok(found.timestamp); - assert.deepStrictEqual(found.timestamp.components(), doc.timestamp.components()); + assert.deepStrictEqual(found.timestamp, doc.timestamp); assert.ok(found.uuid); assert.ok(found.uuid.equals(doc.uuid)); diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index 6acfa8da..2544725f 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -19,7 +19,6 @@ import { DataAPIDuration, DataAPIResponseError, DataAPITime, - DataAPITimestamp, DataAPIVector, InetAddress, UUID, @@ -59,7 +58,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' set: new Set([UUID.v4(), UUID.v7(), UUID.v7()]), smallint: 123, time: new DataAPITime(), - timestamp: new DataAPITimestamp(), + timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), varint: 12312312312312312312312312312312n, diff --git a/tests/integration/documents/tables/misc.test.ts b/tests/integration/documents/tables/misc.test.ts index dedbe9fb..1f5bae23 100644 --- a/tests/integration/documents/tables/misc.test.ts +++ b/tests/integration/documents/tables/misc.test.ts @@ -12,27 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { DataAPIResponseError, timestamp } from '@/src/documents'; +import { DataAPIResponseError } from '@/src/documents'; import { it, parallel } from '@/tests/testlib'; import assert from 'assert'; -parallel('integration.documents.tables.misc', ({ db, table }) => { +parallel('integration.documents.tables.misc', ({ db }) => { it('DataAPIResponseError is thrown when doing data api operation on non-existent tables', async () => { const table = db.table('non_existent_collection'); await assert.rejects(() => table.insertOne({ text: 'test' }), DataAPIResponseError); }); - - it('handles timestamps properly', async () => { - const ts1 = timestamp(); - await table.insertOne({ text: '123', int: 0, timestamp: ts1 }); - const row1 = await table.findOne({ text: '123' }); - assert.deepStrictEqual(row1?.timestamp, ts1); - - const ts2 = timestamp(new Date('2021-01-01')); - await table.insertOne({ text: '123', int: 0, timestamp: ts2 }); - const row2 = await table.findOne({ text: '123' }); - assert.deepStrictEqual(row2?.timestamp, ts2); - - console.log(row1.timestamp, row2.timestamp); - }); }); diff --git a/tests/integration/documents/tables/update-one.test.ts b/tests/integration/documents/tables/update-one.test.ts index 623df8fa..284a70da 100644 --- a/tests/integration/documents/tables/update-one.test.ts +++ b/tests/integration/documents/tables/update-one.test.ts @@ -19,7 +19,6 @@ import { DataAPIDuration, DataAPIResponseError, DataAPITime, - DataAPITimestamp, DataAPIVector, InetAddress, UUID, @@ -74,7 +73,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } set: new Set([uuid, uuid, uuid]), smallint: 123, time: new DataAPITime(), - timestamp: new DataAPITimestamp(), + timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), varint: 12312312312312312312312312312312n, @@ -131,7 +130,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } assert.deepStrictEqual(found.time.components(), doc.time.components()); assert.ok(found.timestamp); - assert.deepStrictEqual(found.timestamp.components(), doc.timestamp.components()); + assert.deepStrictEqual(found.timestamp, doc.timestamp); assert.ok(found.uuid); assert.ok(found.uuid.equals(doc.uuid)); From 1e887abd4ca8c66f4b3338cff777f3a580c16d08 Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 23:24:51 +0530 Subject: [PATCH 12/44] serializeGuard-based codecs now have higher priority --- src/db/types/tables/table-schema.ts | 4 +++- src/documents/collections/ser-des/ser-des.ts | 16 +++++++++------- src/documents/tables/ser-des/codecs.ts | 5 ++++- src/documents/tables/ser-des/ser-des.ts | 15 ++++++++------- .../documents/tables/find-one.test.ts | 2 +- .../documents/tables/insert-one.test.ts | 4 ++-- .../documents/tables/update-one.test.ts | 2 +- 7 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/db/types/tables/table-schema.ts b/src/db/types/tables/table-schema.ts index 6a150d0c..083c109d 100644 --- a/src/db/types/tables/table-schema.ts +++ b/src/db/types/tables/table-schema.ts @@ -254,9 +254,10 @@ export type CqlType2TSType = interface CqlNonGenericType2TSTypeDict { ascii: string | null, - bigint: number | null, + bigint: bigint | null, blob: DataAPIBlob | null, boolean: boolean | null, + counter: bigint | null, date: DataAPIDate | null, decimal: BigNumber | null, double: number | null, @@ -268,6 +269,7 @@ interface CqlNonGenericType2TSTypeDict { text: string | null; time: DataAPITime | null, timestamp: Date | null, + timeuuid: UUID | null, tinyint: number | null, uuid: UUID | null, varchar: string | null, diff --git a/src/documents/collections/ser-des/ser-des.ts b/src/documents/collections/ser-des/ser-des.ts index 977b2ac7..5aac0aea 100644 --- a/src/documents/collections/ser-des/ser-des.ts +++ b/src/documents/collections/ser-des/ser-des.ts @@ -92,6 +92,14 @@ const DefaultCollectionSerDesCfg = { } } + for (const codec of codecs.customGuard) { + if (codec.serializeGuard(value, ctx)) { + if ((resp = codec.serialize(key, value, ctx))[0] !== CONTINUE) { + return resp; + } + } + } + if (typeof value === 'object' && value !== null) { if (value[$SerializeForCollection]) { if ((resp = value[$SerializeForCollection](ctx))[0] !== CONTINUE) { @@ -119,13 +127,6 @@ const DefaultCollectionSerDesCfg = { } } - for (const codec of codecs.customGuard) { - if (codec.serializeGuard(value, ctx)) { - if ((resp = codec.serialize(key, value, ctx))[0] !== CONTINUE) { - return resp; - } - } - } return ctx.continue(); }, deserialize(key, value, ctx) { @@ -153,6 +154,7 @@ const DefaultCollectionSerDesCfg = { return resp; } } + return ctx.continue(); }, codecs: Object.values(CollCodecs.Defaults), diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index 21fb25f4..d0becfbf 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -51,9 +51,12 @@ export type TableCodec<_Class extends TableCodecClass> = EmptyObj; export class TableCodecs { public static Defaults = { bigint: TableCodecs.forType('bigint', { - deserialize: (_, value, ctx) => ctx.done(parseInt(value)), + deserialize: (_, value, ctx) => ctx.done(BigInt(value)), }), blob: TableCodecs.forType('blob', DataAPIBlob), + counter: TableCodecs.forType('counter', { + deserialize: (_, value, ctx) => ctx.done(BigInt(value)), + }), date: TableCodecs.forType('date', DataAPIDate), decimal: TableCodecs.forType('decimal', { deserialize: (_, value, ctx) => ctx.done((value instanceof BigNumber) ? value : new BigNumber(value)), diff --git a/src/documents/tables/ser-des/ser-des.ts b/src/documents/tables/ser-des/ser-des.ts index 987d4f3d..177690f9 100644 --- a/src/documents/tables/ser-des/ser-des.ts +++ b/src/documents/tables/ser-des/ser-des.ts @@ -129,6 +129,14 @@ const DefaultTableSerDesCfg = { } } + for (const codec of codecs.customGuard) { + if (codec.serializeGuard(value, ctx)) { + if ((resp = codec.serialize(key, value, ctx))[0] !== CONTINUE) { + return resp; + } + } + } + if (typeof value === 'number') { if (!isFinite(value)) { return ctx.done(value.toString()); @@ -156,13 +164,6 @@ const DefaultTableSerDesCfg = { ctx.bigNumsPresent = true; } - for (const codec of codecs.customGuard) { - if (codec.serializeGuard(value, ctx)) { - if ((resp = codec.serialize(key, value, ctx))[0] !== CONTINUE) { - return resp; - } - } - } return ctx.continue(); }, deserialize(key, _, ctx) { diff --git a/tests/integration/documents/tables/find-one.test.ts b/tests/integration/documents/tables/find-one.test.ts index 30842916..75f0e383 100644 --- a/tests/integration/documents/tables/find-one.test.ts +++ b/tests/integration/documents/tables/find-one.test.ts @@ -78,7 +78,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr map: new Map([]), ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), - bigint: 1231233, + bigint: 1231233n, date: new DataAPIDate(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index 2544725f..27df2ff1 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -47,7 +47,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' map: new Map([[key, UUID.v4()]]), ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), - bigint: 1231233, + bigint: 1231233n, date: new DataAPIDate(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, @@ -154,7 +154,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' }); it('should handle different different int insertion cases', async () => { - await Promise.all(['int', 'tinyint', 'smallint', 'bigint'].map(async (col) => { + await Promise.all(['int', 'tinyint', 'smallint'].map(async (col) => { const table = await db.createTable(`temp_${col}`, { definition: { columns: { intT: col }, primaryKey: 'intT' } }); await assert.rejects(() => table.insertOne({ intT: 1.1 }), DataAPIResponseError, col); diff --git a/tests/integration/documents/tables/update-one.test.ts b/tests/integration/documents/tables/update-one.test.ts index 284a70da..08267292 100644 --- a/tests/integration/documents/tables/update-one.test.ts +++ b/tests/integration/documents/tables/update-one.test.ts @@ -62,7 +62,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } map: new Map([]), ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), - bigint: 1231233, + bigint: 1231233n, date: new DataAPIDate(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, From 29e5805c2835ab8cc3629813585cac4543de81dc Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 2 Jan 2025 23:29:52 +0530 Subject: [PATCH 13/44] bigints & counters now use bigint --- etc/docs/DATATYPES.md | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/etc/docs/DATATYPES.md b/etc/docs/DATATYPES.md index aca650ab..d49a1363 100644 --- a/etc/docs/DATATYPES.md +++ b/etc/docs/DATATYPES.md @@ -73,7 +73,7 @@ await collection.insertOne({ ### BigNumbers > **NOTE** -> Enabling BigNumbers support for a collection will force a slower, bignum-friendly JSON library to be used for all documents in that collection. The difference should be negligible for most use-cases. +> Enabling bigints/BigNumbers support for a collection will force a slower, bignum-friendly JSON library to be used for all documents in that collection. The difference should be negligible for most use-cases. Proper big-number support is still under works in `astra-db-ts`, but a rough version is out currently. @@ -244,28 +244,28 @@ A variety of scalar types, however, are represented by custom `astra-db-ts`-prov ### BigNumbers > **NOTE** -> Enabling BigNumbers support for a collection will force a slower, bignum-friendly JSON library to be used for all documents in that collection. The difference should be negligible for most use-cases. +> Using bigints/BigNumbers in a table will force a slower, bignum-friendly JSON library to be used for all documents in that collection. The difference should be negligible for most use-cases. Unlike collections, `bigint`s & `BigNumber`s are supported completely and natively in tables; you don't need to enable any special options to use them. The performance penalty still applies, however, but it's only in play when there's actually a `bigint` or `BigNumber` present in the object. While you may technically pass any of `number`, `bigint`, or `BigNumber` to the database, it'll be read back as: -- a `bigint` if the column is a `varint` +- a `bigint` if the column is a `varint`, `bigint`, or `counter` - a `BigNumber` if the column is a `decimal` ```typescript import { BigNumber } from '@datastax/astra-db-ts'; await table.insertOne({ - bigint1: 1234567890123456789012345672321312312890n, - bigint2: 10n, + varint: 1234567890123456789012345672321312312890n, + bigint: 18446744073709551615n, decmial: new BigNumber('12345678901234567890123456.72321312312890'), }); const row = await table.findOne(); -console.log(row.bigint1.toString()); // Will be returned as a `bigint` -console.log(row.bigint2.toString()); // Will be returned as a `bigint` +console.log(row.varint.toString()); // Will be returned as a `bigint` +console.log(row.bigint.toString()); // Will be returned as a `bigint` console.log(row.decimal.toString()); // Will be returned as a `BigNumber` ``` @@ -322,7 +322,7 @@ Due to the variety of date & time classes available through the Data API, some c Only the `timestamp` type is represented by the native JavaScript `Date` object. ```typescript -import { date, duration, time, ...g } from '@datastax/astra-db-ts'; +import { date, duration, time, ... } from '@datastax/astra-db-ts'; await table.insertOne({ date: date(), // Equivalent to `new DataAPIDate()` @@ -484,9 +484,10 @@ If you really want to change the behavior of how a certain type is deserialized, | Type | Type | Shorthand | Examples | |-------------|-------------------|------------|--------------------------------------------------------------------------------------| | `ascii` | `string` | - | `'Hello!'` | -| `bigint` | `number` | - | `42` | +| `bigint` | `bigint` | - | `BigInt('42')`, `42n` | | `blob` | `DataAPIBlob` | `blob` | `new DataAPIBlob(Buffer.from(...))`, `blob({ $binary: '' })` | | `boolean` | `boolean` | - | `true` | +| `counter` | `bigint` | - | `BigInt('42')`, `42n` | | `date` | `DataAPIDate` | `date` | `new DataAPIDate()`, `date(new Date(1734070574056))`, `date('1992-05-28')`, `date()` | | `decimal` | `BigNumber` | - | `new BigNumber(123.4567)`, `BigNumber('123456.7e-3')` | | `double` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | From 0303a5e6c2e89c877d07df069331f03efb89bcab Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 3 Jan 2025 17:46:29 +0530 Subject: [PATCH 14/44] a lot more datatypes test wokr --- src/db/db.ts | 6 +- src/documents/datatypes/blob.ts | 54 ++-- src/documents/datatypes/inet-address.ts | 26 +- src/documents/datatypes/object-id.ts | 18 +- src/documents/datatypes/uuid.ts | 20 +- src/documents/datatypes/vector.ts | 54 ++-- src/documents/tables/ser-des/codecs.ts | 9 - .../documents/tables/datatypes.test.ts | 237 +++++++++++++++ .../documents/tables/insert-one.test.ts | 280 +----------------- 9 files changed, 334 insertions(+), 370 deletions(-) create mode 100644 tests/integration/documents/tables/datatypes.test.ts diff --git a/src/db/db.ts b/src/db/db.ts index 59bf4425..370e33b9 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -232,7 +232,7 @@ export class Db { throw new InvalidEnvironmentError('db.id', this.#defaultOpts.environment, ['astra'], 'non-Astra databases have no appropriate ID'); } if (!this.#id) { - throw new Error(`Malformed AstraDB endpoint URL '${this.#endpoint}'—database ID unable to be parsed`); + throw new Error(`Unexpected AstraDB endpoint URL '${this.#endpoint}'—database ID unable to be parsed`); } return this.#id; } @@ -249,7 +249,7 @@ export class Db { throw new InvalidEnvironmentError('db.region', this.#defaultOpts.environment, ['astra'], 'non-Astra databases have no appropriate region'); } if (!this.#region) { - throw new Error(`Malformed AstraDB endpoint URL '${this.#endpoint}'—database region unable to be parsed`); + throw new Error(`Unexpected AstraDB endpoint URL '${this.#endpoint}'—database region unable to be parsed`); } return this.#region; } @@ -393,7 +393,7 @@ export class Db { } if (environment === 'astra') { - return new AstraDbAdmin(this, this.#defaultOpts, options, this.#defaultOpts.dbOptions.token, this.#endpoint!); + return new AstraDbAdmin(this, this.#defaultOpts, options, this.#defaultOpts.dbOptions.token, this.#endpoint); } return new DataAPIDbAdmin(this, this.#httpClient, options); diff --git a/src/documents/datatypes/blob.ts b/src/documents/datatypes/blob.ts index 08a1b61f..56eb7e6c 100644 --- a/src/documents/datatypes/blob.ts +++ b/src/documents/datatypes/blob.ts @@ -43,7 +43,7 @@ export const blob = (blob: DataAPIBlobLike) => new DataAPIBlob(blob); * @public */ export class DataAPIBlob implements TableCodec { - readonly #raw: Exclude; + private readonly _raw!: Exclude; /** * Implementation of `$SerializeForTable` for {@link TableCodec} @@ -74,9 +74,11 @@ export class DataAPIBlob implements TableCodec { throw new TypeError(`Expected blob to be a string, ArrayBuffer, or Buffer (got '${blob}')`); } - this.#raw = (blob instanceof DataAPIBlob) - ? blob.#raw - : blob; + Object.defineProperty(this, '_raw', { + value: (blob instanceof DataAPIBlob) + ? blob._raw + : blob, + }); Object.defineProperty(this, $CustomInspect, { value: this.toString, @@ -89,15 +91,15 @@ export class DataAPIBlob implements TableCodec { * @returns The byte length of the blob */ public get byteLength(): number { - if (this.#raw instanceof ArrayBuffer) { - return this.#raw.byteLength; + if (this._raw instanceof ArrayBuffer) { + return this._raw.byteLength; } - if (this.#raw instanceof Buffer) { - return this.#raw.length; + if (this._raw instanceof Buffer) { + return this._raw.length; } - return ~~((this.#raw.$binary.replace(/=+$/, '').length * 3) / 4); + return ~~((this._raw.$binary.replace(/=+$/, '').length * 3) / 4); } /** @@ -106,7 +108,7 @@ export class DataAPIBlob implements TableCodec { * @returns The raw blob */ public raw(): Exclude { - return this.#raw; + return this._raw; } /** @@ -115,15 +117,15 @@ export class DataAPIBlob implements TableCodec { * @returns The blob as an `ArrayBuffer` */ public asArrayBuffer(): ArrayBuffer { - if (this.#raw instanceof ArrayBuffer) { - return this.#raw; + if (this._raw instanceof ArrayBuffer) { + return this._raw; } - if (this.#raw instanceof Buffer) { - return bufferToArrayBuffer(this.#raw); + if (this._raw instanceof Buffer) { + return bufferToArrayBuffer(this._raw); } - return base64ToArrayBuffer(this.#raw.$binary); + return base64ToArrayBuffer(this._raw.$binary); } /** @@ -136,15 +138,15 @@ export class DataAPIBlob implements TableCodec { throw new Error("Buffer is not available in this environment"); } - if (this.#raw instanceof Buffer) { - return this.#raw; + if (this._raw instanceof Buffer) { + return this._raw; } - if (this.#raw instanceof ArrayBuffer) { - return Buffer.from(this.#raw); + if (this._raw instanceof ArrayBuffer) { + return Buffer.from(this._raw); } - return Buffer.from(this.#raw.$binary, 'base64'); + return Buffer.from(this._raw.$binary, 'base64'); } /** @@ -153,22 +155,22 @@ export class DataAPIBlob implements TableCodec { * @returns The blob as a base64 string */ public asBase64(): string { - if (this.#raw instanceof ArrayBuffer) { - return arrayBufferToBase64(this.#raw); + if (this._raw instanceof ArrayBuffer) { + return arrayBufferToBase64(this._raw); } - if ('$binary' in this.#raw) { - return this.#raw.$binary; + if ('$binary' in this._raw) { + return this._raw.$binary; } - return this.#raw.toString('base64'); + return this._raw.toString('base64'); } /** * Returns a pretty string representation of the `DataAPIBlob`. */ public toString() { - const type = (this.#raw instanceof ArrayBuffer && 'ArrayBuffer') || (this.#raw instanceof Buffer && 'Buffer') || 'base64'; + const type = (this._raw instanceof ArrayBuffer && 'ArrayBuffer') || (this._raw instanceof Buffer && 'Buffer') || 'base64'; return `DataAPIBlob(typeof raw=${type}, byteLength=${this.byteLength})`; } diff --git a/src/documents/datatypes/inet-address.ts b/src/documents/datatypes/inet-address.ts index 85d51889..0efc9feb 100644 --- a/src/documents/datatypes/inet-address.ts +++ b/src/documents/datatypes/inet-address.ts @@ -34,14 +34,14 @@ export const inet = (address: string, version?: 4 | 6) => new InetAddress(addres * @public */ export class InetAddress implements TableCodec { - readonly #raw: string; - #version: 4 | 6 | nullish; + private readonly _raw!: string; + private _version!: 4 | 6 | nullish; /** * Implementation of `$SerializeForTable` for {@link TableCodec} */ public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.#raw); + return ctx.done(this._raw); }; /** @@ -85,11 +85,17 @@ export class InetAddress implements TableCodec { } } - this.#raw = address.toLowerCase(); - this.#version = version; + Object.defineProperty(this, '_raw', { + value: address.toLowerCase(), + }); + + Object.defineProperty(this, '_version', { + value: version, + writable: true, + }); Object.defineProperty(this, $CustomInspect, { - value: () => `InetAddress<${this.version}>("${this.#raw}")`, + value: () => `InetAddress<${this.version}>("${this._raw}")`, }); } @@ -99,10 +105,10 @@ export class InetAddress implements TableCodec { * @returns The IP version of the inet address */ public get version(): 4 | 6 { - if (!this.#version) { - this.#version = isIPv4(this.#raw) ? 4 : 6; + if (!this._version) { + this._version = isIPv4(this._raw) ? 4 : 6; } - return this.#version; + return this._version; } /** @@ -111,7 +117,7 @@ export class InetAddress implements TableCodec { * @returns The string representation of the inet address */ public toString(): string { - return this.#raw; + return this._raw; } } diff --git a/src/documents/datatypes/object-id.ts b/src/documents/datatypes/object-id.ts index 1f6461b8..6908362c 100644 --- a/src/documents/datatypes/object-id.ts +++ b/src/documents/datatypes/object-id.ts @@ -66,13 +66,13 @@ export const oid = (id?: string | number | null) => new ObjectId(id); * @public */ export class ObjectId implements CollCodec { - readonly #raw: string; + private readonly _raw!: string; /** * Implementation of `$SerializeForCollection` for {@link TableCodec} */ public [$SerializeForCollection](ctx: CollSerCtx) { - return ctx.done({ $objectId: this.#raw }); + return ctx.done({ $objectId: this._raw }); }; /** @@ -101,10 +101,12 @@ export class ObjectId implements CollCodec { } } - this.#raw = (typeof id === 'string') ? id.toLowerCase() : genObjectId(id); + Object.defineProperty(this, '_raw', { + value: (typeof id === 'string') ? id.toLowerCase() : genObjectId(id), + }); Object.defineProperty(this, $CustomInspect, { - value: () => `ObjectId("${this.#raw}")`, + value: () => `ObjectId("${this._raw}")`, }); } @@ -121,10 +123,10 @@ export class ObjectId implements CollCodec { */ public equals(other: unknown): boolean { if (typeof other === 'string') { - return this.#raw.localeCompare(other, undefined, { sensitivity: 'accent' }) === 0; + return this._raw.localeCompare(other, undefined, { sensitivity: 'accent' }) === 0; } if (other instanceof ObjectId) { - return this.#raw.localeCompare(other.#raw, undefined, { sensitivity: 'accent' }) === 0; + return this._raw.localeCompare(other._raw, undefined, { sensitivity: 'accent' }) === 0; } return false; } @@ -135,7 +137,7 @@ export class ObjectId implements CollCodec { * @returns The timestamp of the ObjectId. */ public getTimestamp(): Date { - const time = parseInt(this.#raw.slice(0, 8), 16); + const time = parseInt(this._raw.slice(0, 8), 16); return new Date(~~time * 1000); } @@ -143,7 +145,7 @@ export class ObjectId implements CollCodec { * Returns the string representation of the ObjectId. */ public toString(): string { - return this.#raw; + return this._raw; } } diff --git a/src/documents/datatypes/uuid.ts b/src/documents/datatypes/uuid.ts index 89006c68..85c709e9 100644 --- a/src/documents/datatypes/uuid.ts +++ b/src/documents/datatypes/uuid.ts @@ -87,20 +87,20 @@ export class UUID implements CollCodec, TableCodec { */ public readonly version!: number; - readonly #raw: string; + private readonly _raw!: string; /** * Implementation of `$SerializeForTable` for {@link TableCodec} */ public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.#raw); + return ctx.done(this._raw); }; /** * Implementation of `$SerializeForCollection` for {@link TableCodec} */ public [$SerializeForCollection](ctx: CollSerCtx) { - return ctx.done({ $uuid: this.#raw }); + return ctx.done({ $uuid: this._raw }); }; /** @@ -137,14 +137,16 @@ export class UUID implements CollCodec, TableCodec { } } - this.#raw = uuid.toLowerCase(); + Object.defineProperty(this, '_raw', { + value: uuid.toLowerCase(), + }); Object.defineProperty(this, 'version', { - value: version || parseInt(this.#raw[14], 16), + value: version || parseInt(this._raw[14], 16), }); Object.defineProperty(this, $CustomInspect, { - value: () => `UUID<${this.version}>("${this.#raw}")`, + value: () => `UUID<${this.version}>("${this._raw}")`, }); } @@ -161,10 +163,10 @@ export class UUID implements CollCodec, TableCodec { */ public equals(other: unknown): boolean { if (typeof other === 'string') { - return this.#raw === other.toLowerCase(); + return this._raw === other.toLowerCase(); } if (other instanceof UUID) { - return this.#raw === other.#raw; + return this._raw === other._raw; } return false; } @@ -189,7 +191,7 @@ export class UUID implements CollCodec, TableCodec { * Returns the string representation of the UUID in lowercase. */ public toString(): string { - return this.#raw; + return this._raw; } /** diff --git a/src/documents/datatypes/vector.ts b/src/documents/datatypes/vector.ts index 44f05559..03fc8535 100644 --- a/src/documents/datatypes/vector.ts +++ b/src/documents/datatypes/vector.ts @@ -50,20 +50,20 @@ export const vector = (v: DataAPIVectorLike) => new DataAPIVector(v); * @public */ export class DataAPIVector implements CollCodec, TableCodec { - readonly #vector: Exclude; + private readonly _vector!: Exclude; /** * Implementation of `$SerializeForTable` for {@link TableCodec} */ public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(serialize(this.#vector)); + return ctx.done(serialize(this._vector)); }; /** * Implementation of `$SerializeForCollection` for {@link TableCodec} */ public [$SerializeForCollection](ctx: CollSerCtx) { - return ctx.done(serialize(this.#vector)); + return ctx.done(serialize(this._vector)); }; /** @@ -95,9 +95,11 @@ export class DataAPIVector implements CollCodec, TableCode throw new Error(`Invalid vector type; expected number[], base64 string, Float32Array, or DataAPIVector; got '${vector}'`); } - this.#vector = (vector instanceof DataAPIVector) - ? vector.raw() - : vector; + Object.defineProperty(this, '_vector', { + value: (vector instanceof DataAPIVector) + ? vector.raw() + : vector, + }); Object.defineProperty(this, $CustomInspect, { value: this.toString, @@ -110,10 +112,10 @@ export class DataAPIVector implements CollCodec, TableCode * @returns The length of the vector */ public get length(): number { - if ('$binary' in this.#vector) { - return ~~((this.#vector.$binary.replace(/=+$/, '').length * 3) / 4 / 4); + if ('$binary' in this._vector) { + return ~~((this._vector.$binary.replace(/=+$/, '').length * 3) / 4 / 4); } - return this.#vector.length; + return this._vector.length; } /** @@ -122,7 +124,7 @@ export class DataAPIVector implements CollCodec, TableCode * @returns The raw vector */ public raw(): Exclude { - return this.#vector; + return this._vector; } /** @@ -131,12 +133,12 @@ export class DataAPIVector implements CollCodec, TableCode * @returns The vector as a `number[]` */ public asArray(): number[] { - if (this.#vector instanceof Float32Array) { - return Array.from(this.#vector); + if (this._vector instanceof Float32Array) { + return Array.from(this._vector); } - if ('$binary' in this.#vector) { - const deserialized = deserializeToNumberArray(this.#vector.$binary); + if ('$binary' in this._vector) { + const deserialized = deserializeToNumberArray(this._vector.$binary); if (!deserialized) { throw new Error('Could not to deserialize vector from base64 => number[]; unknown environment. Please manually deserialize the binary from `vector.getAsBase64()`'); @@ -145,7 +147,7 @@ export class DataAPIVector implements CollCodec, TableCode return deserialized; } - return this.#vector; + return this._vector; } /** @@ -154,12 +156,12 @@ export class DataAPIVector implements CollCodec, TableCode * @returns The vector as a `Float32Array` */ public asFloat32Array(): Float32Array { - if (this.#vector instanceof Float32Array) { - return this.#vector; + if (this._vector instanceof Float32Array) { + return this._vector; } - if ('$binary' in this.#vector) { - const deserialized = deserializeToF32Array(this.#vector.$binary); + if ('$binary' in this._vector) { + const deserialized = deserializeToF32Array(this._vector.$binary); if (!deserialized) { throw new Error('Could not to deserialize vector from base64 => Float32Array; unknown environment. Please manually deserialize the binary from `vector.getAsBase64()`'); @@ -168,7 +170,7 @@ export class DataAPIVector implements CollCodec, TableCode return deserialized; } - return new Float32Array(this.#vector); + return new Float32Array(this._vector); } /** @@ -177,10 +179,10 @@ export class DataAPIVector implements CollCodec, TableCode * @returns The vector as a base64 string */ public asBase64(): string { - const serialized = serialize(this.#vector); + const serialized = serialize(this._vector); if (!('$binary' in serialized)) { - if (Array.isArray(this.#vector)) { + if (Array.isArray(this._vector)) { throw new Error('Could not convert vector from number[] => base64; unknown environment. Please manually serialize the binary from `vector.raw()`/`vector.getAsArray()`'); } else { throw new Error('Could not convert vector from Float32Array => base64; unknown environment. Please manually serialize the binary from `vector.raw()`/`vector.getAsFloat32Array()`'); @@ -194,11 +196,11 @@ export class DataAPIVector implements CollCodec, TableCode * Returns a pretty string representation of the `DataAPIVector`. */ public toString(): string { - const type = ('$binary' in this.#vector && 'base64') || (this.#vector instanceof Float32Array && 'Float32Array') || 'number[]'; + const type = ('$binary' in this._vector && 'base64') || (this._vector instanceof Float32Array && 'Float32Array') || 'number[]'; - const partial = ('$binary' in this.#vector) - ? `'${this.#vector.$binary.slice(0, 12)}${this.#vector.$binary.length > 12 ? '...' : ''}'` - : `[${this.#vector.slice(0, 2).join(', ')}${this.#vector.length > 2 ? ', ...' : ''}]`; + const partial = ('$binary' in this._vector) + ? `'${this._vector.$binary.slice(0, 12)}${this._vector.$binary.length > 12 ? '...' : ''}'` + : `[${this._vector.slice(0, 2).join(', ')}${this._vector.length > 2 ? ', ...' : ''}]`; return `DataAPIVector<${this.length}>(typeof raw=${type}, preview=${partial})`; } diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index d0becfbf..0cbffd5a 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -68,13 +68,7 @@ export class TableCodecs { float: TableCodecs.forType('float', { deserialize: (_, value, ctx) => ctx.done(parseFloat(value)), }), - int: TableCodecs.forType('int', { - deserialize: (_, value, ctx) => ctx.done(parseInt(value)), - }), inet: TableCodecs.forType('inet', InetAddress), - smallint: TableCodecs.forType('smallint', { - deserialize: (_, value, ctx) => ctx.done(parseInt(value)), - }), time: TableCodecs.forType('time', DataAPITime), timestamp: TableCodecs.forType('timestamp', { serializeClass: Date, @@ -86,9 +80,6 @@ export class TableCodecs { }, }), timeuuid: TableCodecs.forType('timeuuid', UUID), - tinyint: TableCodecs.forType('tinyint', { - deserialize: (_, value, ctx) => ctx.done(parseInt(value)), - }), uuid: TableCodecs.forType('uuid', UUID), vector: TableCodecs.forType('vector', DataAPIVector), varint: TableCodecs.forType('varint', { diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts new file mode 100644 index 00000000..77957160 --- /dev/null +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -0,0 +1,237 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { + blob, + DataAPIBlob, + DataAPIResponseError, + DataAPIVector, + date, + inet, + InetAddress, SomeRow, Table, + vector, +} from '@/src/documents'; +import { it, parallel } from '@/tests/testlib'; +import assert from 'assert'; +import BigNumber from 'bignumber.js'; + +parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { + interface ColumnAsserterOpts { + eqOn?: (a: T) => unknown; + table?: Table; + } + + const mkColumnAsserter = (key: string, col: string, opts?: ColumnAsserterOpts) => ({ + _counter: 0, + _table: opts?.table ?? table, + _eqOn: opts?.eqOn ?? (x => x), + async ok(value: Exp, mapExp: (ex: Exp) => T = (x => x as unknown as T)) { + const obj = { text: key, int: this._counter++, [col]: value }; + const pkey = { text: obj.text, int: obj.int }; + assert.deepStrictEqual(await this._table.insertOne(obj), { insertedId: pkey }); + assert.deepStrictEqual(this._eqOn((await this._table.findOne(pkey) as any)?.[col]), this._eqOn(mapExp(obj[col] as any))); + }, + async notOk(value: unknown, err = DataAPIResponseError) { + const obj = { text: key, int: this._counter++, [col]: value }; + await assert.rejects(() => this._table.insertOne(obj), err); + }, + }); + + it('should handle different text insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'text'); + + await colAsserter.notOk(''); + await colAsserter.notOk('a'.repeat(65536)); + + await colAsserter.ok('a'.repeat(65535)); + await colAsserter.ok('A!@#$%^&*()'); + }); + + (['int', 'tinyint', 'smallint'] as const).map((col) => + it(`should handle different ${col} insertion cases`, async (key) => { + const colAsserter = mkColumnAsserter(key, col); + + await colAsserter.notOk(1.1); + await colAsserter.notOk(1e50); + await colAsserter.notOk(Infinity); + await colAsserter.notOk(NaN); + await colAsserter.notOk('Infinity'); + await colAsserter.notOk('123'); + + await colAsserter.ok(+1); + await colAsserter.ok(-1); + })); + + it('should handle different ascii insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'ascii'); + + await colAsserter.notOk('⨳⨓⨋'); + await colAsserter.notOk('é'); + await colAsserter.notOk('\x80'); + + await colAsserter.ok(''); + await colAsserter.ok('a'.repeat(65535)); + await colAsserter.ok('A!@#$%^&*()'); + await colAsserter.ok('\u0000'); + await colAsserter.ok('\x7F'); + }); + + it('should handle different blob insertion cases', async (key) => { + const buffer = blob(Buffer.from([0x0, 0x1])); + const base64 = blob({ $binary: buffer.asBase64() }); + const arrBuf = blob(buffer.asArrayBuffer()); + + const colAsserter = mkColumnAsserter(key, 'blob', { + eqOn: (blob: DataAPIBlob) => blob.asBase64(), + }); + + await colAsserter.notOk(base64.asBase64()); + await colAsserter.notOk(buffer.asBuffer()); + await colAsserter.notOk(arrBuf.asArrayBuffer()); + + await colAsserter.ok(base64.raw(), _ => base64); + await colAsserter.ok(base64, _ => base64); + await colAsserter.ok(buffer, _ => base64); + await colAsserter.ok(arrBuf, _ => base64); + }); + + it('should handle the numerous different boolean insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'boolean'); + + await colAsserter.notOk('true'); + await colAsserter.notOk(1); + + await colAsserter.ok(true); + await colAsserter.ok(false); + }); + + it('should handle different date insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'date'); + + await colAsserter.notOk('2000-00-06'); + await colAsserter.notOk('2000-01-00'); + await colAsserter.notOk('2000/01/01'); + await colAsserter.notOk('2000-01-32'); + await colAsserter.notOk(date('2000-02-30')); + await colAsserter.notOk(date('+2000-00-06')); + await colAsserter.notOk(date('-0000-00-06')); + await colAsserter.notOk(date('10000-00-06')); + + await colAsserter.ok('1970-01-01', date); + await colAsserter.ok('-0001-01-01', date); + await colAsserter.ok(date('9999-12-31')); + await colAsserter.ok(date('+10000-12-31')); + await colAsserter.ok(date('-10000-12-31')); + }); + + it('should handle different decimal insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'decimal'); + + await colAsserter.notOk('123123.12312312312'); + await colAsserter.notOk(BigNumber('NaN')); + await colAsserter.notOk(BigNumber('Infinity')); + await colAsserter.notOk(BigNumber('-Infinity')); + await colAsserter.notOk(NaN); + await colAsserter.notOk(Infinity); + await colAsserter.notOk(-Infinity); + + await colAsserter.ok(123123.123, BigNumber); + await colAsserter.ok(123123n, n => BigNumber(n.toString())); + await colAsserter.ok(BigNumber('1.1212121131231231231231231231231231231231233122')); + await colAsserter.ok(BigNumber('-1e50')); + await colAsserter.ok(BigNumber('-1e-50')); + }); + + (['float', 'double'] as const).map((col) => + it(`should handle different ${col} insertion cases`, async (key) => { + const colAsserter = mkColumnAsserter(key, col); + + await colAsserter.notOk('123'); + await colAsserter.notOk('nan'); + await colAsserter.notOk('infinity'); + + await colAsserter.ok(123123.125); + await colAsserter.ok(123123n, Number); + await colAsserter.ok(BigNumber('1.34234'), ex => ex.toNumber()); + await colAsserter.ok(NaN); + await colAsserter.ok(Infinity); + await colAsserter.ok(-Infinity); + await colAsserter.ok('-Infinity', parseFloat); + })); + + (['bigint', 'varint'] as const).map((col) => + it(`should handle different ${col} insertion cases`, async (key) => { + const colAsserter = mkColumnAsserter(key, col); + + await colAsserter.notOk('123'); + await colAsserter.notOk(NaN); + await colAsserter.notOk(Infinity); + await colAsserter.notOk(-Infinity); + + await colAsserter.ok(123123, BigInt); + await colAsserter.ok((2n ** 63n) - 1n); + await colAsserter.ok(BigNumber('23423432049238904'), ex => BigInt(ex.toString())); + })); + + it('should handle different inet insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'inet', { + eqOn: (a: InetAddress) => a.toString(), + }); + + await colAsserter.notOk('127.0.0.1/16'); + await colAsserter.notOk('127.0.0.1:80'); + await colAsserter.notOk('6f4e:1900:4545:3:200:f6ff:fe21:645cf'); + await colAsserter.notOk('10.10.10.1000'); + + await colAsserter.ok('::ffff:192.168.0.1', _ => inet('192.168.0.1')); + await colAsserter.ok('127.1', _ => inet('127.0.0.1')); + await colAsserter.ok('127.0.1', _ => inet('127.0.0.1')); + await colAsserter.ok('localhost', _ => inet('127.0.0.1')); + await colAsserter.ok('192.168.36095', _ => inet('192.168.140.255')); + await colAsserter.ok('192.11046143', _ => inet('192.168.140.255')); + + await colAsserter.ok('127.0.0.1', inet); + await colAsserter.ok('::1', _ => inet('0:0:0:0:0:0:0:1')); + await colAsserter.ok(inet('2001:0db8:85a3:0000:0000:8a2e:0370:7334'), _ => inet('2001:db8:85a3:0:0:8a2e:370:7334')); + await colAsserter.ok(inet('2001:db8:85a3::8a2e:370:7334', 6), _ => inet('2001:db8:85a3:0:0:8a2e:370:7334')); + await colAsserter.ok(inet('168.201.203.205', 4)); + }); + + it('should handle different vector insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'vector', { + eqOn: (a: DataAPIVector) => a.asArray(), + }); + + await colAsserter.notOk('hey, wait, this ain\'t vectorize...'); + await colAsserter.notOk(vector([.5, .5, .5, .5, .5, .5])); + + await colAsserter.ok(vector([.5, .5, .5, .5, .5])); + await colAsserter.ok([.5, .5, .5, .5, .5], vector); + }); + + it('should handle different vectorize insertion cases', async (key) => { + const dummyVec = vector(Array.from({ length: 1024 }, () => .5)); + + const colAsserter = mkColumnAsserter(key, 'vector1', { + eqOn: (a: DataAPIVector) => a.length, + table: table_, + }); + + await colAsserter.notOk(vector([.5, .5, .5, .5, .5])); + + await colAsserter.ok('toto, I\'ve a feeling we\'re in vectorize again', _ => dummyVec); + await colAsserter.ok(dummyVec); + }); +}); diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index 27df2ff1..a695b58f 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -17,13 +17,12 @@ import { DataAPIBlob, DataAPIDate, DataAPIDuration, - DataAPIResponseError, DataAPITime, DataAPIVector, InetAddress, UUID, } from '@/src/documents'; -import { it, parallel, describe } from '@/tests/testlib'; +import { it, parallel } from '@/tests/testlib'; import assert from 'assert'; import BigNumber from 'bignumber.js'; @@ -135,281 +134,4 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' insertedId: { text: key, int: 0 }, }); }); - - describe('scalar inserts (group #1)', ({ db }) => { - it('should handle different text insertion cases', async () => { - await assert.rejects(() => table.insertOne({ text: '', int: 0 }), DataAPIResponseError); - - assert.deepStrictEqual( - await table.insertOne({ text: '⨳⨓⨋', int: 0 }), - { insertedId: { text: '⨳⨓⨋', int: 0 } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ text: 'a'.repeat(65535), int: 0 }), - { insertedId: { text: 'a'.repeat(65535), int: 0 } }, - ); - - await assert.rejects(() => table.insertOne({ text: 'a'.repeat(65536), int: 0 }), DataAPIResponseError); - }); - - it('should handle different different int insertion cases', async () => { - await Promise.all(['int', 'tinyint', 'smallint'].map(async (col) => { - const table = await db.createTable(`temp_${col}`, { definition: { columns: { intT: col }, primaryKey: 'intT' } }); - - await assert.rejects(() => table.insertOne({ intT: 1.1 }), DataAPIResponseError, col); - await assert.rejects(() => table.insertOne({ intT: 1e50 }), DataAPIResponseError, col); - await assert.rejects(() => table.insertOne({ intT: Infinity }), DataAPIResponseError, col); - await assert.rejects(() => table.insertOne({ intT: NaN }), DataAPIResponseError, col); - await assert.rejects(() => table.insertOne({ intT: 'Infinity' as any }), DataAPIResponseError, col); - await assert.rejects(() => table.insertOne({ intT: '123' as any }), DataAPIResponseError, col); - - assert.deepStrictEqual(await table.insertOne({ intT: +1 }), { insertedId: { intT: +1 } }, col); - assert.deepStrictEqual(await table.insertOne({ intT: -1 }), { insertedId: { intT: -1 } }, col); - - await table.drop(); - })); - }); - }); - - describe('scalar inserts (group #2)', ({ db }) => { - it('should handle different ascii insertion cases', async () => { - const table = await db.createTable('temp_ascii', { definition: { columns: { ascii: 'ascii' }, primaryKey: 'ascii' } }); - - await assert.rejects(() => table.insertOne({ ascii: '' }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ ascii: '⨳⨓⨋' }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ ascii: 'é' }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ ascii: '\x80' }), DataAPIResponseError); - - assert.deepStrictEqual( - await table.insertOne({ ascii: 'a'.repeat(65535) }), - { insertedId: { ascii: 'a'.repeat(65535) } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ascii: 'A!@#$%^&*()' }), - { insertedId: { ascii: 'A!@#$%^&*()' } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ascii: '\u0000' }), - { insertedId: { ascii: '\u0000' } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ascii: '\x7F' }), - { insertedId: { ascii: '\x7F' } }, - ); - - await table.drop(); - }); - - it('should handle different blob insertion cases', async () => { - const table = await db.createTable('temp_blob', { definition: { columns: { blob: 'blob' }, primaryKey: 'blob' } }); - const buffer = Buffer.from([0x0, 0x1]); - const base64 = buffer.toString('base64'); - - await assert.rejects(() => table.insertOne({ blob: buffer as any }), DataAPIResponseError); - - assert.deepStrictEqual( - (await table.insertOne({ blob: { $binary: base64 } as any })).insertedId.blob.asBase64(), - base64, - ); - - assert.deepStrictEqual( - (await table.insertOne({ blob: new DataAPIBlob(buffer) })).insertedId.blob.asBase64(), - base64, - ); - - await table.drop(); - }); - - it('should handle different boolean insertion cases (I mean, not that there are many lol)', async () => { - const table = await db.createTable('temp_boolean', { definition: { columns: { boolean: 'boolean' }, primaryKey: 'boolean' } }); - - await assert.rejects(() => table.insertOne({ boolean: 'true' as any }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ boolean: 1 as any }), DataAPIResponseError); - - assert.deepStrictEqual( - await table.insertOne({ boolean: true }), - { insertedId: { boolean: true } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ boolean: false }), - { insertedId: { boolean: false } }, - ); - - await table.drop(); - }); - - it('should handle different date insertion cases', async () => { - const table = await db.createTable('temp_date', { definition: { columns: { date: 'date' }, primaryKey: 'date' } }); - - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('2000-00-06') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('2000-01-00') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('2000/01/01') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('2000-01-32') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('2000-02-30') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('+2000-00-06') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('-0000-00-06') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ date: new DataAPIDate('10000-00-06') }), DataAPIResponseError); - - assert.deepStrictEqual( - (await table.insertOne({ date: '1970-01-01' as any })).insertedId.date.toString(), - '1970-01-01', - ); - - assert.deepStrictEqual( - (await table.insertOne({ date: new DataAPIDate('-0001-01-01') })).insertedId.date.toString(), - '-0001-01-01', - ); - - assert.deepStrictEqual( - (await table.insertOne({ date: new DataAPIDate('9999-12-31') })).insertedId.date.toString(), - '9999-12-31', - ); - - assert.deepStrictEqual( - (await table.insertOne({ date: new DataAPIDate('+10000-12-31') })).insertedId.date.toString(), - '+10000-12-31', - ); - - assert.deepStrictEqual( - (await table.insertOne({ date: new DataAPIDate('-10000-12-31') })).insertedId.date.toString(), - '-10000-12-31', - ); - - await table.drop(); - }); - }); - - describe('scalar inserts (group #3)', ({ db }) => { - it('should handle different decimal insertion cases', async () => { - const table = await db.createTable('temp_decimal', { definition: { columns: { decimal: 'decimal' }, primaryKey: 'decimal' } }); - - await assert.rejects(() => table.insertOne({ decimal: '123123.12312312312' as any }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ decimal: BigNumber('NaN') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ decimal: BigNumber('Infinity') }), DataAPIResponseError); - await assert.rejects(() => table.insertOne({ decimal: BigNumber('-Infinity') }), DataAPIResponseError); - - assert.deepStrictEqual( - await table.insertOne({ decimal: 123123.123 as any }), - { insertedId: { decimal: BigNumber('123123.123') } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ decimal: 123123n as any }), - { insertedId: { decimal: BigNumber('123123') } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ decimal: BigNumber('1.1212121131231231231231231231231231231231233122') }), - { insertedId: { decimal: BigNumber('1.1212121131231231231231231231231231231231233122') } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ decimal: BigNumber('-1e50') }), - { insertedId: { decimal: BigNumber('-1e50') } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ decimal: BigNumber('-1e-50') }), - { insertedId: { decimal: BigNumber('-1e-50') } }, - ); - - await table.drop(); - }); - - it('should handle different iee754 insertion cases', async () => { - await Promise.all(['float', 'double'].map(async (col) => { - const table = await db.createTable(`temp_${col}`, { definition: { columns: { ieeT: col }, primaryKey: 'ieeT' } }); - - await assert.rejects(() => table.insertOne({ ieeT: '123' as any }), DataAPIResponseError, col); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: 123123.125 }), - { insertedId: { ieeT: 123123.125 } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: 123123n }), - { insertedId: { ieeT: 123123 } }, - ); - - assert.notDeepStrictEqual( - BigNumber((await table.insertOne({ ieeT: BigNumber('1.122121312312122212121213123') })).insertedId.ieeT as number), - BigNumber('1.122121312312122212121213123'), - ); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: NaN }), - { insertedId: { ieeT: NaN } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: 'NaN' }), - { insertedId: { ieeT: NaN } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: Infinity }), - { insertedId: { ieeT: Infinity } }, - ); - - assert.deepStrictEqual( - await table.insertOne({ ieeT: -Infinity }), - { insertedId: { ieeT: -Infinity } }, - ); - - await table.drop(); - })); - }); - - // it('should handle different duration insertion cases', () => { - // const table = db.createTable('temp_duration', { definition: { columns: { duration: 'duration' }, primaryKey: 'duration' } }); - // - // }); - }); - - // describe('scalar inserts (group #3)', ({ db }) => { - // it('should handle different vector insertion cases', async () => { - // const table = await db.createTable('temp_vector', { definition: { columns: { vector: { type: 'vector', dimension: 3 } }, primaryKey: 'vector' } }); - // - // await assert.rejects(() => table.insertOne({ vector: "this ain't vectorize" as any }), DataAPIResponseError); - // - // assert.deepStrictEqual( - // (await table.insertOne({ vector: [.5, .5, .5] as any })).insertedId.vector.asArray(), - // [.5, .5, .5], - // ); - // - // assert.deepStrictEqual( - // (await table.insertOne({ vector: new DataAPIVector([.5, .5, .5]) })).insertedId.vector.asArray(), - // [.5, .5, .5], - // ); - // - // await table.drop(); - // }); - // - // it('should handle different vectorize insertion cases', async () => { - // const table = await db.createTable('temp_vectorize', { - // definition: { - // columns: { - // vector1: { type: 'vector', dimension: 1024, service: { provider: 'nvidia', modelName: 'NV-Embed-QA' } }, - // vector2: { type: 'vector', dimension: 1024, service: { provider: 'nvidia', modelName: 'NV-Embed-QA' } }, - // }, - // primaryKey: { - // partitionBy: ['vector1'], - // partitionSort: { vector2: 1 }, - // }, - // }, - // }); - // - // const inserted1 = await table.insertOne({ vector1: "would you do it with me?", vector2: "heal the scars and change the stars..." }); - // assert.strictEqual((inserted1.insertedId.vector1 as DataAPIVector).asArray().length, 1024); - // assert.strictEqual((inserted1.insertedId.vector2 as DataAPIVector).asArray().length, 1024); - // - // await table.drop(); - // }); - // }); }); From 9855d5c82c748994bde290a6238006ddbd166720 Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 3 Jan 2025 18:07:16 +0530 Subject: [PATCH 15/44] nuked InetAddress --- etc/docs/DATATYPES.md | 29 +--- src/db/types/tables/table-schema.ts | 3 +- src/documents/datatypes/index.ts | 1 - src/documents/datatypes/inet-address.ts | 163 ------------------ src/documents/tables/ser-des/codecs.ts | 2 - .../documents/tables/datatypes.test.ts | 32 ++-- .../documents/tables/find-one.test.ts | 3 +- .../documents/tables/insert-one.test.ts | 3 +- .../documents/tables/update-one.test.ts | 3 +- tests/unit/documents/datatypes/inet.test.ts | 69 -------- 10 files changed, 21 insertions(+), 287 deletions(-) delete mode 100644 src/documents/datatypes/inet-address.ts delete mode 100644 tests/unit/documents/datatypes/inet.test.ts diff --git a/etc/docs/DATATYPES.md b/etc/docs/DATATYPES.md index d49a1363..98af4101 100644 --- a/etc/docs/DATATYPES.md +++ b/etc/docs/DATATYPES.md @@ -19,7 +19,6 @@ Some types are strictly meant for tables; others for collections. And a couple, - [Blobs](#blobs) - [Collections](#collections-1) - [Dates/Times](#dates--times) - - [InetAddresses](#inetaddresses) - [UUIDs](#uuids-1) - [Vectors](#vectors-1) - [Inserting native representations](#inserting-native-representations) @@ -349,30 +348,6 @@ From each custom class, you can generally: - Get the date/time as a `Date` object using `.toDate()` - Get the individual components of the date/time using `.components()` -### InetAddresses - -You can use inets in collections using the `InetAddress` class (or the `inet` shorthand). - -```typescript -import { InetAddress, inet } from '@datastax/astra-db-ts'; - -await table.insertOne({ - inet: inet('::1'), // Equivalent to `new InetAddress('::1')` -}); - -const row = await table.findOne(); -console.log(row.inet instanceof InetAddress); // true -``` - -You can create a `InetAddress` through the class, or through the `inet` shorthand, in a few different ways: -1. By passing the inet string to `new InetAddress()` or `inet()`, and having the version be inferred -2. By passing the inet string to `new InetAddress()` or `inet()`, and specifying the version explicitly (validating as that version) - - e.g. `inet('::1', 6)` - -From the `InetAddress` class, you can either: -- Get the string representation of the `InetAddress` using `.toString()` -- Get the version of the `InetAddress` using `.version` - ### UUIDs You can use UUIDs in collections using the `UUID` class (or the `uuid` shorthand). Make sure you're importing this from `'@datastax/astra-db-ts'`, and _not_ from `'uuid'` or `'bson'`. @@ -493,7 +468,7 @@ If you really want to change the behavior of how a certain type is deserialized, | `double` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | | `duration` | `DataAPIDuration` | `duration` | `new DataAPIDuration('3w')`, `duration('P5DT30M')` | | `float` | `number` | - | `3.14`, `NaN`, `Infinity`, `-Infinity` | -| `inet`. | `InetAddress` | `inet` | `new InetAddress('::1')`, `inet('127.0.0.1')` | +| `inet`. | `string` | - | `'::1'`, `'127.0.0.1'`, `'localhost'` | | `int` | `number` | - | `42` | | `list` | `Array` | - | `['value']` | | `map` | `Map` | - | `new Map([['key', 'value']])` | @@ -502,7 +477,7 @@ If you really want to change the behavior of how a certain type is deserialized, | `text` | `string` | - | `'Hello!'` | | `time` | `DataAPITime` | `time` | `new DataAPITime()`, `time(new Date(1734070574056))`, `time('12:34:56')`, `time()` | | `timestamp` | `Date` | - | `new Date()`, `new Date(1734070574056)`, `new Date('...')` | -| `timeuuid` | `UUID` | `timeuuid` | `new UUID('...')`, `UUID.v1()`, `uuid('...')`, `uuid(1)` | +| `timeuuid` | `UUID` | `uuid` | `new UUID('...')`, `UUID.v1()`, `uuid('...')`, `uuid(1)` | | `tinyint` | `number` | - | `42` | | `uuid` | `UUID` | `uuid` | `new UUID('...')`, `UUID.v4()`, `uuid('...')`, `uuid(7)` | | `varchar` | `string` | - | `'Hello!'` | diff --git a/src/db/types/tables/table-schema.ts b/src/db/types/tables/table-schema.ts index 083c109d..4e38be77 100644 --- a/src/db/types/tables/table-schema.ts +++ b/src/db/types/tables/table-schema.ts @@ -26,7 +26,6 @@ import { DataAPIDuration, DataAPITime, FoundRow, - InetAddress, SomeRow, UUID, } from '@/src/documents'; @@ -264,7 +263,7 @@ interface CqlNonGenericType2TSTypeDict { duration: DataAPIDuration | null, float: number | null, int: number | null, - inet: InetAddress | null, + inet: string | null, smallint: number | null, text: string | null; time: DataAPITime | null, diff --git a/src/documents/datatypes/index.ts b/src/documents/datatypes/index.ts index 64bc8fd4..2098d19f 100644 --- a/src/documents/datatypes/index.ts +++ b/src/documents/datatypes/index.ts @@ -14,7 +14,6 @@ export * from './blob'; export * from './dates'; -export * from './inet-address'; export * from './object-id'; export * from './uuid'; export * from './vector'; diff --git a/src/documents/datatypes/inet-address.ts b/src/documents/datatypes/inet-address.ts deleted file mode 100644 index 0efc9feb..00000000 --- a/src/documents/datatypes/inet-address.ts +++ /dev/null @@ -1,163 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { $CustomInspect } from '@/src/lib/constants'; -import { nullish } from '@/src/lib'; -import { TableCodec, TableSerCtx, TableDesCtx } from '@/src/documents'; -import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; - -/** - * A shorthand function for `new InetAddress(addr, version?)` - * - * @public - */ -export const inet = (address: string, version?: 4 | 6) => new InetAddress(address, version); - -/** - * Represents an `inet` column for Data API tables. - * - * You may use the {@link inet} function as a shorthand for creating a new `InetAddress`. - * - * See the official DataStax documentation for more information. - * - * @public - */ -export class InetAddress implements TableCodec { - private readonly _raw!: string; - private _version!: 4 | 6 | nullish; - - /** - * Implementation of `$SerializeForTable` for {@link TableCodec} - */ - public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this._raw); - }; - - /** - * Implementation of `$DeserializeForTable` for {@link TableCodec} - */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new InetAddress(value, null, false)); - } - - /** - * Creates a new `InetAddress` instance from a vector-like value. - * - * If you pass a `version`, the value will be validated as an IPv4 or IPv6 address; otherwise, it'll be validated as - * either, and the version will be inferred from the value. - * - * You can set `validate` to `false` to bypass any validation if you're confident the value is a valid inet address. - * - * @param address - The address to create the `InetAddress` from - * @param version - The IP version to validate the address as - * @param validate - Whether to actually validate the address - * - * @throws TypeError If the address is not a valid IPv4 or IPv6 address - */ - public constructor(address: string, version?: 4 | 6 | null, validate = true) { // ::1 => 0:0:0:0:0:0:0:1 - if (validate) { - switch (version) { - case 4: - if (!isIPv4(address)) { - throw new Error(`'${address}' is not a valid IPv4 address`); - } - break; - case 6: - if (!isIPv6(address)) { - throw new Error(`'${address}' is not a valid IPv6 address`); - } - break; - default: - if (!(version = isIPv4(address) ? 4 : isIPv6(address) ? 6 : null)) { - throw new Error(`'${address}' is not a valid IPv4 or IPv6 address`); - } - } - } - - Object.defineProperty(this, '_raw', { - value: address.toLowerCase(), - }); - - Object.defineProperty(this, '_version', { - value: version, - writable: true, - }); - - Object.defineProperty(this, $CustomInspect, { - value: () => `InetAddress<${this.version}>("${this._raw}")`, - }); - } - - /** - * Returns the IP version of the inet address. - * - * @returns The IP version of the inet address - */ - public get version(): 4 | 6 { - if (!this._version) { - this._version = isIPv4(this._raw) ? 4 : 6; - } - return this._version; - } - - /** - * Returns the string representation of the inet address. - * - * @returns The string representation of the inet address - */ - public toString(): string { - return this._raw; - } -} - -const IPv4Lengths = { max: 15, min: 7 }; -const IPv6Lengths = { max: 45, min: 2 }; - -// ===================================================================================================================== -// From https://github.com/sindresorhus/ip-regex/blob/main/index.js -// Was getting errors trying to import it while as a dependency, so just decided not to deal with it for the time being -const v4 = '(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)(?:\\.(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]\\d|\\d)){3}'; - -const v6segment = '[a-fA-F\\d]{1,4}'; - -const v6 = ` -(?: -(?:${v6segment}:){7}(?:${v6segment}|:)| // 1:2:3:4:5:6:7:: 1:2:3:4:5:6:7:8 -(?:${v6segment}:){6}(?:${v4}|:${v6segment}|:)| // 1:2:3:4:5:6:: 1:2:3:4:5:6::8 1:2:3:4:5:6::8 1:2:3:4:5:6::1.2.3.4 -(?:${v6segment}:){5}(?::${v4}|(?::${v6segment}){1,2}|:)| // 1:2:3:4:5:: 1:2:3:4:5::7:8 1:2:3:4:5::8 1:2:3:4:5::7:1.2.3.4 -(?:${v6segment}:){4}(?:(?::${v6segment}){0,1}:${v4}|(?::${v6segment}){1,3}|:)| // 1:2:3:4:: 1:2:3:4::6:7:8 1:2:3:4::8 1:2:3:4::6:7:1.2.3.4 -(?:${v6segment}:){3}(?:(?::${v6segment}){0,2}:${v4}|(?::${v6segment}){1,4}|:)| // 1:2:3:: 1:2:3::5:6:7:8 1:2:3::8 1:2:3::5:6:7:1.2.3.4 -(?:${v6segment}:){2}(?:(?::${v6segment}){0,3}:${v4}|(?::${v6segment}){1,5}|:)| // 1:2:: 1:2::4:5:6:7:8 1:2::8 1:2::4:5:6:7:1.2.3.4 -(?:${v6segment}:){1}(?:(?::${v6segment}){0,4}:${v4}|(?::${v6segment}){1,6}|:)| // 1:: 1::3:4:5:6:7:8 1::8 1::3:4:5:6:7:1.2.3.4 -(?::(?:(?::${v6segment}){0,5}:${v4}|(?::${v6segment}){1,7}|:)) // ::2:3:4:5:6:7:8 ::2:3:4:5:6:7:8 ::8 ::1.2.3.4 -)(?:%[0-9a-zA-Z]{1,})? // %eth0 %1 -`.replace(/\s*\/\/.*$/gm, '').replace(/\n/g, '').trim(); - -const IPv4Regex = new RegExp(`^${v4}$`); -const IPv6Regex = new RegExp(`^${v6}$`); -// ===================================================================================================================== - -function isIPv6(raw: string) { - if (raw.length < IPv6Lengths.min || IPv6Lengths.max < raw.length) { - return false; - } - return IPv6Regex.test(raw); -} - -function isIPv4(raw: string) { - if (raw.length < IPv4Lengths.min || IPv4Lengths.max < raw.length) { - return false; - } - return IPv4Regex.test(raw); -} diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index 0cbffd5a..a5ecd1b0 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -15,7 +15,6 @@ // Important to import from specific paths here to avoid circular dependencies import { DataAPIBlob } from '@/src/documents/datatypes/blob'; import { DataAPIDate, DataAPIDuration, DataAPITime } from '@/src/documents/datatypes/dates'; -import { InetAddress } from '@/src/documents/datatypes/inet-address'; import { UUID } from '@/src/documents/datatypes/uuid'; import { DataAPIVector } from '@/src/documents/datatypes/vector'; import { SomeDoc, TableDesCtx, TableSerCtx } from '@/src/documents'; @@ -68,7 +67,6 @@ export class TableCodecs { float: TableCodecs.forType('float', { deserialize: (_, value, ctx) => ctx.done(parseFloat(value)), }), - inet: TableCodecs.forType('inet', InetAddress), time: TableCodecs.forType('time', DataAPITime), timestamp: TableCodecs.forType('timestamp', { serializeClass: Date, diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts index 77957160..34260866 100644 --- a/tests/integration/documents/tables/datatypes.test.ts +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -19,8 +19,8 @@ import { DataAPIResponseError, DataAPIVector, date, - inet, - InetAddress, SomeRow, Table, + SomeRow, + Table, vector, } from '@/src/documents'; import { it, parallel } from '@/tests/testlib'; @@ -186,27 +186,25 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { })); it('should handle different inet insertion cases', async (key) => { - const colAsserter = mkColumnAsserter(key, 'inet', { - eqOn: (a: InetAddress) => a.toString(), - }); + const colAsserter = mkColumnAsserter(key, 'inet'); await colAsserter.notOk('127.0.0.1/16'); await colAsserter.notOk('127.0.0.1:80'); await colAsserter.notOk('6f4e:1900:4545:3:200:f6ff:fe21:645cf'); await colAsserter.notOk('10.10.10.1000'); - await colAsserter.ok('::ffff:192.168.0.1', _ => inet('192.168.0.1')); - await colAsserter.ok('127.1', _ => inet('127.0.0.1')); - await colAsserter.ok('127.0.1', _ => inet('127.0.0.1')); - await colAsserter.ok('localhost', _ => inet('127.0.0.1')); - await colAsserter.ok('192.168.36095', _ => inet('192.168.140.255')); - await colAsserter.ok('192.11046143', _ => inet('192.168.140.255')); - - await colAsserter.ok('127.0.0.1', inet); - await colAsserter.ok('::1', _ => inet('0:0:0:0:0:0:0:1')); - await colAsserter.ok(inet('2001:0db8:85a3:0000:0000:8a2e:0370:7334'), _ => inet('2001:db8:85a3:0:0:8a2e:370:7334')); - await colAsserter.ok(inet('2001:db8:85a3::8a2e:370:7334', 6), _ => inet('2001:db8:85a3:0:0:8a2e:370:7334')); - await colAsserter.ok(inet('168.201.203.205', 4)); + await colAsserter.ok('::ffff:192.168.0.1', _ => '192.168.0.1'); + await colAsserter.ok('127.1', _ => '127.0.0.1'); + await colAsserter.ok('127.0.1', _ => '127.0.0.1'); + await colAsserter.ok('localhost', _ => '127.0.0.1'); + await colAsserter.ok('192.168.36095', _ => '192.168.140.255'); + await colAsserter.ok('192.11046143', _ => '192.168.140.255'); + + await colAsserter.ok('127.0.0.1'); + await colAsserter.ok('::1', _ => '0:0:0:0:0:0:0:1'); + await colAsserter.ok('2001:0db8:85a3:0000:0000:8a2e:0370:7334', _ => '2001:db8:85a3:0:0:8a2e:370:7334'); + await colAsserter.ok('2001:db8:85a3::8a2e:370:7334', _ => '2001:db8:85a3:0:0:8a2e:370:7334'); + await colAsserter.ok('168.201.203.205'); }); it('should handle different vector insertion cases', async (key) => { diff --git a/tests/integration/documents/tables/find-one.test.ts b/tests/integration/documents/tables/find-one.test.ts index 75f0e383..f0d2c2e8 100644 --- a/tests/integration/documents/tables/find-one.test.ts +++ b/tests/integration/documents/tables/find-one.test.ts @@ -19,7 +19,6 @@ import { DataAPIDuration, DataAPITime, DataAPIVector, - InetAddress, UUID, } from '@/src/documents'; import { EverythingTableSchema, it, parallel } from '@/tests/testlib'; @@ -84,7 +83,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr double: 123.456, duration: new DataAPIDuration('P1D'), float: 123.456, - inet: new InetAddress('0:0:0:0:0:0:0:1'), + inet: '0:0:0:0:0:0:0:1', list: [uuid, uuid], set: new Set([uuid, uuid, uuid]), smallint: 123, diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index a695b58f..ac7c9cf3 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -19,7 +19,6 @@ import { DataAPIDuration, DataAPITime, DataAPIVector, - InetAddress, UUID, } from '@/src/documents'; import { it, parallel } from '@/tests/testlib'; @@ -52,7 +51,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' double: 123.456, duration: new DataAPIDuration('1d'), float: 123.456, - inet: new InetAddress('::1'), + inet: '::1', list: [UUID.v4(), UUID.v7()], set: new Set([UUID.v4(), UUID.v7(), UUID.v7()]), smallint: 123, diff --git a/tests/integration/documents/tables/update-one.test.ts b/tests/integration/documents/tables/update-one.test.ts index 08267292..7e3c7483 100644 --- a/tests/integration/documents/tables/update-one.test.ts +++ b/tests/integration/documents/tables/update-one.test.ts @@ -20,7 +20,6 @@ import { DataAPIResponseError, DataAPITime, DataAPIVector, - InetAddress, UUID, } from '@/src/documents'; import { EverythingTableSchema, it, parallel } from '@/tests/testlib'; @@ -68,7 +67,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } double: 123.456, duration: new DataAPIDuration('P1D'), float: 123.456, - inet: new InetAddress('0:0:0:0:0:0:0:1'), + inet: '0:0:0:0:0:0:0:1', list: [uuid, uuid], set: new Set([uuid, uuid, uuid]), smallint: 123, diff --git a/tests/unit/documents/datatypes/inet.test.ts b/tests/unit/documents/datatypes/inet.test.ts deleted file mode 100644 index d9b748d2..00000000 --- a/tests/unit/documents/datatypes/inet.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// noinspection DuplicatedCode - -import assert from 'assert'; -import { InetAddress } from '@/src/documents'; -import { describe, it } from '@/tests/testlib'; - -const IPV4 = '127.0.0.1'; -const IPV6 = '2001:0db8:85a3:0000:0000:8a2e:0370:7334'; - -describe('unit.documents.datatypes.inet', () => { - describe('construction', () => { - it('should properly construct an IPv4 address', () => { - const explicit = new InetAddress(IPV4, 4); - assert.strictEqual(explicit.toString(), IPV4); - assert.strictEqual(explicit.version, 4); - - const implicit = new InetAddress(IPV4); - assert.strictEqual(implicit.toString(), IPV4); - assert.strictEqual(implicit.version, 4); - }); - - it('should properly construct an IPv6 address', () => { - const explicit = new InetAddress(IPV6, 6); - assert.strictEqual(explicit.toString(), IPV6); - assert.strictEqual(explicit.version, 6); - - const implicit = new InetAddress(IPV6); - assert.strictEqual(implicit.toString(), IPV6); - assert.strictEqual(implicit.version, 6); - }); - }); - - describe('validation', () => { - it('should error on invalid IPv4', () => { - assert.throws(() => new InetAddress(IPV6, 4)); - }); - - it('should error on invalid IPv6', () => { - assert.throws(() => new InetAddress(IPV4, 6)); - }); - - it('should error on invalid IP', () => { - assert.throws(() => new InetAddress('i like dogs')); - }); - - it('should error on invalid type', () => { - assert.throws(() => new InetAddress({} as any), Error); - }); - - it('should allow force creation of invalid values', () => { - assert.strictEqual(new InetAddress('abc', 4, false).version, 4); - assert.strictEqual(new InetAddress(IPV6, 4, false).version, 4); - assert.throws(() => new InetAddress({} as any, 4, false).version, TypeError); - }); - }); -}); From 56ee344a54794ab159b469289a3b444e3cf986d5 Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 3 Jan 2025 22:10:01 +0530 Subject: [PATCH 16/44] magic, have no clue but it works --- {src/documents => tests/typing}/tables/test.ts | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {src/documents => tests/typing}/tables/test.ts (100%) diff --git a/src/documents/tables/test.ts b/tests/typing/tables/test.ts similarity index 100% rename from src/documents/tables/test.ts rename to tests/typing/tables/test.ts From eee492e603e0cf40d7c8fc7d96a3792c4ef6c4f4 Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 3 Jan 2025 22:15:49 +0530 Subject: [PATCH 17/44] fixed a test --- .../unit/documents/collections/ser-des/key-transformer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/documents/collections/ser-des/key-transformer.test.ts b/tests/unit/documents/collections/ser-des/key-transformer.test.ts index bea57a58..6a65e7b2 100644 --- a/tests/unit/documents/collections/ser-des/key-transformer.test.ts +++ b/tests/unit/documents/collections/ser-des/key-transformer.test.ts @@ -20,7 +20,7 @@ import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; describe('unit.documents.collections.ser-des.key-transformer', () => { describe('Camel2SnakeCase', () => { - const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase() }); + const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase(), enableBigNumbers: true }); it('should serialize top-level keys to snake_case for collections', () => { const [obj] = serdes.serialize({ From 9b7e28198e73cb444fd2c514815e11cc12a06ee8 Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 4 Jan 2025 17:58:28 +0530 Subject: [PATCH 18/44] fix vite example issue thing --- examples/browser/src/vite-env.d.ts | 1 + 1 file changed, 1 insertion(+) create mode 100644 examples/browser/src/vite-env.d.ts diff --git a/examples/browser/src/vite-env.d.ts b/examples/browser/src/vite-env.d.ts new file mode 100644 index 00000000..ee4abca4 --- /dev/null +++ b/examples/browser/src/vite-env.d.ts @@ -0,0 +1 @@ +/// g \ No newline at end of file From a8dfc4fec7ea39ad2537bcb7d3b72355691486df Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 4 Jan 2025 18:35:21 +0530 Subject: [PATCH 19/44] check for broken compilation when skipLibCheck: false w/ astra-db-ts as dep --- .gitignore | 1 + etc/astra-db-ts.api.md | 17 +-------------- scripts/check.sh | 48 +++++++++++++++++++++++++++++++++++++----- 3 files changed, 45 insertions(+), 21 deletions(-) diff --git a/.gitignore b/.gitignore index 909b1531..f4dda5f1 100644 --- a/.gitignore +++ b/.gitignore @@ -140,5 +140,6 @@ tsdoc-metadata.json vectorize_test_spec.json etc/test-reports/ etc/playgrounds/ +tmp-lib-check .direnv diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index 817eed80..05d809ae 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -1465,18 +1465,6 @@ export type IdOf = Doc extends { _id?: infer Id extends SomeId; } ? Id : SomeId; -// @public -export const inet: (address: string, version?: 4 | 6) => InetAddress; - -// @public -export class InetAddress implements TableCodec { - static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (InetAddress | undefined)?]; - [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; - constructor(address: string, version?: 4 | 6 | null, validate?: boolean); - toString(): string; - get version(): 4 | 6; -} - // @public export type InferrableTable = CreateTableDefinition | ((..._: any[]) => Promise>) | ((..._: any[]) => Table) | Promise> | Table; @@ -1792,18 +1780,15 @@ export class TableCodecs { static Defaults: { bigint: RawCodec; blob: RawCodec; + counter: RawCodec; date: RawCodec; decimal: RawCodec; double: RawCodec; duration: RawCodec; float: RawCodec; - int: RawCodec; - inet: RawCodec; - smallint: RawCodec; time: RawCodec; timestamp: RawCodec; timeuuid: RawCodec; - tinyint: RawCodec; uuid: RawCodec; vector: RawCodec; varint: RawCodec; diff --git a/scripts/check.sh b/scripts/check.sh index bc513f85..0604258e 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -1,5 +1,7 @@ #!/usr/bin/env sh +main_dir=$(pwd) + while [ $# -gt 0 ]; do case "$1" in "tc") @@ -11,6 +13,9 @@ while [ $# -gt 0 ]; do "licensing") check_types="$check_types licensing" ;; + "lib-check") + check_types="$check_types lib-check" + ;; *) if [ "$1" != "--help" ] && [ "$1" != "-help" ] && [ "$1" != "-h" ]; then echo "Invalid flag $1" @@ -18,11 +23,12 @@ while [ $# -gt 0 ]; do fi echo "Usage:" echo - echo "$0 [tc] [lint] [licensing]" + echo "$0 [tc] [lint] [licensing] [lib-check]" echo echo "* tc: runs the type-checker" echo "* lint: checks for linting errors" echo "* licensing: checks for missing licensing headers" + echo "* lib-check: ensures library compiles if skipLibCheck: false" echo echo "Defaults to running all checks if no specific checks are specified." exit @@ -31,22 +37,47 @@ while [ $# -gt 0 ]; do done if [ -z "$check_types" ]; then - check_types="tc lint licensing" + check_types="tc lint licensing lib-check" fi +failed=false + for check_type in $check_types; do case $check_type in "tc") echo "Running type-checker..." - npx tsc --noEmit --skipLibCheck || exit 10 + npx tsc --noEmit || failed=true ;; "lint") echo "Running linter..." - npm run lint -- --no-warn-ignored || exit 20 + npm run lint -- --no-warn-ignored || failed=true ;; "licensing") echo "Checking for missing licensing headers..." - find tests/ src/ -type f -exec grep -L "^// Copyright DataStax, Inc." {} + || exit 30 + offenders=$(find tests/ src/ -type f -exec grep -L "^// Copyright DataStax, Inc." {} +) + + if [ -n "$offenders" ]; then + echo "The following files are missing licensing headers:" + echo "$offenders" + failed=true + fi + ;; + "lib-check") + echo "Checking library compiles..." + + tmp_dir="tmp-lib-check" + rm -rf "$tmp_dir" + + (scripts/build.sh > /dev/null \ + && mkdir "$tmp_dir" \ + && cd "$tmp_dir" \ + && npm init -y \ + && npm install typescript "$main_dir" \ + && echo "import '@datastax/astra-db-ts'" > src.ts \ + && npx tsc --init --skipLibCheck false --typeRoots "./node_modules/**" \ + && npx tsc) || failed=true + + cd "$main_dir" && rm -rf "$tmp_dir" ;; "*") echo "Invalid check type '$check_type'" @@ -54,3 +85,10 @@ for check_type in $check_types; do ;; esac done + +if [ "$failed" = true ]; then + echo "Checks failed" + exit 1 +else + echo "Checks passed" +fi From 37d7dbd3ad8428eb3cb573edd6b58fe1b64262d7 Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 4 Jan 2025 19:04:18 +0530 Subject: [PATCH 20/44] work towards not failing on skipLibCheck: false --- etc/astra-db-ts.api.md | 30 +++++++----------- scripts/check.sh | 2 +- src/administration/astra-admin.ts | 2 +- src/administration/astra-db-admin.ts | 4 +-- src/administration/data-api-db-admin.ts | 2 +- src/administration/errors.ts | 3 ++ src/client/data-api-client.ts | 31 ------------------- src/db/db.ts | 2 +- src/documents/collections/collection.ts | 2 +- src/documents/cursor.ts | 9 ++++-- src/documents/datatypes/blob.ts | 6 ++-- src/documents/errors.ts | 3 ++ src/documents/tables/table.ts | 2 +- src/lib/api/clients/data-api-http-client.ts | 6 ++++ src/lib/api/clients/devops-api-http-client.ts | 9 ++++++ src/lib/logging/types.ts | 2 +- .../client/data-api-client.test.ts | 19 ------------ tsconfig.json | 1 - 18 files changed, 52 insertions(+), 83 deletions(-) diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index 05d809ae..8ddbcb76 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -139,10 +139,8 @@ export class AstraAdmin { dbAdmin(id: string, region: string, options?: DbOptions): AstraDbAdmin; dbInfo(id: string, options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; dropDatabase(db: Db | string, options?: AstraDropDatabaseOptions): Promise; - // Warning: (ae-forgotten-export) The symbol "DevOpsAPIHttpClient" needs to be exported by the entry point index.d.ts - // // (undocumented) - get _httpClient(): DevOpsAPIHttpClient; + get _httpClient(): unknown; listDatabases(options?: ListAstraDatabasesOptions): Promise; } @@ -172,7 +170,7 @@ export class AstraDbAdmin extends DbAdmin { dropKeyspace(keyspace: string, options?: AstraDropKeyspaceOptions): Promise; findEmbeddingProviders(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; // (undocumented) - get _httpClient(): DevOpsAPIHttpClient; + get _httpClient(): unknown; get id(): string; info(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; listKeyspaces(options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; @@ -409,7 +407,7 @@ export class Collection(filter: CollectionFilter, options?: CollectionFindOneAndDeleteOptions): Promise; findOneAndReplace(filter: CollectionFilter, replacement: NoId, options?: CollectionFindOneAndReplaceOptions): Promise; findOneAndUpdate(filter: CollectionFilter, update: CollectionUpdateFilter, options?: CollectionFindOneAndUpdateOptions): Promise; - get _httpClient(): DataAPIHttpClient<"normal">; + get _httpClient(): unknown; insertMany(documents: readonly MaybeId[], options?: CollectionInsertManyOptions): Promise>; insertOne(document: MaybeId, options?: WithTimeout<'generalMethodTimeoutMs'>): Promise>; readonly keyspace: string; @@ -796,7 +794,8 @@ export class DataAPIBlob implements TableCodec { constructor(blob: DataAPIBlobLike, validate?: boolean); asArrayBuffer(): ArrayBuffer; asBase64(): string; - asBuffer(): Buffer; + // Warning: (ae-forgotten-export) The symbol "MaybeBuffer" needs to be exported by the entry point index.d.ts + asBuffer(): MaybeBuffer; get byteLength(): number; static isBlobLike(value: unknown): value is DataAPIBlobLike; raw(): Exclude; @@ -804,13 +803,12 @@ export class DataAPIBlob implements TableCodec { } // @public -export type DataAPIBlobLike = DataAPIBlob | ArrayBuffer | Buffer | { +export type DataAPIBlobLike = DataAPIBlob | ArrayBuffer | MaybeBuffer | { $binary: string; }; // @public export class DataAPIClient extends DataAPIClientEventEmitterBase { - [Symbol.asyncDispose]: () => Promise; constructor(options?: DataAPIClientOptions | nullish); constructor(token: string | TokenProvider | nullish, options?: DataAPIClientOptions | nullish); admin(options?: AdminOptions): AstraAdmin; @@ -879,7 +877,7 @@ export class DataAPIDbAdmin extends DbAdmin { dropKeyspace(keyspace: string, options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; findEmbeddingProviders(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; // (undocumented) - get _httpClient(): DataAPIHttpClient<"admin">; + get _httpClient(): unknown; listKeyspaces(options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; } @@ -938,8 +936,6 @@ export type DataAPIHttpOptions = DefaultHttpClientOptions | FetchHttpClientOptio // @public export type DataAPILoggingConfig = DataAPILoggingEvent | readonly (DataAPILoggingEvent | DataAPIExplicitLoggingConfig)[]; -// Warning: (ae-incompatible-release-tags) The symbol "DataAPILoggingDefaults" is marked as @public, but its signature references "NormalizedLoggingConfig" which is marked as @internal -// // @public export const DataAPILoggingDefaults: NormalizedLoggingConfig[]; @@ -984,7 +980,7 @@ export class DataAPITimeoutError extends DataAPIError { // // @internal constructor(info: HTTPRequestInfo, types: TimedOutCategories); - // (undocumented) + // @internal (undocumented) static mk(info: HTTPRequestInfo, types: TimedOutCategories): DataAPITimeoutError; // (undocumented) readonly timedOutTypes: TimedOutCategories; @@ -1034,7 +1030,7 @@ export class Db { dropTable(name: string, options?: DropTableOptions): Promise; dropTableIndex(name: string, options?: TableDropIndexOptions): Promise; // (undocumented) - get _httpClient(): DataAPIHttpClient<"normal">; + get _httpClient(): unknown; get id(): string; info(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; get keyspace(): string; @@ -1117,7 +1113,7 @@ export class DevOpsAPIResponseError extends DevOpsAPIError { export class DevOpsAPITimeoutError extends DevOpsAPIError { // @internal constructor(info: HTTPRequestInfo, types: TimedOutCategories); - // (undocumented) + // @internal (undocumented) static mk(info: HTTPRequestInfo, types: TimedOutCategories): DevOpsAPITimeoutError; // (undocumented) readonly timedOutTypes: TimedOutCategories; @@ -1607,9 +1603,7 @@ export type Normalize = { [K in keyof T]: T[K]; } & EmptyObj; -// Warning: (ae-internal-missing-underscore) The name "NormalizedLoggingConfig" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal (undocumented) +// @public (undocumented) export interface NormalizedLoggingConfig { // (undocumented) emits: readonly DataAPILoggingOutput[]; @@ -1753,7 +1747,7 @@ export class Table | null>; findOne>(filter: TableFilter, options: TableFindOneOptions): Promise; - get _httpClient(): DataAPIHttpClient<"normal">; + get _httpClient(): unknown; insertMany(rows: readonly WSchema[], options?: TableInsertManyOptions): Promise>; insertOne(row: WSchema, timeout?: WithTimeout<'generalMethodTimeoutMs'>): Promise>; readonly keyspace: string; diff --git a/scripts/check.sh b/scripts/check.sh index 0604258e..ce6c8c28 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -68,7 +68,7 @@ for check_type in $check_types; do tmp_dir="tmp-lib-check" rm -rf "$tmp_dir" - (scripts/build.sh > /dev/null \ + (scripts/build.sh \ && mkdir "$tmp_dir" \ && cd "$tmp_dir" \ && npm init -y \ diff --git a/src/administration/astra-admin.ts b/src/administration/astra-admin.ts index ddb54ea8..8b18a246 100644 --- a/src/administration/astra-admin.ts +++ b/src/administration/astra-admin.ts @@ -480,7 +480,7 @@ export class AstraAdmin { }); } - public get _httpClient() { + public get _httpClient(): unknown { return this.#httpClient; } } diff --git a/src/administration/astra-db-admin.ts b/src/administration/astra-db-admin.ts index 01996a02..a85bb4e9 100644 --- a/src/administration/astra-db-admin.ts +++ b/src/administration/astra-db-admin.ts @@ -94,7 +94,7 @@ export class AstraDbAdmin extends DbAdmin { timeoutDefaults: Timeouts.merge(rootOpts.adminOptions.timeoutDefaults, adminOpts?.timeoutDefaults), }); - this.#dataApiHttpClient = db._httpClient.forDbAdmin(adminOpts); + this.#dataApiHttpClient = (db._httpClient as DataAPIHttpClient).forDbAdmin(adminOpts); this.#db = db; Object.defineProperty(this, $CustomInspect, { @@ -335,7 +335,7 @@ export class AstraDbAdmin extends DbAdmin { }); } - public get _httpClient() { + public get _httpClient(): unknown { return this.#httpClient; } diff --git a/src/administration/data-api-db-admin.ts b/src/administration/data-api-db-admin.ts index 1e4b89a0..08ce2dca 100644 --- a/src/administration/data-api-db-admin.ts +++ b/src/administration/data-api-db-admin.ts @@ -223,7 +223,7 @@ export class DataAPIDbAdmin extends DbAdmin { }); } - public get _httpClient() { + public get _httpClient(): unknown { return this.#httpClient; } } diff --git a/src/administration/errors.ts b/src/administration/errors.ts index 4b43c2a6..85d00a89 100644 --- a/src/administration/errors.ts +++ b/src/administration/errors.ts @@ -83,6 +83,9 @@ export class DevOpsAPITimeoutError extends DevOpsAPIError { this.name = 'DevOpsAPITimeoutError'; } + /** + * @internal + */ public static mk(info: HTTPRequestInfo, types: TimedOutCategories): DevOpsAPITimeoutError { return new DevOpsAPITimeoutError(info, types); } diff --git a/src/client/data-api-client.ts b/src/client/data-api-client.ts index 4183dc6f..087e0018 100644 --- a/src/client/data-api-client.ts +++ b/src/client/data-api-client.ts @@ -176,10 +176,6 @@ export class DataAPIClient extends DataAPIClientEventEmitterBase { userAgent: buildUserAgent(options?.caller), }; - if (Symbol.asyncDispose) { - this[Symbol.asyncDispose] = () => this.close(); - } - Object.defineProperty(this, $CustomInspect, { value: () => `DataAPIClient(env="${this.#options.environment}")`, }); @@ -280,33 +276,6 @@ export class DataAPIClient extends DataAPIClientEventEmitterBase { await this.#options.fetchCtx.ctx.close?.(); this.#options.fetchCtx.closed.ref = true; } - - /** - * Allows for the `await using` syntax (if your typescript version \>= 5.2) to automatically close the client when - * it's out of scope. - * - * Equivalent to wrapping the client usage in a `try`/`finally` block and calling `client.close()` in the `finally` - * block. - * - * @example - * ```typescript - * async function main() { - *   // Will unconditionally close the client when the function exits - *   await using client = new DataAPIClient('*TOKEN*'); - * - *   // Using the client as normal - *   const db = client.db('*ENDPOINT*'); - *   console.log(await db.listCollections()); - * - *   // Or pass it to another function to run your application - *   app(client); - * } - * main(); - * ``` - * - * *This will only be defined if the `Symbol.asyncDispose` symbol is actually defined.* - */ - public [Symbol.asyncDispose]!: () => Promise; } const buildFetchCtx = (options: DataAPIClientOptions | undefined): FetchCtx => { diff --git a/src/db/db.ts b/src/db/db.ts index 370e33b9..55040a10 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -1214,7 +1214,7 @@ export class Db { }); } - public get _httpClient() { + public get _httpClient(): unknown { return this.#httpClient; } } diff --git a/src/documents/collections/collection.ts b/src/documents/collections/collection.ts index f422949d..29c03540 100644 --- a/src/documents/collections/collection.ts +++ b/src/documents/collections/collection.ts @@ -1507,7 +1507,7 @@ export class Collection { readonly #parent: Table | Collection; + readonly #httpClient: DataAPIHttpClient; readonly #serdes: SerDes; readonly #options: GenericFindOptions; @@ -152,6 +154,7 @@ export abstract class FindCursor { */ constructor(parent: Table | Collection, serdes: SerDes, filter: [Filter, boolean], options?: GenericFindOptions, mapping?: (doc: TRaw) => T) { this.#parent = parent; + this.#httpClient = parent._httpClient as DataAPIHttpClient; this.#serdes = serdes; this.#filter = filter; this.#options = options ?? {}; @@ -576,7 +579,7 @@ export abstract class FindCursor { } const docs: T[] = []; - const tm = this.#parent._httpClient.tm.multipart('generalMethodTimeoutMs', this.#options); + const tm = this.#httpClient.tm.multipart('generalMethodTimeoutMs', this.#options); try { let doc: T | null; @@ -651,8 +654,8 @@ export abstract class FindCursor { }, }; - const raw = await this.#parent._httpClient.executeCommand(command, { - timeoutManager: tm ?? this.#parent._httpClient.tm.single('generalMethodTimeoutMs', this.#options), + const raw = await this.#httpClient.executeCommand(command, { + timeoutManager: tm ?? this.#httpClient.tm.single('generalMethodTimeoutMs', this.#options), bigNumsPresent: this.#filter[1], }); diff --git a/src/documents/datatypes/blob.ts b/src/documents/datatypes/blob.ts index 56eb7e6c..b93f48f9 100644 --- a/src/documents/datatypes/blob.ts +++ b/src/documents/datatypes/blob.ts @@ -17,12 +17,14 @@ import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; import { forJSEnv } from '@/src/lib/utils'; +type MaybeBuffer = typeof globalThis extends { Buffer: infer B extends abstract new (...args: any) => any } ? InstanceType : never; + /** * Represents any type that can be converted into a {@link DataAPIBlob} * * @public */ -export type DataAPIBlobLike = DataAPIBlob | ArrayBuffer | Buffer | { $binary: string }; +export type DataAPIBlobLike = DataAPIBlob | ArrayBuffer | MaybeBuffer | { $binary: string }; /** * A shorthand function for `new DataAPIBlob(blob)` @@ -133,7 +135,7 @@ export class DataAPIBlob implements TableCodec { * * @returns The blob as a `Buffer` */ - public asBuffer(): Buffer { + public asBuffer(): MaybeBuffer { if (typeof Buffer === 'undefined') { throw new Error("Buffer is not available in this environment"); } diff --git a/src/documents/errors.ts b/src/documents/errors.ts index ffc6645d..819fa4c0 100644 --- a/src/documents/errors.ts +++ b/src/documents/errors.ts @@ -196,6 +196,9 @@ export class DataAPITimeoutError extends DataAPIError { this.name = 'DataAPITimeoutError'; } + /** + * @internal + */ public static mk(info: HTTPRequestInfo, types: TimedOutCategories): DataAPITimeoutError { return new DataAPITimeoutError(info, types); } diff --git a/src/documents/tables/table.ts b/src/documents/tables/table.ts index 0b197b2b..43255f14 100644 --- a/src/documents/tables/table.ts +++ b/src/documents/tables/table.ts @@ -1294,7 +1294,7 @@ export class Table = (logger: Logger) => { emitCommandStarted?(info: DataAPIRequestInfo, opts: ExecCmdOpts): void, emitCommandFailed?(info: DataAPIRequestInfo, error: Error, started: number, opts: ExecCmdOpts): void, @@ -64,6 +67,9 @@ type EmissionStrategy = (logger: Logger) => { emitCommandWarnings?(info: DataAPIRequestInfo, warnings: DataAPIErrorDescriptor[], opts: ExecCmdOpts): void, } +/** + * @internal + */ type EmissionStrategies = { Normal: EmissionStrategy<'normal'>, Admin: EmissionStrategy<'admin'>, diff --git a/src/lib/api/clients/devops-api-http-client.ts b/src/lib/api/clients/devops-api-http-client.ts index 7f26bdc2..ce5f0789 100644 --- a/src/lib/api/clients/devops-api-http-client.ts +++ b/src/lib/api/clients/devops-api-http-client.ts @@ -33,6 +33,9 @@ export interface DevOpsAPIRequestInfo { methodName: string, } +/** + * @internal + */ interface LongRunningRequestInfo { id: string | ((resp: DevopsAPIResponse) => string), target: string, @@ -42,12 +45,18 @@ interface LongRunningRequestInfo { timeoutManager: TimeoutManager, } +/** + * @internal + */ interface DevopsAPIResponse { data?: Record, headers: Record, status: number, } +/** + * @internal + */ interface DevOpsAPIHttpClientOpts extends HTTPClientOptions { tokenProvider: TokenProvider | undefined, } diff --git a/src/lib/logging/types.ts b/src/lib/logging/types.ts index 7ac13d89..83cf2424 100644 --- a/src/lib/logging/types.ts +++ b/src/lib/logging/types.ts @@ -288,7 +288,7 @@ export interface DataAPIExplicitLoggingConfig { } /** - * @internal + * @public */ export interface NormalizedLoggingConfig { events: readonly DataAPILoggingEvent[], diff --git a/tests/integration/client/data-api-client.test.ts b/tests/integration/client/data-api-client.test.ts index 3e4114a5..e9d47343 100644 --- a/tests/integration/client/data-api-client.test.ts +++ b/tests/integration/client/data-api-client.test.ts @@ -63,25 +63,6 @@ describe('integration.client.data-api-client', () => { }); }); - describe('asyncDispose', () => { - it('should not allow operations after using the client', async () => { - const client = new DataAPIClient(TEST_APPLICATION_TOKEN, { environment: ENVIRONMENT }); - const db = client.db(TEST_APPLICATION_URI, { keyspace: DEFAULT_KEYSPACE }); - - { - await using _client = client; - } - - try { - await db.listCollections(); - assert.fail('should have thrown an error'); - } catch (e) { - assert.ok(e instanceof Error); - assert.ok(e.name !== 'AssertionError'); - } - }); - }); - describe('monitoring commands', () => { let stdout: string[] = [], stderr: string[] = []; const _console = global.console; diff --git a/tsconfig.json b/tsconfig.json index 7852354b..9ba3339a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,6 @@ }, "lib": [ "es2020", - "esnext.disposable", "dom" ] }, From 1a9a45e5e667dcf3543c332d143dc89e872d826b Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 4 Jan 2025 19:44:36 +0530 Subject: [PATCH 21/44] updated build script to have report updating be conditional --- api-extractor.jsonc | 4 ++-- etc/astra-db-ts.api.md | 15 ++++++++------ scripts/build.sh | 27 ++++++++++++++++++++++--- scripts/check.sh | 4 ++-- scripts/repl.sh | 2 +- src/administration/astra-admin.ts | 4 ++-- src/administration/astra-db-admin.ts | 4 ++-- src/administration/data-api-db-admin.ts | 4 ++-- src/db/db.ts | 4 ++-- src/documents/collections/collection.ts | 4 ++-- src/documents/tables/table.ts | 4 ++-- src/lib/api/index.ts | 1 + src/lib/api/types.ts | 7 +++++++ 13 files changed, 58 insertions(+), 26 deletions(-) diff --git a/api-extractor.jsonc b/api-extractor.jsonc index 402bda15..9498192a 100644 --- a/api-extractor.jsonc +++ b/api-extractor.jsonc @@ -142,7 +142,7 @@ /** * (REQUIRED) Whether to generate an API report. */ - "enabled": true + "enabled": true, /** * The filename for the API report files. It will be combined with "reportFolder" or "reportTempFolder" to produce @@ -168,7 +168,7 @@ * SUPPORTED TOKENS: , , * DEFAULT VALUE: "/temp/" */ - // "reportFolder": "/temp/", + "reportFolder": "/temp/" /** * Specifies the folder where the temporary report file is written. The file name portion is determined by diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index 8ddbcb76..bfd3e779 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -140,7 +140,7 @@ export class AstraAdmin { dbInfo(id: string, options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; dropDatabase(db: Db | string, options?: AstraDropDatabaseOptions): Promise; // (undocumented) - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; listDatabases(options?: ListAstraDatabasesOptions): Promise; } @@ -170,7 +170,7 @@ export class AstraDbAdmin extends DbAdmin { dropKeyspace(keyspace: string, options?: AstraDropKeyspaceOptions): Promise; findEmbeddingProviders(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; // (undocumented) - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; get id(): string; info(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; listKeyspaces(options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; @@ -407,7 +407,7 @@ export class Collection(filter: CollectionFilter, options?: CollectionFindOneAndDeleteOptions): Promise; findOneAndReplace(filter: CollectionFilter, replacement: NoId, options?: CollectionFindOneAndReplaceOptions): Promise; findOneAndUpdate(filter: CollectionFilter, update: CollectionUpdateFilter, options?: CollectionFindOneAndUpdateOptions): Promise; - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; insertMany(documents: readonly MaybeId[], options?: CollectionInsertManyOptions): Promise>; insertOne(document: MaybeId, options?: WithTimeout<'generalMethodTimeoutMs'>): Promise>; readonly keyspace: string; @@ -877,7 +877,7 @@ export class DataAPIDbAdmin extends DbAdmin { dropKeyspace(keyspace: string, options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; findEmbeddingProviders(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; // (undocumented) - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; listKeyspaces(options?: WithTimeout<'keyspaceAdminTimeoutMs'>): Promise; } @@ -1030,7 +1030,7 @@ export class Db { dropTable(name: string, options?: DropTableOptions): Promise; dropTableIndex(name: string, options?: TableDropIndexOptions): Promise; // (undocumented) - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; get id(): string; info(options?: WithTimeout<'databaseAdminTimeoutMs'>): Promise; get keyspace(): string; @@ -1638,6 +1638,9 @@ export const oid: (id?: string | number | null) => ObjectId; // @public export type OneOrMany = T | readonly T[]; +// @public +export type OpaqueHttpClient = any; + // @public (undocumented) export type PathCodec = { serialize?: Fns['serialize']; @@ -1747,7 +1750,7 @@ export class Table | null>; findOne>(filter: TableFilter, options: TableFindOneOptions): Promise; - get _httpClient(): unknown; + get _httpClient(): OpaqueHttpClient; insertMany(rows: readonly WSchema[], options?: TableInsertManyOptions): Promise>; insertOne(row: WSchema, timeout?: WithTimeout<'generalMethodTimeoutMs'>): Promise>; readonly keyspace: string; diff --git a/scripts/build.sh b/scripts/build.sh index 87c9176d..233839fa 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,5 +1,17 @@ #!/usr/bin/env sh +while [ $# -gt 0 ]; do + case "$1" in + "-no-report" | "-R") + no_report=true + ;; + "-light" | "-l") + light=true + ;; + esac + shift +done + # Cleans the previous build rm -rf ./dist @@ -7,7 +19,7 @@ rm -rf ./dist node scripts/utils/build-version-file.js > src/version.ts # Transpiles the project -if [ "$1" = "-light" ]; then +if [ "$light" = true ]; then npx tsc --project tsconfig.production.json -d false --noCheck else npx tsc --project tsconfig.production.json @@ -16,9 +28,15 @@ fi # Replaces alias paths with relative paths (e.g. `@/src/version` -> `../../src/version`) npx tsc-alias -p tsconfig.production.json -if [ "$1" != "-light" ]; then +if [ "$light" != true ]; then # Creates the rollup .d.ts, generates an API report in etc/, and cleans up any temp files - npx api-extractor run -c ./api-extractor.jsonc --local && rm -r ./temp + npx api-extractor run -c ./api-extractor.jsonc --local + + # Updates the API report if flag not set + if [ "$no_report" != true ]; then + mv -f ./temp/*.api.md ./etc/ + fi + rm -r ./temp # Uses a more succinct licence notice + removes block comments (the rollup .d.ts file already contains the ts-doc) find ./dist -type f -name '*.js' -exec node scripts/utils/reduce-comments.js {} \; @@ -26,6 +44,9 @@ if [ "$1" != "-light" ]; then # Adds the missing license notice to the rollup .d.ts node scripts/utils/add-license-bumf.js dist/astra-db-ts.d.ts + # Protects against Symbol.asyncIterator not found + sed -i -E '/^(\s+)\[Symbol\.asyncIterator\]/i\// @ts-ignore-error - May or may not be found depending on TS version & esnext.disposable in lib' dist/astra-db-ts.d.ts + # Delete the "empty" files where only types were declared node scripts/utils/del-empty-dist-files.js diff --git a/scripts/check.sh b/scripts/check.sh index ce6c8c28..4a9a3399 100755 --- a/scripts/check.sh +++ b/scripts/check.sh @@ -66,9 +66,9 @@ for check_type in $check_types; do echo "Checking library compiles..." tmp_dir="tmp-lib-check" - rm -rf "$tmp_dir" + rm -rf "$tmp_dir" "$main_dir/dist" - (scripts/build.sh \ + (scripts/build.sh -no-report \ && mkdir "$tmp_dir" \ && cd "$tmp_dir" \ && npm init -y \ diff --git a/scripts/repl.sh b/scripts/repl.sh index 41dd76e6..0b7a9c87 100755 --- a/scripts/repl.sh +++ b/scripts/repl.sh @@ -12,7 +12,7 @@ if [ -z "$CLIENT_DB_TOKEN" ] || [ -z "$CLIENT_DB_TOKEN" ]; then fi # Rebuild the client (without types or any extra processing for speed) -sh scripts/build.sh -light || exit 2 +sh scripts/build.sh -light -no-report || exit 2 while [ $# -gt 0 ]; do case "$1" in diff --git a/src/administration/astra-admin.ts b/src/administration/astra-admin.ts index 8b18a246..2e8320a7 100644 --- a/src/administration/astra-admin.ts +++ b/src/administration/astra-admin.ts @@ -23,7 +23,7 @@ import { Db } from '@/src/db/db'; import { buildAstraDatabaseAdminInfo } from '@/src/administration/utils'; import { DEFAULT_DEVOPS_API_ENDPOINTS, DEFAULT_KEYSPACE, HttpMethods } from '@/src/lib/api/constants'; import { DevOpsAPIHttpClient } from '@/src/lib/api/clients/devops-api-http-client'; -import { TokenProvider, WithTimeout } from '@/src/lib'; +import { OpaqueHttpClient, TokenProvider, WithTimeout } from '@/src/lib'; import { AstraDbAdminInfo } from '@/src/administration/types/admin/database-info'; import { parseAdminSpawnOpts } from '@/src/client/parsers/spawn-admin'; import { InternalRootClientOpts } from '@/src/client/types/internal'; @@ -480,7 +480,7 @@ export class AstraAdmin { }); } - public get _httpClient(): unknown { + public get _httpClient(): OpaqueHttpClient { return this.#httpClient; } } diff --git a/src/administration/astra-db-admin.ts b/src/administration/astra-db-admin.ts index a85bb4e9..835935e5 100644 --- a/src/administration/astra-db-admin.ts +++ b/src/administration/astra-db-admin.ts @@ -15,7 +15,7 @@ import { AstraCreateKeyspaceOptions, AstraDropKeyspaceOptions } from '@/src/administration/types'; import { DbAdmin } from '@/src/administration/db-admin'; -import type { WithTimeout } from '@/src/lib'; +import { OpaqueHttpClient, WithTimeout } from '@/src/lib'; import { TokenProvider } from '@/src/lib'; import { buildAstraDatabaseAdminInfo, extractAstraEnvironment } from '@/src/administration/utils'; import { FindEmbeddingProvidersResult } from '@/src/administration/types/db-admin/find-embedding-providers'; @@ -335,7 +335,7 @@ export class AstraDbAdmin extends DbAdmin { }); } - public get _httpClient(): unknown { + public get _httpClient(): OpaqueHttpClient { return this.#httpClient; } diff --git a/src/administration/data-api-db-admin.ts b/src/administration/data-api-db-admin.ts index 08ce2dca..2674f6c9 100644 --- a/src/administration/data-api-db-admin.ts +++ b/src/administration/data-api-db-admin.ts @@ -15,7 +15,7 @@ import { DataAPICreateKeyspaceOptions } from '@/src/administration/types'; import { DbAdmin } from '@/src/administration/db-admin'; -import type { WithTimeout } from '@/src/lib'; +import type { OpaqueHttpClient, WithTimeout } from '@/src/lib'; import { FindEmbeddingProvidersResult } from '@/src/administration/types/db-admin/find-embedding-providers'; import { DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client'; import { Db } from '@/src/db'; @@ -223,7 +223,7 @@ export class DataAPIDbAdmin extends DbAdmin { }); } - public get _httpClient(): unknown { + public get _httpClient(): OpaqueHttpClient { return this.#httpClient; } } diff --git a/src/db/db.ts b/src/db/db.ts index 55040a10..6c4d0cf3 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -13,7 +13,7 @@ // limitations under the License. import { Collection, FoundDoc, SomeDoc, WithId } from '@/src/documents/collections'; -import { DEFAULT_KEYSPACE, RawDataAPIResponse, WithTimeout } from '@/src/lib/api'; +import { DEFAULT_KEYSPACE, type OpaqueHttpClient, RawDataAPIResponse, WithTimeout } from '@/src/lib/api'; import { AstraDbAdmin } from '@/src/administration/astra-db-admin'; import { DataAPIEnvironment, nullish } from '@/src/lib/types'; import { extractDbIdFromUrl, extractRegionFromUrl } from '@/src/documents/utils'; @@ -1214,7 +1214,7 @@ export class Db { }); } - public get _httpClient(): unknown { + public get _httpClient(): OpaqueHttpClient { return this.#httpClient; } } diff --git a/src/documents/collections/collection.ts b/src/documents/collections/collection.ts index 29c03540..3368c8ad 100644 --- a/src/documents/collections/collection.ts +++ b/src/documents/collections/collection.ts @@ -43,7 +43,7 @@ import { } from '@/src/documents/collections/types'; import { CollectionDefinition, CollectionOptions, Db } from '@/src/db'; import { BigNumberHack, DataAPIHttpClient } from '@/src/lib/api/clients/data-api-http-client'; -import { WithTimeout } from '@/src/lib'; +import { type OpaqueHttpClient, WithTimeout } from '@/src/lib'; import { CommandImpls } from '@/src/documents/commands/command-impls'; import { $CustomInspect } from '@/src/lib/constants'; import { CollectionInsertManyError, TooManyDocumentsToCountError, WithSim } from '@/src/documents'; @@ -1507,7 +1507,7 @@ export class Collection Date: Sat, 4 Jan 2025 20:00:16 +0530 Subject: [PATCH 22/44] reexport from version file --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index b9578446..132a2665 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,4 +17,5 @@ export * from './db'; export * from './documents'; export * from './administration'; export * from './lib'; +export * from './version'; export * from 'bignumber.js'; From 93e0e92262d5c85a005f905e1f90b31071ee473f Mon Sep 17 00:00:00 2001 From: toptobes Date: Sat, 4 Jan 2025 20:14:43 +0530 Subject: [PATCH 23/44] fixed some import errors --- etc/astra-db-ts.api.md | 95 +++++++++++++++------ scripts/utils/build-version-file.js | 11 +++ src/db/types/index.ts | 1 + src/db/types/tables/list-indexes.ts | 38 ++++++++- src/documents/collections/index.ts | 2 +- src/documents/commands/command-impls.ts | 4 +- src/documents/commands/helpers/insertion.ts | 3 +- src/documents/commands/types/index.ts | 5 +- src/documents/datatypes/blob.ts | 9 +- src/documents/tables/index.ts | 1 + src/version.ts | 11 +++ 11 files changed, 141 insertions(+), 39 deletions(-) diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index bfd3e779..345000b5 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -794,7 +794,6 @@ export class DataAPIBlob implements TableCodec { constructor(blob: DataAPIBlobLike, validate?: boolean); asArrayBuffer(): ArrayBuffer; asBase64(): string; - // Warning: (ae-forgotten-export) The symbol "MaybeBuffer" needs to be exported by the entry point index.d.ts asBuffer(): MaybeBuffer; get byteLength(): number; static isBlobLike(value: unknown): value is DataAPIBlobLike; @@ -1380,11 +1379,6 @@ export interface GenericFindOptions extends WithTimeout<'generalMethodTimeoutMs' sort?: Sort; } -// Warning: (ae-internal-missing-underscore) The name "GenericInsertManyDocumentResponse" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export type GenericInsertManyDocumentResponse<_T> = any; - // @public export type GenericInsertManyOptions = GenericInsertManyUnorderedOptions | GenericInsertManyOrderedOptions; @@ -1394,16 +1388,6 @@ export interface GenericInsertManyOrderedOptions extends WithTimeout<'generalMet ordered: true; } -// Warning: (ae-internal-missing-underscore) The name "GenericInsertManyResult" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export interface GenericInsertManyResult { - // (undocumented) - insertedCount: number; - // (undocumented) - insertedIds: ID[]; -} - // @public export interface GenericInsertManyUnorderedOptions extends WithTimeout<'generalMethodTimeoutMs'> { chunkSize?: number; @@ -1411,14 +1395,6 @@ export interface GenericInsertManyUnorderedOptions extends WithTimeout<'generalM ordered?: false; } -// Warning: (ae-internal-missing-underscore) The name "GenericInsertOneResult" should be prefixed with an underscore because the declaration is marked as @internal -// -// @internal -export interface GenericInsertOneResult { - // (undocumented) - insertedId: ID; -} - // @public export interface GenericReplaceOneOptions extends WithTimeout<'generalMethodTimeoutMs'> { // (undocumented) @@ -1502,6 +1478,12 @@ export abstract class KeyTransformer { abstract serializeKey(key: string, ctx: BaseSerCtx): string; } +// @public +export const LIB_NAME = "astra-db-ts"; + +// @public +export const LIB_VERSION = "2.0.0-preview.0"; + // @public export interface ListAstraDatabasesOptions extends WithTimeout<'databaseAdminTimeoutMs'> { include?: AstraDbStatusFilter; @@ -1523,6 +1505,12 @@ export interface ListCreateTableColumnDefinition { valueType: TableScalarType; } +// @public +export interface ListIndexOptions extends WithTimeout<'tableAdminTimeoutMs'> { + // (undocumented) + nameOnly?: boolean; +} + // @public export type ListTableColumnDefinitions = Record; @@ -1582,6 +1570,11 @@ export interface MapCreateTableColumnDefinition { valueType: TableScalarType; } +// @public +export type MaybeBuffer = typeof globalThis extends { + Buffer: infer B extends abstract new (...args: any) => any; +} ? InstanceType : never; + // @public export type MaybeId = NoId & { _id?: IdOf; @@ -1754,11 +1747,9 @@ export class Table>; insertOne(row: WSchema, timeout?: WithTimeout<'generalMethodTimeoutMs'>): Promise>; readonly keyspace: string; - // Warning: (ae-forgotten-export) The symbol "ListIndexOptions" needs to be exported by the entry point index.d.ts listIndexes(options: ListIndexOptions & { nameOnly: true; }): Promise; - // Warning: (ae-forgotten-export) The symbol "TableIndexDescriptor" needs to be exported by the entry point index.d.ts listIndexes(options?: ListIndexOptions & { nameOnly?: false; }): Promise; @@ -1766,11 +1757,17 @@ export class Table, update: TableUpdateFilter, timeout?: WithTimeout<'generalMethodTimeoutMs'>): Promise; } -// Warning: (ae-forgotten-export) The symbol "TableCodecClass" needs to be exported by the entry point index.d.ts -// // @public (undocumented) export type TableCodec<_Class extends TableCodecClass> = EmptyObj; +// @public (undocumented) +export type TableCodecClass = { + new (...args: any[]): { + [$SerializeForTable]: (ctx: TableSerCtx) => ReturnType>; + }; + [$DeserializeForTable]: TableCodecSerDesFns['deserialize']; +}; + // @public (undocumented) export class TableCodecs { // (undocumented) @@ -1871,6 +1868,14 @@ export type TableFindOneOptions = GenericFindOneOptions; // @public export type TableFindOptions = GenericFindOptions; +// @public +export interface TableIndexDescriptor { + // (undocumented) + definition: TableNormalIndexDescriptor | TableVectorIndexDescriptor | TableUnknownIndex; + // (undocumented) + name: string; +} + // @public export interface TableIndexOptions { ascii?: boolean; @@ -1878,6 +1883,16 @@ export interface TableIndexOptions { normalize?: boolean; } +// @public +export interface TableIndexUnsupportedColumnApiSupport { + // (undocumented) + cqlDefinition: string; + // (undocumented) + createIndex: boolean; + // (undocumented) + filter: boolean; +} + // @public export class TableInsertManyError extends CumulativeOperationError { name: string; @@ -1898,6 +1913,14 @@ export interface TableInsertOneResult { insertedId: PKey; } +// @public +export interface TableNormalIndexDescriptor { + // (undocumented) + column: string; + // (undocumented) + options: TableIndexOptions; +} + // @public export interface TableOptions extends WithKeyspace { embeddingApiKey?: string | EmbeddingHeadersProvider | null; @@ -1924,12 +1947,28 @@ export interface TableSerDesConfig extends BaseSerDesConfig { $set?: Partial & SomeRow; $unset?: Record; } +// @public +export interface TableVectorIndexDescriptor { + // (undocumented) + column: string; + // (undocumented) + options: TableVectorIndexOptions; +} + // @public export interface TableVectorIndexOptions { metric?: 'cosine' | 'euclidean' | 'dot_product'; diff --git a/scripts/utils/build-version-file.js b/scripts/utils/build-version-file.js index ec287ca2..45f0ed44 100644 --- a/scripts/utils/build-version-file.js +++ b/scripts/utils/build-version-file.js @@ -13,6 +13,17 @@ console.log([ `// See the License for the specific language governing permissions and`, `// limitations under the License.`, ``, + `/**`, + ` * The name of the library.`, + ` *`, + ` * @public`, + ` */`, `export const LIB_NAME = 'astra-db-ts';`, + ``, + `/**`, + ` * The version of the library.`, + ` *`, + ` * @public`, + ` */`, `export const LIB_VERSION = '${require('../../package.json').version}';`, ].join('\n')); diff --git a/src/db/types/index.ts b/src/db/types/index.ts index b2f105e4..3fde90bd 100644 --- a/src/db/types/index.ts +++ b/src/db/types/index.ts @@ -24,6 +24,7 @@ export type * from './tables/create-table'; export type * from './tables/table-schema'; export type * from './tables/drop-table'; export type * from './tables/list-tables'; +export type * from './tables/list-indexes'; export type * from './tables/spawn-table'; export type { WithKeyspace } from './common'; diff --git a/src/db/types/tables/list-indexes.ts b/src/db/types/tables/list-indexes.ts index cf853cec..31401840 100644 --- a/src/db/types/tables/list-indexes.ts +++ b/src/db/types/tables/list-indexes.ts @@ -15,31 +15,61 @@ import { TableIndexOptions, TableVectorIndexOptions } from '@/src/documents'; import { WithTimeout } from '@/src/lib'; +/** + * Options for listing indexes on a table. + * + * @public + */ export interface ListIndexOptions extends WithTimeout<'tableAdminTimeoutMs'> { nameOnly?: boolean, } +/** + * A descriptor for an index on a table. + * + * @public + */ export interface TableIndexDescriptor { name: string, definition: TableNormalIndexDescriptor | TableVectorIndexDescriptor | TableUnknownIndex, } -interface TableNormalIndexDescriptor { +/** + * A descriptor for a normal index on a table. + * + * @public + */ +export interface TableNormalIndexDescriptor { column: string, options: TableIndexOptions, } -interface TableVectorIndexDescriptor { +/** + * A descriptor for a vector index on a table. + * + * @public + */ +export interface TableVectorIndexDescriptor { column: string, options: TableVectorIndexOptions, } -interface TableUnknownIndex { +/** + * A descriptor for an index on a table with an unsupported column type. + * + * @public + */ +export interface TableUnknownIndex { column: 'UNKNOWN', apiSupport: TableIndexUnsupportedColumnApiSupport, } -interface TableIndexUnsupportedColumnApiSupport { +/** + * API support for an index on a table with an unsupported column type. + * + * @public + */ +export interface TableIndexUnsupportedColumnApiSupport { createIndex: boolean, filter: boolean, cqlDefinition: string, diff --git a/src/documents/collections/index.ts b/src/documents/collections/index.ts index 0aef27f2..4a70fe04 100644 --- a/src/documents/collections/index.ts +++ b/src/documents/collections/index.ts @@ -25,8 +25,8 @@ export { export { CollCodecSerDesFns, - CollCodecs, CollCodecClass, + CollCodecs, CollCodec, } from './ser-des/codecs'; export { $DeserializeForCollection } from '@/src/documents/collections/ser-des/constants'; diff --git a/src/documents/commands/command-impls.ts b/src/documents/commands/command-impls.ts index aa7cd5b7..a6c85ef4 100644 --- a/src/documents/commands/command-impls.ts +++ b/src/documents/commands/command-impls.ts @@ -31,8 +31,6 @@ import { GenericFindOneAndUpdateOptions, GenericFindOneOptions, GenericFindOptions, - GenericInsertManyResult, - GenericInsertOneResult, GenericReplaceOneOptions, GenericUpdateManyOptions, GenericUpdateOneOptions, @@ -48,6 +46,8 @@ import { mkRespErrorFromResponse } from '@/src/documents/errors'; import { normalizedSort } from '@/src/documents/utils'; import { mkDistinctPathExtractor, pullSafeProjection4Distinct } from '@/src/documents/commands/helpers/distinct'; import stableStringify from 'safe-stable-stringify'; +import { GenericInsertOneResult } from '@/src/documents/commands/types/insert/insert-one'; +import { GenericInsertManyResult } from '@/src/documents/commands/types/insert/insert-many'; /** * @internal diff --git a/src/documents/commands/helpers/insertion.ts b/src/documents/commands/helpers/insertion.ts index 2d26360f..411898cd 100644 --- a/src/documents/commands/helpers/insertion.ts +++ b/src/documents/commands/helpers/insertion.ts @@ -20,9 +20,10 @@ import { mkRespErrorFromResponse, mkRespErrorFromResponses, } from '@/src/documents/errors'; -import { GenericInsertManyDocumentResponse, SomeDoc, SomeId } from '@/src/documents'; +import { SomeDoc, SomeId } from '@/src/documents'; import { TimeoutManager } from '@/src/lib/api/timeouts'; import { RawDataAPIResponse } from '@/src/lib'; +import { GenericInsertManyDocumentResponse } from '@/src/documents/commands/types/insert/insert-many'; /** * @internal diff --git a/src/documents/commands/types/index.ts b/src/documents/commands/types/index.ts index 13054f5d..b6d4724d 100644 --- a/src/documents/commands/types/index.ts +++ b/src/documents/commands/types/index.ts @@ -19,8 +19,9 @@ export type * from './find/find-one'; export type * from './find/find-one-delete'; export type * from './find/find-one-replace'; export type * from './find/find-one-update'; -export type * from './insert/insert-many'; -export type * from './insert/insert-one'; +export type { + GenericInsertManyOptions, GenericInsertManyOrderedOptions, GenericInsertManyUnorderedOptions, +} from './insert/insert-many'; export type * from './update/update-common'; export type * from './update/update-many'; export type * from './update/update-one'; diff --git a/src/documents/datatypes/blob.ts b/src/documents/datatypes/blob.ts index b93f48f9..250fbd4c 100644 --- a/src/documents/datatypes/blob.ts +++ b/src/documents/datatypes/blob.ts @@ -17,7 +17,14 @@ import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; import { forJSEnv } from '@/src/lib/utils'; -type MaybeBuffer = typeof globalThis extends { Buffer: infer B extends abstract new (...args: any) => any } ? InstanceType : never; +/** + * Represents a `Buffer` type, if available. + * + * @public + */ +export type MaybeBuffer = typeof globalThis extends { Buffer: infer B extends abstract new (...args: any) => any } + ? InstanceType + : never; /** * Represents any type that can be converted into a {@link DataAPIBlob} diff --git a/src/documents/tables/index.ts b/src/documents/tables/index.ts index c1f101d6..5610a0ba 100644 --- a/src/documents/tables/index.ts +++ b/src/documents/tables/index.ts @@ -25,6 +25,7 @@ export { export { TableCodecSerDesFns, + TableCodecClass, TableCodecs, TableCodec, } from './ser-des/codecs'; diff --git a/src/version.ts b/src/version.ts index 5054951a..e9b2d78f 100644 --- a/src/version.ts +++ b/src/version.ts @@ -12,5 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +/** + * The name of the library. + * + * @public + */ export const LIB_NAME = 'astra-db-ts'; + +/** + * The version of the library. + * + * @public + */ export const LIB_VERSION = '2.0.0-preview.0'; From d43992a3f28eae2ac7ddd1a4a28f775f00bc0bcf Mon Sep 17 00:00:00 2001 From: toptobes Date: Sun, 5 Jan 2025 19:33:32 +0530 Subject: [PATCH 24/44] more datatype tests --- src/documents/datatypes/dates.ts | 11 +- .../documents/tables/datatypes.test.ts | 143 +++++++++++++++--- tests/integration/misc/quickstart.test.ts | 2 +- 3 files changed, 132 insertions(+), 24 deletions(-) diff --git a/src/documents/datatypes/dates.ts b/src/documents/datatypes/dates.ts index eeb212a3..fa532270 100644 --- a/src/documents/datatypes/dates.ts +++ b/src/documents/datatypes/dates.ts @@ -211,7 +211,7 @@ export class DataAPIDuration implements TableCodec { * * @public */ -export const time = (time?: string | Date | DataAPITimeComponents) => new DataAPITime(time); +export const time = (time?: string | Date | PartialDataAPITimeComponents) => new DataAPITime(time); /** * Represents the time components that make up a `DataAPITime` @@ -237,6 +237,13 @@ export interface DataAPITimeComponents { nanoseconds: number } +/** + * Represents the time components that make up a `DataAPITime`, with the nanoseconds being optional + * + * @public + */ +export type PartialDataAPITimeComponents = (Omit & { nanoseconds?: number }); + /** * Represents a `time` column for Data API tables. * @@ -268,7 +275,7 @@ export class DataAPITime implements TableCodec { * * @param input - The input to create the `DataAPITime` from */ - public constructor(input?: string | Date | (DataAPITimeComponents & { nanoseconds?: number })) { + public constructor(input?: string | Date | PartialDataAPITimeComponents) { input ||= new Date(); if (typeof input === 'string') { diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts index 34260866..481063b1 100644 --- a/tests/integration/documents/tables/datatypes.test.ts +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -18,9 +18,10 @@ import { DataAPIBlob, DataAPIResponseError, DataAPIVector, - date, + date, duration, SomeRow, - Table, + Table, time, + uuid, vector, } from '@/src/documents'; import { it, parallel } from '@/tests/testlib'; @@ -57,6 +58,7 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.ok('a'.repeat(65535)); await colAsserter.ok('A!@#$%^&*()'); + await colAsserter.ok('⨳⨓⨋'); }); (['int', 'tinyint', 'smallint'] as const).map((col) => @@ -117,25 +119,6 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.ok(false); }); - it('should handle different date insertion cases', async (key) => { - const colAsserter = mkColumnAsserter(key, 'date'); - - await colAsserter.notOk('2000-00-06'); - await colAsserter.notOk('2000-01-00'); - await colAsserter.notOk('2000/01/01'); - await colAsserter.notOk('2000-01-32'); - await colAsserter.notOk(date('2000-02-30')); - await colAsserter.notOk(date('+2000-00-06')); - await colAsserter.notOk(date('-0000-00-06')); - await colAsserter.notOk(date('10000-00-06')); - - await colAsserter.ok('1970-01-01', date); - await colAsserter.ok('-0001-01-01', date); - await colAsserter.ok(date('9999-12-31')); - await colAsserter.ok(date('+10000-12-31')); - await colAsserter.ok(date('-10000-12-31')); - }); - it('should handle different decimal insertion cases', async (key) => { const colAsserter = mkColumnAsserter(key, 'decimal'); @@ -232,4 +215,122 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.ok('toto, I\'ve a feeling we\'re in vectorize again', _ => dummyVec); await colAsserter.ok(dummyVec); }); + + it('should handle different map insertion cases', async (key) => { + const uuid1 = uuid(1); + const uuid4 = uuid(4); + + const colAsserter = mkColumnAsserter(key, 'map'); + + await colAsserter.notOk([]); + await colAsserter.notOk([['a', uuid(4)]]); + await colAsserter.notOk(new Map([['a', uuid1], ['b', 'uuid4']])); + + for (const val of [null, undefined, {}, new Map()]) { + await colAsserter.ok(val, _ => new Map()); + } + + await colAsserter.ok({ a: uuid1.toString(), b: uuid4 }, _ => new Map([['a', uuid1], ['b', uuid4]])); + await colAsserter.ok(new Map([['a', uuid1.toString()], ['b', uuid4]]), _ => new Map([['a', uuid1], ['b', uuid4]])); + await colAsserter.ok(new Map([['⨳⨓⨋', uuid1]])); + await colAsserter.ok(new Map([['a'.repeat(50000), uuid1]])); + await colAsserter.ok(new Map(Array.from({ length: 1000 }, (_, i) => [i.toString(), uuid(7)]))); + }); + + it('should handle different set insertion cases', async (key) => { + const uuid1 = uuid(1); + const uuid4 = uuid(4); + + const colAsserter = mkColumnAsserter(key, 'set'); + + await colAsserter.notOk({}); + await colAsserter.notOk(new Set([uuid1, 'uuid4'])); + + for (const val of [null, undefined, [], new Set()]) { + await colAsserter.ok(val, _ => new Set()); + } + + await colAsserter.ok([uuid1.toString(), uuid4], _ => new Set([uuid1, uuid4])); + await colAsserter.ok(new Set([uuid1.toString(), uuid4]), _ => new Set([uuid1, uuid4])); + await colAsserter.ok([uuid1, uuid1, uuid4], _ => new Set([uuid1, uuid4])); + await colAsserter.ok(new Set(Array.from({ length: 1000 }, () => uuid(7)))); + }); + + it('should handle different list insertion cases', async (key) => { + const uuid1 = uuid(1); + const uuid4 = uuid(4); + + const colAsserter = mkColumnAsserter(key, 'list'); + + await colAsserter.notOk({}); + await colAsserter.notOk([uuid1, 'uuid4']); + + for (const val of [null, undefined, []]) { + await colAsserter.ok(val, _ => []); + } + + await colAsserter.ok([uuid1.toString(), uuid1, uuid1, uuid4], _ => [uuid1, uuid1, uuid1, uuid4]); + await colAsserter.ok(new Array(1000).fill(uuid(7))); + }); + + it('should handle different time insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'time'); + + await colAsserter.notOk('24:00:00'); + await colAsserter.notOk('00:60:00'); + await colAsserter.notOk('00:00:00.0000000000'); + await colAsserter.notOk('1:00:00'); + await colAsserter.notOk('-01:00:00'); + await colAsserter.notOk('23-59-00'); + await colAsserter.notOk('12:34:56Z+05:30'); + await colAsserter.notOk(3123123); + + await colAsserter.ok('23:59:00.', _ => time('23:00:00')); // S + await colAsserter.ok('23:59', _ => time('23:59:00')); // S + await colAsserter.ok('00:00:00.000000000', time); + await colAsserter.ok(time('23:59:59.999999999')); + await colAsserter.ok(time(new Date('1970-01-01T23:59:59.999Z')), _ => time('23:59:59.999')); + await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59 })); + await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59, nanoseconds: 120012 })); + }); + + it('should handle different date insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'date'); + + await colAsserter.notOk('2000-00-01'); + await colAsserter.notOk('2000-01-00'); + await colAsserter.notOk('2000/01/01'); + await colAsserter.notOk('2000-01-32'); + await colAsserter.notOk('2000-02-30'); + await colAsserter.notOk('+2000-01-01'); + await colAsserter.notOk('-0000-01-01'); + await colAsserter.notOk(3123123); + + await colAsserter.ok('0000-01-01', date); + await colAsserter.ok('1970-01-01', date); + await colAsserter.ok('-0001-01-01', date); + await colAsserter.ok(date('9999-12-31')); + await colAsserter.ok(date('+500000-12-31')); + await colAsserter.ok(date('-500000-12-31')); + await colAsserter.ok(date(new Date('1970-01-01T23:59:59.999Z')), _ => date('1970-01-01')); + await colAsserter.ok(date({ year: 1970, month: 1, date: 1 })); + }); + + it('should handle different timestamp insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'timestamp'); + + await colAsserter.notOk(3123123); + + await colAsserter.ok(new Date()); + await colAsserter.ok(new Date('1970-01-01T00:00:00.000Z')); + await colAsserter.ok('1970-01-01T00:00:00.000+07:00', _ => new Date('1970-01-01T00:00:00.000+07:00')); + }); + + it('should handle different duration insertion cases', async (key) => { + const colAsserter = mkColumnAsserter(key, 'duration'); + + await colAsserter.notOk(duration('1 hour')); + + await colAsserter.ok('1h', duration); + }); }); diff --git a/tests/integration/misc/quickstart.test.ts b/tests/integration/misc/quickstart.test.ts index 75185365..4cd81720 100644 --- a/tests/integration/misc/quickstart.test.ts +++ b/tests/integration/misc/quickstart.test.ts @@ -97,7 +97,7 @@ parallel('integration.misc.quickstart', { drop: 'colls:after' }, () => { assert.strictEqual(dbInfo.id, id); assert.strictEqual(dbInfo.regions.length, 1); assert.strictEqual(dbInfo.regions[0].name, region); - assert.strictEqual(dbInfo.regions[0].apiEndpoint, region); + assert.strictEqual(dbInfo.regions[0].apiEndpoint, TEST_APPLICATION_URI); assert.ok(dbInfo.regions[0].createdAt as unknown instanceof Date); const dbAdmin = admin.dbAdmin(dbInfo.id, dbInfo.regions[0].name); From cbcc10163cf1098f0660aa134febd388a2ab6f16 Mon Sep 17 00:00:00 2001 From: toptobes Date: Mon, 6 Jan 2025 20:17:17 +0530 Subject: [PATCH 25/44] enhanced coll bignumbers support --- etc/astra-db-ts.api.md | 45 +++++++-- scripts/playground.sh | 25 +---- src/documents/collections/collection.ts | 2 +- src/documents/collections/index.ts | 8 ++ src/documents/collections/ser-des/big-nums.ts | 91 +++++++++++++++++++ src/documents/collections/ser-des/ser-des.ts | 84 ++++++++++++++--- src/documents/tables/ser-des/codecs.ts | 4 +- src/documents/tables/ser-des/ser-des.ts | 2 +- src/lib/api/ser-des/ctx.ts | 10 +- src/lib/api/ser-des/ser-des.ts | 37 +++++--- src/lib/utils.ts | 4 +- .../ser-des/key-transformer.test.ts | 2 +- .../ser-des/key-transformer.test.ts | 2 +- 13 files changed, 248 insertions(+), 68 deletions(-) create mode 100644 src/documents/collections/ser-des/big-nums.ts diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index 345000b5..d3c06037 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -287,9 +287,9 @@ export interface BaseSerDesCtx { // (undocumented) keyTransformer?: KeyTransformer; // (undocumented) - path: string[]; + next(obj?: T): readonly [1, T?]; // (undocumented) - recurse(obj?: T): readonly [1, T?]; + path: string[]; // (undocumented) rootObj: SomeDoc; } @@ -381,7 +381,7 @@ export interface CollCodecSerDesFns { // @public (undocumented) export interface CollDesCtx extends BaseDesCtx { // (undocumented) - bigNumsEnabled: boolean; + getNumRepForPath?: GetCollNumRepFn; } // @public @@ -599,7 +599,7 @@ export type CollectionReplaceOneResult = GenericUpdateResult { // (undocumented) - enableBigNumbers?: boolean; + enableBigNumbers?: GetCollNumRepFn; } // @public @@ -648,6 +648,12 @@ export interface CollectionVectorOptions { sourceModel?: string; } +// @public (undocumented) +export type CollNumRep = 'number' | 'bigint' | 'bignumber' | 'string' | 'number_or_string'; + +// @public (undocumented) +export type CollNumRepCfg = Record; + // @public (undocumented) export interface CollSerCtx extends BaseSerCtx { // (undocumented) @@ -957,9 +963,7 @@ export class DataAPIResponseError extends DataAPIError { export class DataAPITime implements TableCodec { static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (DataAPITime | undefined)?]; [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; - constructor(input?: string | Date | (DataAPITimeComponents & { - nanoseconds?: number; - })); + constructor(input?: string | Date | PartialDataAPITimeComponents); components(): DataAPITimeComponents; toDate(base?: Date | DataAPIDate): Date; toString(): string; @@ -1418,6 +1422,9 @@ export interface GenericUpdateOneOptions extends WithTimeout<'generalMethodTimeo // @public export type GenericUpdateResult = (GuaranteedUpdateResult & UpsertedUpdateResult) | (GuaranteedUpdateResult & NoUpsertUpdateResult); +// @public (undocumented) +export type GetCollNumRepFn = (path: string[]) => CollNumRep; + // @public export interface GuaranteedUpdateResult { matchedCount: N; @@ -1613,6 +1620,19 @@ export interface NoUpsertUpdateResult { // @public export type nullish = null | undefined; +// @public (undocumented) +export class NumCoercionError extends Error { + constructor(path: string[], value: number | BigNumber, from: 'number' | 'bignumber', to: CollNumRep); + // (undocumented) + readonly from: 'number' | 'bignumber'; + // (undocumented) + readonly path: string[]; + // (undocumented) + readonly to: CollNumRep; + // (undocumented) + readonly value: number | BigNumber; +} + // @public export class ObjectId implements CollCodec { static [$DeserializeForCollection](_: string, value: any, ctx: CollDesCtx): readonly [0, (ObjectId | undefined)?]; @@ -1634,6 +1654,11 @@ export type OneOrMany = T | readonly T[]; // @public export type OpaqueHttpClient = any; +// @public +export type PartialDataAPITimeComponents = (Omit & { + nanoseconds?: number; +}); + // @public (undocumented) export type PathCodec = { serialize?: Fns['serialize']; @@ -1818,9 +1843,9 @@ export interface TableDescriptor { // @public (undocumented) export interface TableDesCtx extends BaseDesCtx { // (undocumented) - populateSparseData: boolean; + next: never; // (undocumented) - recurse: never; + populateSparseData: boolean; // (undocumented) tableSchema: ListTableColumnDefinitions; } @@ -1976,7 +2001,7 @@ export interface TableVectorIndexOptions { } // @public -export const time: (time?: string | Date | DataAPITimeComponents) => DataAPITime; +export const time: (time?: string | Date | PartialDataAPITimeComponents) => DataAPITime; // @public export type TimedOutCategories = OneOrMany | 'provided'; diff --git a/scripts/playground.sh b/scripts/playground.sh index eb1aa73e..8fbcf01e 100755 --- a/scripts/playground.sh +++ b/scripts/playground.sh @@ -19,8 +19,7 @@ create) exit 1 fi - npm run build - npm pack + scripts/build.sh -no-report rm -rf etc/playgrounds/"$2" mkdir -p etc/playgrounds/"$2" @@ -30,8 +29,7 @@ create) npm i -D typescript tsx dotenv cp ../../../tsconfig.json . - npm i "${tarball_dir}"/datastax-astra-db-ts-*.tgz - rm "${tarball_dir}"/datastax-astra-db-ts-*.tgz + npm i "$tarball_dir" echo "import * as $ from '@datastax/astra-db-ts'; import dotenv from 'dotenv'; @@ -49,25 +47,6 @@ const table = db.table('test_table'); })();" > index.ts ;; -update) - if [ -z "$2" ]; then - echo "Usage: $0 update " - exit 1 - fi - - if [ ! -d etc/playgrounds/"$2" ]; then - echo "Playground '$2' not found" - exit 1 - fi - - npm run build - npm pack - - cd etc/playgrounds/"$2" || exit 1 - - npm i "${tarball_dir}"/datastax-astra-db-ts-*.tgz - rm "${tarball_dir}"/datastax-astra-db-ts-*.tgz - ;; destroy) if [ -z "$2" ]; then echo "Usage: $0 destroy " diff --git a/src/documents/collections/collection.ts b/src/documents/collections/collection.ts index 3368c8ad..52e58bda 100644 --- a/src/documents/collections/collection.ts +++ b/src/documents/collections/collection.ts @@ -52,7 +52,7 @@ import { CollectionFindCursor } from '@/src/documents/collections/cursor'; import { withJbiNullProtoFix } from '@/src/lib/utils'; import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; -const jbi = JBI({ storeAsString: true }); +const jbi = JBI; /** * #### Overview diff --git a/src/documents/collections/index.ts b/src/documents/collections/index.ts index 4a70fe04..21d38b0e 100644 --- a/src/documents/collections/index.ts +++ b/src/documents/collections/index.ts @@ -29,5 +29,13 @@ export { CollCodecs, CollCodec, } from './ser-des/codecs'; + +export { + CollNumRep, + GetCollNumRepFn, + NumCoercionError, + CollNumRepCfg, +} from './ser-des/big-nums'; + export { $DeserializeForCollection } from '@/src/documents/collections/ser-des/constants'; export { $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; diff --git a/src/documents/collections/ser-des/big-nums.ts b/src/documents/collections/ser-des/big-nums.ts new file mode 100644 index 00000000..74747001 --- /dev/null +++ b/src/documents/collections/ser-des/big-nums.ts @@ -0,0 +1,91 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import BigNumber from 'bignumber.js'; + +export type CollNumRep = + | 'number' + | 'bigint' + | 'bignumber' + | 'string' + | 'number_or_string'; + +export type GetCollNumRepFn = (path: string[]) => CollNumRep; + +export type CollNumRepCfg = Record; + +const $NumRep = Symbol('NumRep'); + +interface NumRepTree { + [$NumRep]?: CollNumRep; + [key: string]: NumRepTree; +} + +export const collNumRepFnFromCfg = (cfg: CollNumRepCfg): GetCollNumRepFn => { + const tree = buildNumRepTree(cfg); + + return (path: string[]) => { + return findMatchingPath(path, tree) ?? 'number'; + }; +}; + +const buildNumRepTree = (cfg: CollNumRepCfg): NumRepTree => { + const result: NumRepTree = {}; + + Object.entries(cfg).forEach(([path, rep]) => { + const keys = path.split('.'); + let current = result; + + keys.forEach((key, index) => { + current[key] ??= {}; + + if (index === keys.length - 1) { + current[key][$NumRep] = rep; + } + + current = current[key]; + }); + }); + + return result; +}; + +const findMatchingPath = (path: string[], tree: NumRepTree | undefined): CollNumRep | undefined => { + if (!tree) { + return undefined; + } + + if (path.length === 0) { + return tree[$NumRep]; + } + + const [key, ...rest] = path; + + return findMatchingPath(rest, tree[key]) ?? findMatchingPath(rest, tree['*']) ?? tree['*']?.[$NumRep]; +}; + +export class NumCoercionError extends Error { + public readonly path: string[]; + public readonly value: number | BigNumber; + public readonly from: 'number' | 'bignumber'; + public readonly to: CollNumRep; + + public constructor(path: string[], value: number | BigNumber, from: 'number' | 'bignumber', to: CollNumRep) { + super(`Failed to coerce value from ${from} to ${to} at path: ${path.join('.')}`); + this.path = path; + this.value = value; + this.from = from; + this.to = to; + } +} diff --git a/src/documents/collections/ser-des/ser-des.ts b/src/documents/collections/ser-des/ser-des.ts index 5aac0aea..0cc1a3f6 100644 --- a/src/documents/collections/ser-des/ser-des.ts +++ b/src/documents/collections/ser-des/ser-des.ts @@ -12,11 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { SerDes, BaseSerDesConfig } from '@/src/lib/api/ser-des/ser-des'; +import { BaseSerDesConfig, SerDes } from '@/src/lib/api/ser-des/ser-des'; import { BaseDesCtx, BaseSerCtx, CONTINUE } from '@/src/lib/api/ser-des/ctx'; import { CollCodecs, CollCodecSerDesFns } from '@/src/documents/collections/ser-des/codecs'; import { $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; import { isBigNumber, stringArraysEqual } from '@/src/lib/utils'; +import { CollNumRepCfg, GetCollNumRepFn } from '@/src/documents'; +import BigNumber from 'bignumber.js'; +import { collNumRepFnFromCfg, NumCoercionError } from '@/src/documents/collections/ser-des/big-nums'; /** * @public @@ -29,49 +32,103 @@ export interface CollSerCtx extends BaseSerCtx { * @public */ export interface CollDesCtx extends BaseDesCtx { - bigNumsEnabled: boolean, + getNumRepForPath?: GetCollNumRepFn, } /** * @public */ export interface CollectionSerDesConfig extends BaseSerDesConfig { - enableBigNumbers?: boolean, + enableBigNumbers?: GetCollNumRepFn | CollNumRepCfg, } /** * @internal */ export class CollectionSerDes extends SerDes { - declare protected readonly _cfg: CollectionSerDesConfig; + declare protected readonly _cfg: CollectionSerDesConfig & { enableBigNumbers?: GetCollNumRepFn }; + private readonly _getNumRepForPath: GetCollNumRepFn | undefined; public constructor(cfg?: CollectionSerDesConfig) { - super(CollectionSerDes.mergeConfig(DefaultCollectionSerDesCfg, cfg)); + super(CollectionSerDes.mergeConfig(DefaultCollectionSerDesCfg, cfg, cfg?.enableBigNumbers ? BigNumCollectionDesCfg : {})); + + this._getNumRepForPath = (typeof cfg?.enableBigNumbers === 'object') + ? collNumRepFnFromCfg(cfg.enableBigNumbers) + : cfg?.enableBigNumbers; } public override adaptSerCtx(ctx: CollSerCtx): CollSerCtx { - ctx.bigNumsEnabled = this._cfg?.enableBigNumbers === true; + ctx.bigNumsEnabled = !!this._getNumRepForPath; return ctx; } public override adaptDesCtx(ctx: CollDesCtx): CollDesCtx { - ctx.bigNumsEnabled = this._cfg?.enableBigNumbers === true; + ctx.getNumRepForPath = this._getNumRepForPath; return ctx; } public override bigNumsPresent(): boolean { - return this._cfg?.enableBigNumbers === true; + return !!this._cfg?.enableBigNumbers; } public static mergeConfig(...cfg: (CollectionSerDesConfig | undefined)[]): CollectionSerDesConfig { return { - enableBigNumbers: cfg.reduce((acc, c) => c?.enableBigNumbers ?? acc, undefined), + enableBigNumbers: cfg.reduce((acc, c) => c?.enableBigNumbers ?? acc, undefined), ...super._mergeConfig(...cfg), }; } } -const DefaultCollectionSerDesCfg = { +const BigNumCollectionDesCfg: CollectionSerDesConfig = { + deserialize(_, value, ctx) { + if (isBigNumber(value)) { + switch (ctx.getNumRepForPath!(ctx.path)) { + case 'number': { + const asNum = value.toNumber(); + + if (!value.isEqualTo(asNum)) { + throw new NumCoercionError(ctx.path, value, 'bignumber', 'number'); + } + + return ctx.next(asNum); + } + case 'bigint': { + if (!value.isInteger()) { + throw new NumCoercionError(ctx.path, value, 'bignumber', 'bigint'); + } + return ctx.next(BigInt(value.toFixed(0))); + } + case 'bignumber': + return ctx.next(value); + case 'string': + case 'number_or_string': + return ctx.next(value.toString()); + } + } + + if (typeof value === 'number') { + switch (ctx.getNumRepForPath!(ctx.path)) { + case 'bigint': { + if (!Number.isInteger(value)) { + throw new NumCoercionError(ctx.path, value, 'number', 'bigint'); + } + return ctx.next(BigInt(value)); + } + case 'bignumber': + return ctx.next(BigNumber(value)); + case 'string': + return ctx.next(value.toString()); + case 'number': + case 'number_or_string': + return ctx.next(value); + } + } + + return ctx.continue(); + }, +}; + +const DefaultCollectionSerDesCfg: CollectionSerDesConfig = { serialize(key, value, ctx) { const codecs = ctx.codecs; let resp; @@ -125,6 +182,7 @@ const DefaultCollectionSerDesCfg = { if (!ctx.bigNumsEnabled) { throw new Error('BigNumber serialization must be enabled through serdes.enableBigNumbers in CollectionSerDesConfig'); } + return ctx.done(); } return ctx.continue(); @@ -155,7 +213,11 @@ const DefaultCollectionSerDesCfg = { } } + if (typeof value === 'object' && isBigNumber(value) || value instanceof Date) { + return ctx.done(value); + } + return ctx.continue(); }, codecs: Object.values(CollCodecs.Defaults), -} satisfies CollectionSerDesConfig; +}; diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index a5ecd1b0..cb8ad57d 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -86,7 +86,7 @@ export class TableCodecs { map: TableCodecs.forType('map', { serializeClass: Map, serialize: (_, value, ctx) => { - return ctx.recurse(Object.fromEntries(value)); + return ctx.next(Object.fromEntries(value)); }, deserialize(_, map, ctx, def) { const entries = Array.isArray(map) ? map : Object.entries(map); @@ -118,7 +118,7 @@ export class TableCodecs { set: TableCodecs.forType('set', { serializeClass: Set, serialize: (_, value, ctx) => { - return ctx.recurse([...value]); + return ctx.next([...value]); }, deserialize(_, list, ctx, def) { for (let i = 0, n = list.length; i < n; i++) { diff --git a/src/documents/tables/ser-des/ser-des.ts b/src/documents/tables/ser-des/ser-des.ts index 177690f9..6c307afb 100644 --- a/src/documents/tables/ser-des/ser-des.ts +++ b/src/documents/tables/ser-des/ser-des.ts @@ -39,7 +39,7 @@ export interface TableSerCtx extends BaseSerCtx { export interface TableDesCtx extends BaseDesCtx { tableSchema: ListTableColumnDefinitions, populateSparseData: boolean, - recurse: never; + next: never; } /** diff --git a/src/lib/api/ser-des/ctx.ts b/src/lib/api/ser-des/ctx.ts index ed6c8df8..2da2e944 100644 --- a/src/lib/api/ser-des/ctx.ts +++ b/src/lib/api/ser-des/ctx.ts @@ -39,18 +39,18 @@ export interface BaseSerDesCtx { rootObj: SomeDoc, path: string[], done(obj?: T): readonly [0, T?], - recurse(obj?: T): readonly [1, T?], + next(obj?: T): readonly [1, T?], continue(): readonly [2], codecs: Codecs, keyTransformer?: KeyTransformer, } export const DONE = 0 as const; -export const RECURSE = 1 as const; +export const NEXT = 1 as const; export const CONTINUE = 2 as const; const DONE_ARR = [DONE] as const; -const RECURSE_ARR = [RECURSE] as const; +const RECURSE_ARR = [NEXT] as const; const CONTINUE_ARR = [CONTINUE] as const; /** @@ -66,9 +66,9 @@ export function ctxDone(obj?: T): readonly [0, T?] { /** * @internal */ -export function ctxRecurse(obj?: T): readonly [1, T?] { +export function ctxNext(obj?: T): readonly [1, T?] { if (arguments.length === 1) { - return [RECURSE, obj]; + return [NEXT, obj]; } return RECURSE_ARR; } diff --git a/src/lib/api/ser-des/ser-des.ts b/src/lib/api/ser-des/ser-des.ts index 5e11e953..842f05e4 100644 --- a/src/lib/api/ser-des/ser-des.ts +++ b/src/lib/api/ser-des/ser-des.ts @@ -22,7 +22,7 @@ import { BaseSerDesCtx, ctxContinue, ctxDone, - ctxRecurse, + ctxNext, DONE, } from '@/src/lib/api/ser-des/ctx'; @@ -56,15 +56,28 @@ export abstract class SerDes(obj: SomeDoc | nullish, raw: RawDataAPIResponse, parsingId = false): S { if (obj === null || obj === undefined) { return obj as S; } - const ctx = this.adaptDesCtx(this._mkCtx(obj, { rawDataApiResp: raw, keys: [], parsingInsertedId: parsingId })); + + const ctx = this.adaptDesCtx(this._mkCtx(obj, { + parsingInsertedId: parsingId, + rawDataApiResp: raw, + keys: [], + })); + return deserializeRecord('', { ['']: ctx.rootObj }, ctx, toArray(this._cfg.deserialize!))[''] as S; } @@ -85,7 +98,7 @@ export abstract class SerDes(obj: SomeDoc, ctx: Ctx): Ctx & BaseSerDesCtx { return { done: ctxDone, - recurse: ctxRecurse, + next: ctxNext, continue: ctxContinue, codecs: this._codecs, keyTransformer: this._cfg.keyTransformer, @@ -181,17 +194,17 @@ function deserializeRecordHelper>(keys: string[], ob } function applySerdesFns(fns: readonly SerDesFn[], key: string, obj: SomeDoc, ctx: Ctx): boolean { - let stop: unknown; - - for (let f = 0; f < fns.length && !stop; f++) { - const res = fns[f](key, obj[key], ctx) as [number] | [number, any]; - - stop = res?.[0] === DONE; + for (let f = 0; f < fns.length; f++) { + const res = fns[f](key, obj[key], ctx) as [number] | [number, unknown]; if (res.length === 2) { obj[key] = res[1]; } + + if (res?.[0] === DONE) { + return true; + } } - return !!stop; + return false; } diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 521bacc2..cf2aa9c5 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -78,7 +78,9 @@ function nullProtoFix(doc: SomeDoc): SomeDoc { } } } else { - doc = Object.assign({}, doc); + if (Object.getPrototypeOf(doc) === null) { + Object.setPrototypeOf(doc, {}); + } for (const key of Object.keys(doc)) { if (typeof doc[key] === 'object' && doc[key] !== null) { diff --git a/tests/integration/documents/collections/ser-des/key-transformer.test.ts b/tests/integration/documents/collections/ser-des/key-transformer.test.ts index 237965eb..ba24f301 100644 --- a/tests/integration/documents/collections/ser-des/key-transformer.test.ts +++ b/tests/integration/documents/collections/ser-des/key-transformer.test.ts @@ -56,7 +56,7 @@ describe('integration.documents.collections.ser-des.key-transformer', ({ db }) = serdes: { keyTransformer: new Camel2SnakeCase(), codecs: [CollCodecs.forName('camelCaseName2', Newtype)], - enableBigNumbers: true, + enableBigNumbers: () => 'bigint', }, }), })); diff --git a/tests/unit/documents/collections/ser-des/key-transformer.test.ts b/tests/unit/documents/collections/ser-des/key-transformer.test.ts index 6a65e7b2..0b90831d 100644 --- a/tests/unit/documents/collections/ser-des/key-transformer.test.ts +++ b/tests/unit/documents/collections/ser-des/key-transformer.test.ts @@ -20,7 +20,7 @@ import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; describe('unit.documents.collections.ser-des.key-transformer', () => { describe('Camel2SnakeCase', () => { - const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase(), enableBigNumbers: true }); + const serdes = new CollectionSerDes({ keyTransformer: new Camel2SnakeCase(), enableBigNumbers: () => 'bigint' }); it('should serialize top-level keys to snake_case for collections', () => { const [obj] = serdes.serialize({ From ba18893b18edf5027fc11352036a2c31bffef069 Mon Sep 17 00:00:00 2001 From: toptobes Date: Mon, 6 Jan 2025 21:31:43 +0530 Subject: [PATCH 26/44] unit tests for extended bignum support --- etc/astra-db-ts.api.md | 3 +- src/documents/collections/ser-des/big-nums.ts | 17 ++ .../documents/tables/datatypes.test.ts | 4 +- .../ser-des/enable-big-numbers.test.ts | 198 ++++++++++++++++++ 4 files changed, 219 insertions(+), 3 deletions(-) create mode 100644 tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index d3c06037..d248db75 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -599,7 +599,7 @@ export type CollectionReplaceOneResult = GenericUpdateResult { // (undocumented) - enableBigNumbers?: GetCollNumRepFn; + enableBigNumbers?: GetCollNumRepFn | CollNumRepCfg; } // @public @@ -1622,6 +1622,7 @@ export type nullish = null | undefined; // @public (undocumented) export class NumCoercionError extends Error { + // @internal constructor(path: string[], value: number | BigNumber, from: 'number' | 'bignumber', to: CollNumRep); // (undocumented) readonly from: 'number' | 'bignumber'; diff --git a/src/documents/collections/ser-des/big-nums.ts b/src/documents/collections/ser-des/big-nums.ts index 74747001..c1b08223 100644 --- a/src/documents/collections/ser-des/big-nums.ts +++ b/src/documents/collections/ser-des/big-nums.ts @@ -14,6 +14,9 @@ import BigNumber from 'bignumber.js'; +/** + * @public + */ export type CollNumRep = | 'number' | 'bigint' @@ -21,8 +24,14 @@ export type CollNumRep = | 'string' | 'number_or_string'; +/** + * @public + */ export type GetCollNumRepFn = (path: string[]) => CollNumRep; +/** + * @public + */ export type CollNumRepCfg = Record; const $NumRep = Symbol('NumRep'); @@ -75,12 +84,20 @@ const findMatchingPath = (path: string[], tree: NumRepTree | undefined): CollNum return findMatchingPath(rest, tree[key]) ?? findMatchingPath(rest, tree['*']) ?? tree['*']?.[$NumRep]; }; +/** + * @public + */ export class NumCoercionError extends Error { public readonly path: string[]; public readonly value: number | BigNumber; public readonly from: 'number' | 'bignumber'; public readonly to: CollNumRep; + /** + * Should not be instantiated by the user. + * + * @internal + */ public constructor(path: string[], value: number | BigNumber, from: 'number' | 'bignumber', to: CollNumRep) { super(`Failed to coerce value from ${from} to ${to} at path: ${path.join('.')}`); this.path = path; diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts index 481063b1..aa934a10 100644 --- a/tests/integration/documents/tables/datatypes.test.ts +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -285,8 +285,8 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.notOk('12:34:56Z+05:30'); await colAsserter.notOk(3123123); - await colAsserter.ok('23:59:00.', _ => time('23:00:00')); // S - await colAsserter.ok('23:59', _ => time('23:59:00')); // S + await colAsserter.ok('23:59:00.', _ => time('23:00:00')); // S + await colAsserter.ok('23:59', _ => time('23:59:00')); // S await colAsserter.ok('00:00:00.000000000', time); await colAsserter.ok(time('23:59:59.999999999')); await colAsserter.ok(time(new Date('1970-01-01T23:59:59.999Z')), _ => time('23:59:59.999')); diff --git a/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts b/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts new file mode 100644 index 00000000..81b577c7 --- /dev/null +++ b/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts @@ -0,0 +1,198 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { describe, it } from '@/tests/testlib'; +import assert from 'assert'; +import { CollectionSerDes } from '@/src/documents/collections/ser-des/ser-des'; +import BigNumber from 'bignumber.js'; +import { CollNumRep, NumCoercionError } from '@/src/documents'; + +describe('unit.documents.collections.ser-des.enable-big-numbers', () => { + describe('coercions', () => { + const mkDesAsserter = (type: CollNumRep, coerce: (n: BigNumber | number) => unknown) => ({ + _serdesFn: new CollectionSerDes({ enableBigNumbers: () => type }), + _serdesCfg: new CollectionSerDes({ enableBigNumbers: { '*': type } }), + ok(n: BigNumber | number) { + assert.deepStrictEqual(this._serdesFn.deserialize({ n }, null!), { n: coerce(n) }); + assert.deepStrictEqual(this._serdesCfg.deserialize({ n }, null!), { n: coerce(n) }); + }, + notOk(n: BigNumber | number) { + assert.throws(() => this._serdesFn.deserialize({ n }, null!), NumCoercionError); + assert.throws(() => this._serdesCfg.deserialize({ n }, null!), NumCoercionError); + }, + }); + + it('should handle to-number coercion properly', () => { + const desAsserter = mkDesAsserter('number', Number); + + desAsserter.ok(0); + desAsserter.ok(123); + desAsserter.ok(-123.123); + desAsserter.ok(BigNumber(123)); + desAsserter.ok(BigNumber(-123.123)); + + desAsserter.notOk(BigNumber('120213123123123123213213')); + desAsserter.notOk(BigNumber('12.213123123123123213213')); + }); + + it('should handle to-bigint coercion properly', () => { + const desAsserter = mkDesAsserter('bigint', (n) => BigInt(n.toFixed())); + + desAsserter.ok(0); + desAsserter.ok(123); + desAsserter.ok(-123); + desAsserter.ok(BigNumber('1200213123123123123213213')); + desAsserter.ok(BigNumber('-120213123123123123213213')); + + desAsserter.notOk(BigNumber('12.213123123123123213213')); + desAsserter.notOk(.1); + }); + + it('should handle to-bignumber coercion properly', () => { + const desAsserter = mkDesAsserter('bignumber', (n) => BigNumber(n)); + + desAsserter.ok(0); + desAsserter.ok(123); + desAsserter.ok(-123); + desAsserter.ok(BigNumber('1200213123123123123213213')); + desAsserter.ok(BigNumber('-120213123123123123213213')); + desAsserter.ok(BigNumber('12.213123123123123213213')); + desAsserter.ok(.1); + }); + + it('should handle to-string coercion properly', () => { + const desAsserter = mkDesAsserter('string', (n) => n.toString()); + + desAsserter.ok(0); + desAsserter.ok(123); + desAsserter.ok(-123); + desAsserter.ok(BigNumber('1200213123123123123213213')); + desAsserter.ok(BigNumber('-120213123123123123213213')); + desAsserter.ok(BigNumber('12.213123123123123213213')); + desAsserter.ok(.1); + }); + + it('should handle to-number_or_string coercion properly', () => { + const desAsserterNum = mkDesAsserter('number_or_string', Number); + const desAsserterStr = mkDesAsserter('number_or_string', (n) => n.toString()); + + desAsserterNum.ok(0); + desAsserterNum.ok(123); + desAsserterNum.ok(-123); + desAsserterStr.ok(BigNumber('1200213123123123123213213')); + desAsserterStr.ok(BigNumber('-120213123123123123213213')); + desAsserterStr.ok(BigNumber('12.213123123123123213213')); + desAsserterNum.ok(.1); + }); + }); + + const TestObjAct1 = () => ({ + root0: 0, + root1: BigNumber('12.12312321312312312312312321'), // why long des when GetCollNumRepFn + stats: { + stats0: -123, + stats1: BigNumber('12321321321312312321312312321'), + cars: [ + { a: BigNumber('-123.123') }, + { a: BigNumber('-123.123') }, + { a: -123.123 }, + ], + mars: { + stars: [{ bars: BigNumber(0) }, { czars: 1 }], + }, + }, + bats: { + mars: { + stars: [{ bars: 2 }, { czars: BigNumber(3) }], + }, + }, + }); + + const TestObjExp1 = { + root0: 0, + root1: '12.12312321312312312312312321', + stats: { + stats0: -123n, + stats1: 12321321321312312321312312321n, + cars: [ + { a: -123.123 }, + { a: BigNumber('-123.123') }, + { a: BigNumber('-123.123') }, + ], + mars: { + stars: [{ bars: 0 }, { czars: 1n }], + }, + }, + bats: { + mars: { + stars: [{ bars: 2 }, { czars: '3' }], + }, + }, + }; + + const TestObjAct2 = () => ({ + stats: 123, + }); + + const TestObjExp2 = { + stats: '123', + }; + + it('should work with a GetCollNumRepFn', () => { + const serdes = new CollectionSerDes({ + enableBigNumbers: (path: string[]) => { + if (path[0] !== 'stats') { + return 'number_or_string'; + } + + if (path.length === 1) { + return 'string'; + } + + if (path[1] === 'cars' && path[3] === 'a') { + if (path[2] === '0') { + return 'number'; + } + return 'bignumber'; + } + + if (path[2] === 'stars' && path[4] === 'bars') { + return 'number'; + } + + return 'bigint'; + }, + }); + assert.deepStrictEqual(serdes.deserialize(TestObjAct1(), null!), TestObjExp1); + assert.deepStrictEqual(serdes.deserialize(TestObjAct2(), null!), TestObjExp2); + assert.deepStrictEqual(serdes.deserialize({}, null!), {}); + }); + + it('should work with a CollNumRepCfg', () => { + const serdes = new CollectionSerDes({ + enableBigNumbers: { + '*': 'number_or_string', + 'stats': 'string', + 'stats.*': 'bigint', + 'stats.cars.0.a': 'number', + 'stats.cars.*.a': 'bignumber', + 'stats.*.stars.*.bars': 'number', + }, + }); + assert.deepStrictEqual(serdes.deserialize(TestObjAct1(), null!), TestObjExp1); + assert.deepStrictEqual(serdes.deserialize(TestObjAct2(), null!), TestObjExp2); + assert.deepStrictEqual(serdes.deserialize({}, null!), {}); + }); +}); From abbe0df2e5a6bcbe97b14d97c0bb8a66aebbfe1e Mon Sep 17 00:00:00 2001 From: toptobes Date: Tue, 7 Jan 2025 09:26:08 +0530 Subject: [PATCH 27/44] little bit of cleanup --- src/documents/collections/ser-des/big-nums.ts | 67 ++++++++++++++++--- src/documents/collections/ser-des/ser-des.ts | 46 ++----------- src/lib/api/ser-des/ser-des.ts | 2 +- 3 files changed, 65 insertions(+), 50 deletions(-) diff --git a/src/documents/collections/ser-des/big-nums.ts b/src/documents/collections/ser-des/big-nums.ts index c1b08223..a72108fe 100644 --- a/src/documents/collections/ser-des/big-nums.ts +++ b/src/documents/collections/ser-des/big-nums.ts @@ -13,6 +13,7 @@ // limitations under the License. import BigNumber from 'bignumber.js'; +import { CollDesCtx } from '@/src/documents'; /** * @public @@ -71,17 +72,24 @@ const buildNumRepTree = (cfg: CollNumRepCfg): NumRepTree => { }; const findMatchingPath = (path: string[], tree: NumRepTree | undefined): CollNumRep | undefined => { - if (!tree) { - return undefined; - } + let rep: CollNumRep | undefined = undefined; - if (path.length === 0) { - return tree[$NumRep]; - } + for (let i = 0; tree && i <= path.length; i++) { + if (i === path.length) { + return tree[$NumRep]; + } - const [key, ...rest] = path; + const exactMatch = tree[path[i]]; + + if (exactMatch) { + tree = exactMatch; + } else { + tree = tree['*']; + rep = tree?.[$NumRep] ?? rep; + } + } - return findMatchingPath(rest, tree[key]) ?? findMatchingPath(rest, tree['*']) ?? tree['*']?.[$NumRep]; + return rep; }; /** @@ -106,3 +114,46 @@ export class NumCoercionError extends Error { this.to = to; } } + +export const coerceBigNumber = (value: BigNumber, ctx: CollDesCtx): readonly [0 | 1 | 2, unknown?] => { + switch (ctx.getNumRepForPath!(ctx.path)) { + case 'number': { + const asNum = value.toNumber(); + + if (!value.isEqualTo(asNum)) { + throw new NumCoercionError(ctx.path, value, 'bignumber', 'number'); + } + + return ctx.next(asNum); + } + case 'bigint': { + if (!value.isInteger()) { + throw new NumCoercionError(ctx.path, value, 'bignumber', 'bigint'); + } + return ctx.next(BigInt(value.toFixed(0))); + } + case 'bignumber': + return ctx.next(value); + case 'string': + case 'number_or_string': + return ctx.next(value.toString()); + } +}; + +export const coerceNumber = (value: number, ctx: CollDesCtx): readonly [0 | 1 | 2, unknown?] => { + switch (ctx.getNumRepForPath!(ctx.path)) { + case 'bigint': { + if (!Number.isInteger(value)) { + throw new NumCoercionError(ctx.path, value, 'number', 'bigint'); + } + return ctx.next(BigInt(value)); + } + case 'bignumber': + return ctx.next(BigNumber(value)); + case 'string': + return ctx.next(value.toString()); + case 'number': + case 'number_or_string': + return ctx.next(value); + } +}; diff --git a/src/documents/collections/ser-des/ser-des.ts b/src/documents/collections/ser-des/ser-des.ts index 0cc1a3f6..3706e222 100644 --- a/src/documents/collections/ser-des/ser-des.ts +++ b/src/documents/collections/ser-des/ser-des.ts @@ -18,8 +18,7 @@ import { CollCodecs, CollCodecSerDesFns } from '@/src/documents/collections/ser- import { $SerializeForCollection } from '@/src/documents/collections/ser-des/constants'; import { isBigNumber, stringArraysEqual } from '@/src/lib/utils'; import { CollNumRepCfg, GetCollNumRepFn } from '@/src/documents'; -import BigNumber from 'bignumber.js'; -import { collNumRepFnFromCfg, NumCoercionError } from '@/src/documents/collections/ser-des/big-nums'; +import { coerceBigNumber, coerceNumber, collNumRepFnFromCfg } from '@/src/documents/collections/ser-des/big-nums'; /** * @public @@ -81,47 +80,12 @@ export class CollectionSerDes extends SerDes = (key: string, value: any, ctx: Ctx) => readonly [0 | 1 | 2, any?, string?] | 'Return ctx.done(val?), ctx.recurse(val?), ctx.continue(), or void'; +export type SerDesFn = (key: string, value: any, ctx: Ctx) => readonly [0 | 1 | 2, any?] | 'Return ctx.done(val?), ctx.recurse(val?), ctx.continue(), or void'; /** * @public From 5152d63816cfe631c58a4988b54fe36a9ec2e56e Mon Sep 17 00:00:00 2001 From: toptobes Date: Tue, 7 Jan 2025 21:27:01 +0530 Subject: [PATCH 28/44] more work on collections bignumber support --- src/documents/collections/ser-des/big-nums.ts | 6 +- src/lib/utils.ts | 2 +- .../administration/lifecycle.test.ts | 10 +- .../ser-des/enable-big-numbers.test.ts | 240 ++++++++++++++++++ .../ser-des/key-transformer.test.ts | 31 ++- .../tables/ser-des/key-transformer.test.ts | 5 +- .../ser-des/enable-big-numbers.test.ts | 98 ------- 7 files changed, 269 insertions(+), 123 deletions(-) create mode 100644 tests/integration/documents/collections/ser-des/enable-big-numbers.test.ts diff --git a/src/documents/collections/ser-des/big-nums.ts b/src/documents/collections/ser-des/big-nums.ts index a72108fe..4e733e89 100644 --- a/src/documents/collections/ser-des/big-nums.ts +++ b/src/documents/collections/ser-des/big-nums.ts @@ -28,7 +28,7 @@ export type CollNumRep = /** * @public */ -export type GetCollNumRepFn = (path: string[]) => CollNumRep; +export type GetCollNumRepFn = (path: readonly string[]) => CollNumRep; /** * @public @@ -45,7 +45,7 @@ interface NumRepTree { export const collNumRepFnFromCfg = (cfg: CollNumRepCfg): GetCollNumRepFn => { const tree = buildNumRepTree(cfg); - return (path: string[]) => { + return (path: readonly string[]) => { return findMatchingPath(path, tree) ?? 'number'; }; }; @@ -71,7 +71,7 @@ const buildNumRepTree = (cfg: CollNumRepCfg): NumRepTree => { return result; }; -const findMatchingPath = (path: string[], tree: NumRepTree | undefined): CollNumRep | undefined => { +const findMatchingPath = (path: readonly string[], tree: NumRepTree | undefined): CollNumRep | undefined => { let rep: CollNumRep | undefined = undefined; for (let i = 0; tree && i <= path.length; i++) { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index cf2aa9c5..abf0ca8f 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -79,7 +79,7 @@ function nullProtoFix(doc: SomeDoc): SomeDoc { } } else { if (Object.getPrototypeOf(doc) === null) { - Object.setPrototypeOf(doc, {}); + doc = { ...doc }; } for (const key of Object.keys(doc)) { diff --git a/tests/integration/administration/lifecycle.test.ts b/tests/integration/administration/lifecycle.test.ts index 5fd0e4eb..747e9c3f 100644 --- a/tests/integration/administration/lifecycle.test.ts +++ b/tests/integration/administration/lifecycle.test.ts @@ -51,7 +51,9 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl assert.ok(['PENDING', 'INITIALIZING'].includes(dbInfo1.status)); assert.strictEqual(dbInfo1.name, TEMP_DB_NAME); assert.strictEqual(dbInfo1.cloudProvider, 'GCP'); - assert.deepStrictEqual(dbInfo1.regions, [{ name: 'us-east1', apiEndpoint: buildAstraEndpoint(dbInfo1.id, 'us-east1') }]); + assert.deepStrictEqual(dbInfo1.regions.length, 1); + assert.deepStrictEqual({ ...dbInfo1.regions[0], createdAt: 0 }, { name: 'us-east1', apiEndpoint: buildAstraEndpoint(dbInfo1.id, 'us-east1'), createdAt: 0 }); + assert.ok(dbInfo1.regions[0].createdAt as unknown instanceof Date); assert.deepStrictEqual(dbInfo1.keyspaces, ['my_keyspace']); const dbInfo2 = await admin.dbInfo(asyncDb.id); @@ -72,7 +74,7 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl assert.strictEqual(event.method, HttpMethods.Post); assert.strictEqual(event.longRunning, true); assert.strictEqual(event.params, undefined); - assert.strictEqual(event.timeout, 720000); + assert.deepStrictEqual(event.timeout, { databaseAdminTimeoutMs: 720000, requestTimeoutMs: 60000 }); }); client.on('adminCommandPolling', (event) => { @@ -139,7 +141,9 @@ background('(ADMIN) (LONG) (NOT-DEV) (ASTRA) integration.administration.lifecycl assert.strictEqual(dbInfo.status, 'ACTIVE'); assert.strictEqual(dbInfo.name, TEMP_DB_NAME); assert.strictEqual(dbInfo.cloudProvider, 'GCP'); - assert.deepStrictEqual(dbInfo.regions, [{ name: 'us-east1', apiEndpoint: buildAstraEndpoint(dbInfo.id, 'us-east1') }]); + assert.deepStrictEqual(dbInfo.regions.length, 1); + assert.deepStrictEqual({ ...dbInfo.regions[0], createdAt: 0 }, { name: 'us-east1', apiEndpoint: buildAstraEndpoint(dbInfo.id, 'us-east1'), createdAt: 0 }); + assert.ok(dbInfo.regions[0].createdAt as unknown instanceof Date); assert.deepStrictEqual(dbInfo.keyspaces, [db.keyspace]); const collections1 = await db.listCollections({ nameOnly: true }); diff --git a/tests/integration/documents/collections/ser-des/enable-big-numbers.test.ts b/tests/integration/documents/collections/ser-des/enable-big-numbers.test.ts new file mode 100644 index 00000000..7bfb56c7 --- /dev/null +++ b/tests/integration/documents/collections/ser-des/enable-big-numbers.test.ts @@ -0,0 +1,240 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import { DEFAULT_COLLECTION_NAME, it, parallel } from '@/tests/testlib'; +import assert from 'assert'; +import BigNumber from 'bignumber.js'; +import { + $DeserializeForCollection, + $SerializeForCollection, + CollCodec, + CollCodecs, + CollDesCtx, + CollNumRepCfg, + CollSerCtx, + GetCollNumRepFn, + uuid, +} from '@/src/documents'; + +parallel('integration.documents.collections.ser-des.enable-big-numbers', ({ db }) => { + const TestObjAct1 = (key: string) => ({ + _id: key, + root0: 0, + root1: BigNumber('12.12312321312312312312312321'), // why long des when GetCollNumRepFn + stats: { + stats0: -123, + stats1: BigNumber('12321321321312312321312312321'), + cars: [ + { a: BigNumber('-123.123') }, + { a: BigNumber('-123.123') }, + { a: -123.123 }, + ], + mars: { + stars: [{ bars: BigNumber(0) }, { czars: 1 }], + }, + }, + bats: { + mars: { + stars: [{ bars: 2 }, { czars: 9007199254740991n }], + }, + }, + }); + + const TestObjExp1 = (key: string) => ({ + _id: key, + root0: 0, + root1: '12.12312321312312312312312321', + stats: { + stats0: -123n, + stats1: 12321321321312312321312312321n, + cars: [ + { a: -123.123 }, + { a: BigNumber('-123.123') }, + { a: BigNumber('-123.123') }, + ], + mars: { + stars: [{ bars: 0 }, { czars: 1n }], + }, + }, + bats: { + mars: { + stars: [{ bars: 2 }, { czars: '9007199254740991' }], + }, + }, + }); + + const TestObjExp1d = (key: string) => ({ + _id: key, + root0: BigNumber(0), + root1: BigNumber('12.12312321312312312312312321'), + stats: { + stats0: BigNumber(-123), + stats1: BigNumber(12321321321312312321312312321n.toString()), + cars: [ + { a: BigNumber(-123.123) }, + { a: BigNumber('-123.123') }, + { a: BigNumber('-123.123') }, + ], + mars: { + stars: [{ bars: BigNumber(0) }, { czars: BigNumber(1) }], + }, + }, + bats: { + mars: { + stars: [{ bars: BigNumber(2) }, { czars: BigNumber('9007199254740991') }], + }, + }, + }); + + const TestObjAct2 = (key: string) => ({ + _id: key, + stats: 123, + }); + + const TestObjExp2 = (key: string) => ({ + _id: key, + stats: '123', + }); + + const TestObjExp2d = (key: string) => ({ + _id: key, + stats: BigNumber('123'), + }); + + const TestObjAct3 = (key: string) => ({ + _id: key, + }); + + const TestObjExp3 = (key: string) => ({ + _id: key, + }); + + const TestObjExp3d = (key: string) => ({ + _id: key, + }); + + class Newtype implements CollCodec { + constructor(public unwrap: unknown) {} + + [$SerializeForCollection](ctx: CollSerCtx) { + return ctx.done(this.unwrap); + } + + static [$DeserializeForCollection](_: unknown, value: string, ctx: CollDesCtx) { + return ctx.done(new Newtype(value)); + } + } + + const TestObjAct4 = (key: string) => ({ + _id: key, + root0: new Newtype(BigNumber(3)), + root1: new Newtype(9007199254740991n), + stats: { + value: new Newtype(3), + }, + }); + + const TestObjExp4 = (key: string) => ({ + _id: key, + root0: 3, + root1: '9007199254740991', + stats: { + value: 3n, + }, + }); + + const TestObjExp4d = (key: string) => ({ + _id: key, + root0: BigNumber(3), + root1: BigNumber('9007199254740991'), + stats: { + value: BigNumber(3), + }, + }); + + const mkAsserter = (opts: GetCollNumRepFn | CollNumRepCfg) => ({ + coll: db.collection(DEFAULT_COLLECTION_NAME, { + serdes: { + codecs: [CollCodecs.forName('camelCaseName2', Newtype)], + enableBigNumbers: opts, + }, + }), + async ok(exp: (key: string) => object, act: (key: string) => object) { + const key = uuid(4).toString(); + const { insertedId } = await this.coll.insertOne(act(key)); + assert.deepStrictEqual(await this.coll.findOne({ _id: insertedId }), exp(key)); + }, + }); + + it('should work with a GetCollNumRepFn', async () => { + const asserter = mkAsserter((path: readonly string[]) => { + if (path[0] !== 'stats') { + return 'number_or_string'; + } + + if (path.length === 1) { + return 'string'; + } + + if (path[1] === 'cars' && path[3] === 'a') { + if (path[2] === '0') { + return 'number'; + } + return 'bignumber'; + } + + if (path[2] === 'stars' && path[4] === 'bars') { + return 'number'; + } + + return 'bigint'; + }); + await asserter.ok(TestObjExp1, TestObjAct1); + await asserter.ok(TestObjExp2, TestObjAct2); + await asserter.ok(TestObjExp3, TestObjAct3); + await asserter.ok(TestObjExp4, TestObjAct4); + }); + + it('should allow a universal default with a GetCollNumRepFn', async () => { + const asserter = mkAsserter(() => 'bignumber'); + await asserter.ok(TestObjExp1d, TestObjAct1); + await asserter.ok(TestObjExp2d, TestObjAct2); + await asserter.ok(TestObjExp3d, TestObjAct3); + await asserter.ok(TestObjExp4d, TestObjAct4); + }); + + it('should work with a CollNumRepCfg', async () => { + const asserter = mkAsserter({ + '*': 'number_or_string', + 'stats': 'string', + 'stats.*': 'bigint', + 'stats.cars.0.a': 'number', + 'stats.cars.*.a': 'bignumber', + 'stats.*.stars.*.bars': 'number', + }); + await asserter.ok(TestObjExp1, TestObjAct1); + await asserter.ok(TestObjExp2, TestObjAct2); + await asserter.ok(TestObjExp3, TestObjAct3); + await asserter.ok(TestObjExp4, TestObjAct4); + }); + + it('should allow a universal default with a GetCollNumRepFn', async () => { + const asserter = mkAsserter({ '*': 'bignumber' }); + await asserter.ok(TestObjExp1d, TestObjAct1); + await asserter.ok(TestObjExp2d, TestObjAct2); + await asserter.ok(TestObjExp3d, TestObjAct3); + await asserter.ok(TestObjExp4d, TestObjAct4); + }); +}); diff --git a/tests/integration/documents/collections/ser-des/key-transformer.test.ts b/tests/integration/documents/collections/ser-des/key-transformer.test.ts index ba24f301..06d6bb69 100644 --- a/tests/integration/documents/collections/ser-des/key-transformer.test.ts +++ b/tests/integration/documents/collections/ser-des/key-transformer.test.ts @@ -13,7 +13,7 @@ // limitations under the License. // noinspection DuplicatedCode -import { describe, it, useSuiteResources } from '@/tests/testlib'; +import { DEFAULT_COLLECTION_NAME, describe, it, parallel } from '@/tests/testlib'; import { Camel2SnakeCase } from '@/src/lib'; import assert from 'assert'; import { @@ -26,6 +26,7 @@ import { uuid, UUID, } from '@/src/index'; +import BigNumber from 'bignumber.js'; describe('integration.documents.collections.ser-des.key-transformer', ({ db }) => { class Newtype implements CollCodec { @@ -40,43 +41,41 @@ describe('integration.documents.collections.ser-des.key-transformer', ({ db }) = } } - describe('Camel2SnakeCase', { drop: 'colls:after' }, () => { + parallel('Camel2SnakeCase', () => { interface SnakeCaseTest { _id: UUID, camelCase1: string, camelCaseName2: Newtype, CamelCaseName3: string[], _CamelCaseName4: Record, - camelCaseName5_: bigint, + camelCaseName5_: BigNumber | number, name: string[], } - const coll = useSuiteResources(() => ({ - ref: db.createCollection('test_camel_snake_case_coll', { - serdes: { - keyTransformer: new Camel2SnakeCase(), - codecs: [CollCodecs.forName('camelCaseName2', Newtype)], - enableBigNumbers: () => 'bigint', - }, - }), - })); + const coll = db.collection(DEFAULT_COLLECTION_NAME, { + serdes: { + keyTransformer: new Camel2SnakeCase(), + codecs: [CollCodecs.forName('camelCaseName2', Newtype)], + enableBigNumbers: () => 'bignumber', + }, + }); it('should work', async () => { const id = uuid(4); - const { insertedId } = await coll.ref.insertOne({ + const { insertedId } = await coll.insertOne({ _id: id, camelCase1: 'dontChange_me', camelCaseName2: new Newtype('dontChange_me'), CamelCaseName3: ['dontChange_me'], _CamelCaseName4: { dontChange_me: 'dontChange_me' }, - camelCaseName5_: 123n, + camelCaseName5_: 123, name: ['dontChange_me'], }); assert.deepStrictEqual(insertedId, id); - const result = await coll.ref.findOne({ _id: insertedId }); + const result = await coll.findOne({ _id: insertedId }); assert.deepStrictEqual(result, { _id: id, @@ -84,7 +83,7 @@ describe('integration.documents.collections.ser-des.key-transformer', ({ db }) = camelCaseName2: new Newtype('dontChange_me'), CamelCaseName3: ['dontChange_me'], _CamelCaseName4: { dontChange_me: 'dontChange_me' }, - camelCaseName5_: 123, + camelCaseName5_: BigNumber(123), name: ['dontChange_me'], }); }); diff --git a/tests/integration/documents/tables/ser-des/key-transformer.test.ts b/tests/integration/documents/tables/ser-des/key-transformer.test.ts index e4ca3e71..de2f8474 100644 --- a/tests/integration/documents/tables/ser-des/key-transformer.test.ts +++ b/tests/integration/documents/tables/ser-des/key-transformer.test.ts @@ -13,7 +13,7 @@ // limitations under the License. // noinspection DuplicatedCode -import { describe, it, useSuiteResources } from '@/tests/testlib'; +import { describe, it, parallel, useSuiteResources } from '@/tests/testlib'; import { Camel2SnakeCase } from '@/src/lib'; import assert from 'assert'; import { @@ -38,7 +38,7 @@ describe('integration.documents.tables.ser-des.key-transformer', ({ db }) => { } } - describe('Camel2SnakeCase', { drop: 'tables:after' }, () => { + parallel('Camel2SnakeCase', { drop: 'tables:after' }, () => { interface SnakeCaseTest { camelCase1: string, camelCaseName2: Newtype, @@ -68,6 +68,7 @@ describe('integration.documents.tables.ser-des.key-transformer', ({ db }) => { keyTransformer: new Camel2SnakeCase(), codecs: [TableCodecs.forName('camelCaseName2', Newtype)], }, + ifNotExists: true, }), })); diff --git a/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts b/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts index 81b577c7..31367ce9 100644 --- a/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts +++ b/tests/unit/documents/collections/ser-des/enable-big-numbers.test.ts @@ -97,102 +97,4 @@ describe('unit.documents.collections.ser-des.enable-big-numbers', () => { desAsserterNum.ok(.1); }); }); - - const TestObjAct1 = () => ({ - root0: 0, - root1: BigNumber('12.12312321312312312312312321'), // why long des when GetCollNumRepFn - stats: { - stats0: -123, - stats1: BigNumber('12321321321312312321312312321'), - cars: [ - { a: BigNumber('-123.123') }, - { a: BigNumber('-123.123') }, - { a: -123.123 }, - ], - mars: { - stars: [{ bars: BigNumber(0) }, { czars: 1 }], - }, - }, - bats: { - mars: { - stars: [{ bars: 2 }, { czars: BigNumber(3) }], - }, - }, - }); - - const TestObjExp1 = { - root0: 0, - root1: '12.12312321312312312312312321', - stats: { - stats0: -123n, - stats1: 12321321321312312321312312321n, - cars: [ - { a: -123.123 }, - { a: BigNumber('-123.123') }, - { a: BigNumber('-123.123') }, - ], - mars: { - stars: [{ bars: 0 }, { czars: 1n }], - }, - }, - bats: { - mars: { - stars: [{ bars: 2 }, { czars: '3' }], - }, - }, - }; - - const TestObjAct2 = () => ({ - stats: 123, - }); - - const TestObjExp2 = { - stats: '123', - }; - - it('should work with a GetCollNumRepFn', () => { - const serdes = new CollectionSerDes({ - enableBigNumbers: (path: string[]) => { - if (path[0] !== 'stats') { - return 'number_or_string'; - } - - if (path.length === 1) { - return 'string'; - } - - if (path[1] === 'cars' && path[3] === 'a') { - if (path[2] === '0') { - return 'number'; - } - return 'bignumber'; - } - - if (path[2] === 'stars' && path[4] === 'bars') { - return 'number'; - } - - return 'bigint'; - }, - }); - assert.deepStrictEqual(serdes.deserialize(TestObjAct1(), null!), TestObjExp1); - assert.deepStrictEqual(serdes.deserialize(TestObjAct2(), null!), TestObjExp2); - assert.deepStrictEqual(serdes.deserialize({}, null!), {}); - }); - - it('should work with a CollNumRepCfg', () => { - const serdes = new CollectionSerDes({ - enableBigNumbers: { - '*': 'number_or_string', - 'stats': 'string', - 'stats.*': 'bigint', - 'stats.cars.0.a': 'number', - 'stats.cars.*.a': 'bignumber', - 'stats.*.stars.*.bars': 'number', - }, - }); - assert.deepStrictEqual(serdes.deserialize(TestObjAct1(), null!), TestObjExp1); - assert.deepStrictEqual(serdes.deserialize(TestObjAct2(), null!), TestObjExp2); - assert.deepStrictEqual(serdes.deserialize({}, null!), {}); - }); }); From f2b109ba3a8db098e0df4887bde9ce9533c5c64b Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 8 Jan 2025 09:56:23 +0530 Subject: [PATCH 29/44] split dates/times/durations into separate files --- scripts/build.sh | 6 +- src/documents/datatypes/date.ts | 149 +++++++++++ src/documents/datatypes/dates.ts | 350 ------------------------- src/documents/datatypes/duration.ts | 73 ++++++ src/documents/datatypes/index.ts | 4 +- src/documents/datatypes/time.ts | 162 ++++++++++++ src/documents/tables/ser-des/codecs.ts | 4 +- 7 files changed, 395 insertions(+), 353 deletions(-) create mode 100644 src/documents/datatypes/date.ts delete mode 100644 src/documents/datatypes/dates.ts create mode 100644 src/documents/datatypes/duration.ts create mode 100644 src/documents/datatypes/time.ts diff --git a/scripts/build.sh b/scripts/build.sh index 233839fa..e2c7d36d 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -25,6 +25,10 @@ else npx tsc --project tsconfig.production.json fi +if [ ! -d ./dist ]; then + exit 10 +fi + # Replaces alias paths with relative paths (e.g. `@/src/version` -> `../../src/version`) npx tsc-alias -p tsconfig.production.json @@ -51,7 +55,7 @@ if [ "$light" != true ]; then node scripts/utils/del-empty-dist-files.js # Removes all .d.ts files except the main rollup .d.ts - cd dist || return 1 + cd dist || exit 20 find . -type f -name '*.d.ts' ! -name 'astra-db-ts.d.ts' -exec rm {} + cd .. diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts new file mode 100644 index 00000000..9865a9a2 --- /dev/null +++ b/src/documents/datatypes/date.ts @@ -0,0 +1,149 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { isNullish } from '@/src/lib/utils'; +import { $CustomInspect } from '@/src/lib/constants'; +import { DataAPITime, TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; +import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; + +/** + * A shorthand function for `new DataAPIDate(date?)` + * + * If no date is provided, it defaults to the current date. + * + * @public + */ +export const date = (date?: string | Date | DataAPIDateComponents) => new DataAPIDate(date); + +/** + * Represents the time components that make up a `DataAPIDate` + * + * @public + */ +export interface DataAPIDateComponents { + /** + * The year of the date + */ + year: number, + /** + * The month of the date (should be between 1 and 12) + */ + month: number, + /** + * The day of the month + */ + date: number, +} + +/** + * Represents a `date` column for Data API tables. + * + * You may use the {@link date} function as a shorthand for creating a new `DataAPIDate`. + * + * See the official DataStax documentation for more information. + * + * @public + */ +export class DataAPIDate implements TableCodec { + readonly #date: string; + + /** + * Implementation of `$SerializeForTable` for {@link TableCodec} + */ + public [$SerializeForTable](ctx: TableSerCtx) { + return ctx.done(this.toString()); + }; + + /** + * Implementation of `$DeserializeForTable` for {@link TableCodec} + */ + public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { + return ctx.done(new DataAPIDate(value)); + } + + /** + * Creates a new `DataAPIVector` instance from various formats. + * + * @param input - The input to create the `DataAPIDate` from + */ + public constructor(input?: string | Date | DataAPIDateComponents) { + if (typeof input === 'string') { + this.#date = input; + } else if (input instanceof Date || isNullish(input)) { + input ||= new Date(); + this.#date = `${input.getFullYear().toString().padStart(4, '0')}-${(input.getMonth() + 1).toString().padStart(2, '0')}-${input.getDate().toString().padStart(2, '0')}`; + } else { + if (input.month < 1 || input.month > 12) { + throw new RangeError('Month must be between 1 and 12 (DataAPIDate month is NOT zero-indexed)'); + } + this.#date = `${input.year.toString().padStart(4, '0') ?? '0000'}-${input.month.toString().padStart(2, '0') ?? '00'}-${input.date.toString().padStart(2, '0') ?? '00'}`; + } + + Object.defineProperty(this, $CustomInspect, { + value: () => `DataAPIDate("${this.#date}")`, + }); + } + + /** + * Returns the {@link DataAPIDateComponents} that make up this `DataAPIDate` + * + * @returns The components of the date + */ + public components(): DataAPIDateComponents { + const signum = this.#date.startsWith('-') ? -1 : 1; + const date = this.#date.split('-'); + + if (signum === -1) { + date.shift(); + } + + return { year: +date[0], month: +date[1], date: +date[2] }; + } + + /** + * Converts this `DataAPIDate` to a `Date` object + * + * If no `base` date/time is provided to use the time from, the time component is set to be the current time. + * + * @param base - The base date/time to use for the time component + * + * @returns The `Date` object representing this `DataAPIDate` + */ + public toDate(base?: Date | DataAPITime): Date { + if (!base) { + base = new Date(); + } + + const date = this.components(); + + if (base instanceof Date) { + const ret = new Date(base); + ret.setFullYear(date.year, date.month - 1, date.date); + return ret; + } + + const time = base.components(); + + return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + } + + /** + * Returns the string representation of this `DataAPIDate` + * + * @returns The string representation of this `DataAPIDate` + */ + public toString(): string { + return this.#date; + } +} diff --git a/src/documents/datatypes/dates.ts b/src/documents/datatypes/dates.ts deleted file mode 100644 index fa532270..00000000 --- a/src/documents/datatypes/dates.ts +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright DataStax, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { isNullish } from '@/src/lib/utils'; -import { $CustomInspect } from '@/src/lib/constants'; -import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; -import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; - -/** - * A shorthand function for `new DataAPIDate(date?)` - * - * If no date is provided, it defaults to the current date. - * - * @public - */ -export const date = (date?: string | Date | DataAPIDateComponents) => new DataAPIDate(date); - -/** - * Represents the time components that make up a `DataAPIDate` - * - * @public - */ -export interface DataAPIDateComponents { - /** - * The year of the date - */ - year: number, - /** - * The month of the date (should be between 1 and 12) - */ - month: number, - /** - * The day of the month - */ - date: number, -} - -/** - * Represents a `date` column for Data API tables. - * - * You may use the {@link date} function as a shorthand for creating a new `DataAPIDate`. - * - * See the official DataStax documentation for more information. - * - * @public - */ -export class DataAPIDate implements TableCodec { - readonly #date: string; - - /** - * Implementation of `$SerializeForTable` for {@link TableCodec} - */ - public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.toString()); - }; - - /** - * Implementation of `$DeserializeForTable` for {@link TableCodec} - */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new DataAPIDate(value)); - } - - /** - * Creates a new `DataAPIVector` instance from various formats. - * - * @param input - The input to create the `DataAPIDate` from - */ - public constructor(input?: string | Date | DataAPIDateComponents) { - if (typeof input === 'string') { - this.#date = input; - } else if (input instanceof Date || isNullish(input)) { - input ||= new Date(); - this.#date = `${input.getFullYear().toString().padStart(4, '0')}-${(input.getMonth() + 1).toString().padStart(2, '0')}-${input.getDate().toString().padStart(2, '0')}`; - } else { - if (input.month < 1 || input.month > 12) { - throw new RangeError('Month must be between 1 and 12 (DataAPIDate month is NOT zero-indexed)'); - } - this.#date = `${input.year.toString().padStart(4, '0') ?? '0000'}-${input.month.toString().padStart(2, '0') ?? '00'}-${input.date.toString().padStart(2, '0') ?? '00'}`; - } - - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPIDate("${this.#date}")`, - }); - } - - /** - * Returns the {@link DataAPIDateComponents} that make up this `DataAPIDate` - * - * @returns The components of the date - */ - public components(): DataAPIDateComponents { - const signum = this.#date.startsWith('-') ? -1 : 1; - const date = this.#date.split('-'); - - if (signum === -1) { - date.shift(); - } - - return { year: +date[0], month: +date[1], date: +date[2] }; - } - - /** - * Converts this `DataAPIDate` to a `Date` object - * - * If no `base` date/time is provided to use the time from, the time component is set to be the current time. - * - * @param base - The base date/time to use for the time component - * - * @returns The `Date` object representing this `DataAPIDate` - */ - public toDate(base?: Date | DataAPITime): Date { - if (!base) { - base = new Date(); - } - - const date = this.components(); - - if (base instanceof Date) { - const ret = new Date(base); - ret.setFullYear(date.year, date.month - 1, date.date); - return ret; - } - - const time = base.components(); - - return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); - } - - /** - * Returns the string representation of this `DataAPIDate` - * - * @returns The string representation of this `DataAPIDate` - */ - public toString(): string { - return this.#date; - } -} - -/** - * A shorthand function for `new DataAPIDuration(duration)` - * - * @public - */ -export const duration = (duration: string) => new DataAPIDuration(duration); - -/** - * Represents a `duration` column for Data API tables. - * - * You may use the {@link duration} function as a shorthand for creating a new `DataAPIDuration`. - * - * See the official DataStax documentation for more information. - * - * @public - */ -export class DataAPIDuration implements TableCodec { - readonly #duration: string; - - /** - * Implementation of `$SerializeForTable` for {@link TableCodec} - */ - public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.toString()); - }; - - /** - * Implementation of `$DeserializeForTable` for {@link TableCodec} - */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new DataAPIDuration(value)); - } - - /** - * Creates a new `DataAPIDuration` instance from a duration string. - * - * @param input - The duration string to create the `DataAPIDuration` from - */ - constructor(input: string) { - this.#duration = input; - - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPIDuration("${this.#duration}")`, - }); - } - - /** - * Returns the string representation of this `DataAPIDuration` - * - * @returns The string representation of this `DataAPIDuration` - */ - public toString() { - return this.#duration; - } -} - -/** - * A shorthand function for `new DataAPITime(time?)` - * - * If no time is provided, it defaults to the current time. - * - * @public - */ -export const time = (time?: string | Date | PartialDataAPITimeComponents) => new DataAPITime(time); - -/** - * Represents the time components that make up a `DataAPITime` - * - * @public - */ -export interface DataAPITimeComponents { - /** - * The hour of the time - */ - hours: number, - /** - * The minute of the time - */ - minutes: number, - /** - * The second of the time - */ - seconds: number, - /** - * The nanosecond of the time - */ - nanoseconds: number -} - -/** - * Represents the time components that make up a `DataAPITime`, with the nanoseconds being optional - * - * @public - */ -export type PartialDataAPITimeComponents = (Omit & { nanoseconds?: number }); - -/** - * Represents a `time` column for Data API tables. - * - * You may use the {@link time} function as a shorthand for creating a new `DataAPITime`. - * - * See the official DataStax documentation for more information. - * - * @public - */ -export class DataAPITime implements TableCodec { - readonly #time: string; - - /** - * Implementation of `$SerializeForTable` for {@link TableCodec} - */ - public [$SerializeForTable](ctx: TableSerCtx) { - return ctx.done(this.toString()); - }; - - /** - * Implementation of `$DeserializeForTable` for {@link TableCodec} - */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new DataAPITime(value)); - } - - /** - * Creates a new `DataAPITime` instance from various formats. - * - * @param input - The input to create the `DataAPITime` from - */ - public constructor(input?: string | Date | PartialDataAPITimeComponents) { - input ||= new Date(); - - if (typeof input === 'string') { - this.#time = input; - } else if (input instanceof Date) { - this.#time = DataAPITime.#initTime(input.getHours(), input.getMinutes(), input.getSeconds(), input.getMilliseconds()); - } else { - this.#time = DataAPITime.#initTime(input.hours, input.minutes, input.seconds, input.nanoseconds ? input.nanoseconds.toString().padStart(9, '0') : ''); - } - - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPITime("${this.#time}")`, - }); - } - - static #initTime(hours: number, minutes: number, seconds: number, fractional?: unknown): string { - return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}${fractional ? `.${fractional}` : ''}`; - } - - /** - * Returns the {@link DataAPITimeComponents} that make up this `DataAPITime` - * - * @returns The components of the time - */ - public components(): DataAPITimeComponents { - const [timePart, fractionPart] = this.#time.split('.'); - const [hours, mins, secs] = timePart.split(':'); - - return { - hours: +hours, - minutes: +mins, - seconds: +secs, - nanoseconds: +fractionPart.padEnd(9, '0'), - }; - } - - /** - * Converts this `DataAPITime` to a `Date` object - * - * If no `base` date/time is provided to use the date from, the date component is set to be the current date. - * - * @param base - The base date/time to use for the date component - * - * @returns The `Date` object representing this `DataAPITime` - */ - public toDate(base?: Date | DataAPIDate): Date { - if (!base) { - base = new Date(); - } - - const time = this.components(); - - if (base instanceof Date) { - const ret = new Date(base); - ret.setHours(time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); - return ret; - } - - const date = base.components(); - - return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); - } - - /** - * Returns the string representation of this `DataAPITime` - * - * @returns The string representation of this `DataAPITime` - */ - public toString() { - return this.#time; - } -} diff --git a/src/documents/datatypes/duration.ts b/src/documents/datatypes/duration.ts new file mode 100644 index 00000000..1c8ca78a --- /dev/null +++ b/src/documents/datatypes/duration.ts @@ -0,0 +1,73 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { $CustomInspect } from '@/src/lib/constants'; +import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; +import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; + +/** + * A shorthand function for `new DataAPIDuration(duration)` + * + * @public + */ +export const duration = (duration: string) => new DataAPIDuration(duration); + +/** + * Represents a `duration` column for Data API tables. + * + * You may use the {@link duration} function as a shorthand for creating a new `DataAPIDuration`. + * + * See the official DataStax documentation for more information. + * + * @public + */ +export class DataAPIDuration implements TableCodec { + readonly #duration: string; + + /** + * Implementation of `$SerializeForTable` for {@link TableCodec} + */ + public [$SerializeForTable](ctx: TableSerCtx) { + return ctx.done(this.toString()); + }; + + /** + * Implementation of `$DeserializeForTable` for {@link TableCodec} + */ + public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { + return ctx.done(new DataAPIDuration(value)); + } + + /** + * Creates a new `DataAPIDuration` instance from a duration string. + * + * @param input - The duration string to create the `DataAPIDuration` from + */ + constructor(input: string) { + this.#duration = input; + + Object.defineProperty(this, $CustomInspect, { + value: () => `DataAPIDuration("${this.#duration}")`, + }); + } + + /** + * Returns the string representation of this `DataAPIDuration` + * + * @returns The string representation of this `DataAPIDuration` + */ + public toString() { + return this.#duration; + } +} diff --git a/src/documents/datatypes/index.ts b/src/documents/datatypes/index.ts index 2098d19f..20d8e307 100644 --- a/src/documents/datatypes/index.ts +++ b/src/documents/datatypes/index.ts @@ -13,7 +13,9 @@ // limitations under the License. export * from './blob'; -export * from './dates'; +export * from './date'; +export * from './duration'; export * from './object-id'; +export * from './time'; export * from './uuid'; export * from './vector'; diff --git a/src/documents/datatypes/time.ts b/src/documents/datatypes/time.ts new file mode 100644 index 00000000..499256e9 --- /dev/null +++ b/src/documents/datatypes/time.ts @@ -0,0 +1,162 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { $CustomInspect } from '@/src/lib/constants'; +import { DataAPIDate, TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; +import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; + +/** + * A shorthand function for `new DataAPITime(time?)` + * + * If no time is provided, it defaults to the current time. + * + * @public + */ +export const time = (time?: string | Date | PartialDataAPITimeComponents) => new DataAPITime(time); + +/** + * Represents the time components that make up a `DataAPITime` + * + * @public + */ +export interface DataAPITimeComponents { + /** + * The hour of the time + */ + hours: number, + /** + * The minute of the time + */ + minutes: number, + /** + * The second of the time + */ + seconds: number, + /** + * The nanosecond of the time + */ + nanoseconds: number +} + +/** + * Represents the time components that make up a `DataAPITime`, with the nanoseconds being optional + * + * @public + */ +export type PartialDataAPITimeComponents = (Omit & { nanoseconds?: number }); + +/** + * Represents a `time` column for Data API tables. + * + * You may use the {@link time} function as a shorthand for creating a new `DataAPITime`. + * + * See the official DataStax documentation for more information. + * + * @public + */ +export class DataAPITime implements TableCodec { + readonly #time: string; + + /** + * Implementation of `$SerializeForTable` for {@link TableCodec} + */ + public [$SerializeForTable](ctx: TableSerCtx) { + return ctx.done(this.toString()); + }; + + /** + * Implementation of `$DeserializeForTable` for {@link TableCodec} + */ + public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { + return ctx.done(new DataAPITime(value)); + } + + /** + * Creates a new `DataAPITime` instance from various formats. + * + * @param input - The input to create the `DataAPITime` from + */ + public constructor(input?: string | Date | PartialDataAPITimeComponents) { + input ||= new Date(); + + if (typeof input === 'string') { + this.#time = input; + } else if (input instanceof Date) { + this.#time = DataAPITime.#initTime(input.getHours(), input.getMinutes(), input.getSeconds(), input.getMilliseconds()); + } else { + this.#time = DataAPITime.#initTime(input.hours, input.minutes, input.seconds, input.nanoseconds ? input.nanoseconds.toString().padStart(9, '0') : ''); + } + + Object.defineProperty(this, $CustomInspect, { + value: () => `DataAPITime("${this.#time}")`, + }); + } + + static #initTime(hours: number, minutes: number, seconds: number, fractional?: unknown): string { + return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}${fractional ? `.${fractional}` : ''}`; + } + + /** + * Returns the {@link DataAPITimeComponents} that make up this `DataAPITime` + * + * @returns The components of the time + */ + public components(): DataAPITimeComponents { + const [timePart, fractionPart] = this.#time.split('.'); + const [hours, mins, secs] = timePart.split(':'); + + return { + hours: +hours, + minutes: +mins, + seconds: +secs, + nanoseconds: +fractionPart.padEnd(9, '0'), + }; + } + + /** + * Converts this `DataAPITime` to a `Date` object + * + * If no `base` date/time is provided to use the date from, the date component is set to be the current date. + * + * @param base - The base date/time to use for the date component + * + * @returns The `Date` object representing this `DataAPITime` + */ + public toDate(base?: Date | DataAPIDate): Date { + if (!base) { + base = new Date(); + } + + const time = this.components(); + + if (base instanceof Date) { + const ret = new Date(base); + ret.setHours(time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + return ret; + } + + const date = base.components(); + + return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + } + + /** + * Returns the string representation of this `DataAPITime` + * + * @returns The string representation of this `DataAPITime` + */ + public toString() { + return this.#time; + } +} diff --git a/src/documents/tables/ser-des/codecs.ts b/src/documents/tables/ser-des/codecs.ts index cb8ad57d..2c31ee29 100644 --- a/src/documents/tables/ser-des/codecs.ts +++ b/src/documents/tables/ser-des/codecs.ts @@ -14,7 +14,9 @@ // Important to import from specific paths here to avoid circular dependencies import { DataAPIBlob } from '@/src/documents/datatypes/blob'; -import { DataAPIDate, DataAPIDuration, DataAPITime } from '@/src/documents/datatypes/dates'; +import { DataAPIDate } from '@/src/documents/datatypes/date'; +import { DataAPIDuration } from '@/src/documents/datatypes/duration'; +import { DataAPITime } from '@/src/documents/datatypes/time'; import { UUID } from '@/src/documents/datatypes/uuid'; import { DataAPIVector } from '@/src/documents/datatypes/vector'; import { SomeDoc, TableDesCtx, TableSerCtx } from '@/src/documents'; From ce54e0142f6067ff364eb2a82e96a956dccca28b Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 8 Jan 2025 19:05:36 +0530 Subject: [PATCH 30/44] wip overhaul of dates --- eslint.config.mjs | 3 + src/documents/datatypes/date.ts | 518 +++++++++++++++--- src/documents/datatypes/time.ts | 4 +- src/documents/utils.ts | 24 + .../documents/tables/datatypes.test.ts | 15 +- .../documents/tables/find-one.test.ts | 4 +- .../documents/tables/insert-one.test.ts | 2 +- .../documents/tables/update-one.test.ts | 4 +- tests/unit/documents/datatypes/date.test.ts | 107 ++++ 9 files changed, 601 insertions(+), 80 deletions(-) create mode 100644 tests/unit/documents/datatypes/date.test.ts diff --git a/eslint.config.mjs b/eslint.config.mjs index def371c4..655c5efa 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -21,6 +21,9 @@ export default ts.config( // Only way I can do indentation in ts-doc 'no-irregular-whitespace': 'off', + // sorry. + '@typescript-eslint/no-unused-expressions': 'off', + // Makes underscore variables not throw a fit '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts index 9865a9a2..ef90fadb 100644 --- a/src/documents/datatypes/date.ts +++ b/src/documents/datatypes/date.ts @@ -12,51 +12,85 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { isNullish } from '@/src/lib/utils'; import { $CustomInspect } from '@/src/lib/constants'; import { DataAPITime, TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; +import { mkInvArgsErr } from '@/src/documents/utils'; + +const MillisecondsPerDay = 1000 * 60 * 60 * 24; /** - * A shorthand function for `new DataAPIDate(date?)` + * ##### Overview * - * If no date is provided, it defaults to the current date. + * Represents a `date` column for Data API tables, * - * @public - */ -export const date = (date?: string | Date | DataAPIDateComponents) => new DataAPIDate(date); - -/** - * Represents the time components that make up a `DataAPIDate` + * ##### Format * - * @public - */ -export interface DataAPIDateComponents { - /** - * The year of the date - */ - year: number, - /** - * The month of the date (should be between 1 and 12) - */ - month: number, - /** - * The day of the month - */ - date: number, -} - -/** - * Represents a `date` column for Data API tables. + * `date`s consist simply of a year, a month, and a date. + * + * - The year may be either positive or negative, and must be at least four digits long (with leading padding zeros if necessary). + * + * - The month must be between 1-12 (not zero-indexed like JS dates), and must be two digits long. + * + * - The day must be a valid day for the given month, and starts at 1. It must also be two digits long. Feb 29th is allowed on leap years + * + * Together, the hypothetical pseudo-regex would be as such: `[+-]?YYY(Y+)-MM-DD`. + * + * **Note that the `DataAPIDate`'s parser is lenient on if the leading `+` is included or not.** For example, `+2000-01-01` is accepted, even if it is not technically valid; same with `10000-01-01`. A plus will be prepended in {@link DataAPIDate.toString} as necessary. + * + * ##### Creation + * + * There are a number of different ways to initialize a `DataAPIDate`: * - * You may use the {@link date} function as a shorthand for creating a new `DataAPIDate`. + * @example + * ```ts + * // Convert a native JS `Date` to a `DataAPIDate` (extracting only the date) + * new DataAPIDate(new Date('2004-09-14T12:00:00.000Z')) // '2004-09-14' + * + * // Parse a date given the above date-string format + * new DataAPIDate('+2004-09-14) + * + * // Create a `DataAPIDate` from a year, a month, and a date + * new DataAPIDate(2004, 9, 14) + * + * // Get the current date (using the local timezone) + * DataAPIDate.now() + * + * // Get the current date (using UTC) + * DataAPIDate.utcnow() + * + * // Create a `DataAPIDate` from a year and a valid day of the year + * DataAPIDate.ofYearDay(2004, 258) // '2004-09-14' + * + * // Create a `DataAPIDate` given the number of days since the epoch (may be negative) + * DataAPIDate.ofEpochDay(12675) // '2004-09-14' + * ``` + * + * ##### The `date` shorthand + * + * You may use the {@link date} shorthand function-object anywhere when creating new `DataAPIDate`s. + * + * ```ts + * // equiv. to `new DataAPIDate('2004-09-14')` + * date('2004-09-14') + * + * // equiv. to `new DataAPIDate(2004, 9, 14)` + * date(2004, 9, 14) + * + * // equiv. to `DataAPIDate.now()` + * date.now() + * ``` * * See the official DataStax documentation for more information. * + * @see date + * * @public */ export class DataAPIDate implements TableCodec { - readonly #date: string; + readonly year: number; + readonly month: number; + readonly date: number; /** * Implementation of `$SerializeForTable` for {@link TableCodec} @@ -68,54 +102,217 @@ export class DataAPIDate implements TableCodec { /** * Implementation of `$DeserializeForTable` for {@link TableCodec} */ - public static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx) { - return ctx.done(new DataAPIDate(value)); + public static [$DeserializeForTable](_: unknown, value: string, ctx: TableDesCtx) { + return ctx.done(new DataAPIDate(value, false)); } /** - * Creates a new `DataAPIVector` instance from various formats. + * ##### Overview * - * @param input - The input to create the `DataAPIDate` from + * Returns the current date in the local timezone. + * + * Equivalent to `new DataAPIDate(new Date())`. + * + * @example + * ```ts + * const now = date.now(); + * // or + * const now = DataAPIDate.now() + * ``` + * + * @returns The current date in the local timezone */ - public constructor(input?: string | Date | DataAPIDateComponents) { - if (typeof input === 'string') { - this.#date = input; - } else if (input instanceof Date || isNullish(input)) { - input ||= new Date(); - this.#date = `${input.getFullYear().toString().padStart(4, '0')}-${(input.getMonth() + 1).toString().padStart(2, '0')}-${input.getDate().toString().padStart(2, '0')}`; - } else { - if (input.month < 1 || input.month > 12) { - throw new RangeError('Month must be between 1 and 12 (DataAPIDate month is NOT zero-indexed)'); - } - this.#date = `${input.year.toString().padStart(4, '0') ?? '0000'}-${input.month.toString().padStart(2, '0') ?? '00'}-${input.date.toString().padStart(2, '0') ?? '00'}`; - } + public static now(): DataAPIDate { + return new DataAPIDate(new Date()); + } - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPIDate("${this.#date}")`, - }); + /** + * ##### Overview + * + * Returns the current date in UTC. + * + * Uses `Date.now()` under the hood. + * + * @example + * ```ts + * const now = date.utcnow(); + * // or + * const now = DataAPIDate.utcnow() + * ``` + * + * @returns The current date in UTC + */ + public static utcnow(): DataAPIDate { + return new DataAPIDate(ofEpochDay(Math.floor(Date.now() / MillisecondsPerDay))); } /** - * Returns the {@link DataAPIDateComponents} that make up this `DataAPIDate` + * ##### Overview + * + * Creates a `DataAPIDate` from the number of days since the epoch (may be negative). + * + * The number may be negative, but must be an integer within the range `-100_000_000..=100_000_000`. + * + * @example + * ```ts + * DataAPIDate.ofEpochDay(0) // 1970-01-01 * - * @returns The components of the date + * date.ofEpochDay(12675) // 2004-09-14 + * + * date.ofEpochDay(0-1) // 1969-12-31 + * ``` + * + * @param epochDays - The number of days since the epoch (may be negative) + * + * @returns The date representing the given number of days since the epoch */ - public components(): DataAPIDateComponents { - const signum = this.#date.startsWith('-') ? -1 : 1; - const date = this.#date.split('-'); + public static ofEpochDay(epochDays: number): DataAPIDate { + return new DataAPIDate(ofEpochDay(epochDays)); + } - if (signum === -1) { - date.shift(); + /** + * ##### Overview + * + * Creates a `DataAPIDate` from a year and a valid day of the year. + * + * The year may be negative. + * + * The day-of-year must be valid for the year, otherwise an exception will be thrown. + * + * @example + * ```ts + * DataAPIDate.ofYearDay(2004, 258) // 2004-09-14 + * + * date.ofYearDay(2004, 1) // 2004-01-01 + * + * date.ofYearDay(2004, 366) // 2004-12-31 (ok b/c 2004 is a leap year) + * ``` + * + * @param year - The year to use + * @param dayOfYear - The day of the year to use (1-indexed) + * + * @returns The date representing the given year and day of the year + */ + public static ofYearDay(year: number, dayOfYear: number): DataAPIDate { + return new DataAPIDate(ofYearDay(year, dayOfYear)); + } + + /** + * ##### Overview + * + * Converts a native JS `Date` to a `DataAPIDate` (extracting only the date). + * + * @example + * ```ts + * new DataAPIDate(new Date('2004-09-14T12:00:00.000Z')) // '2004-09-14' + * + * date(new Date('-200004-09-14')) // '200004-09-14' + * ``` + * + * @param date - The date to convert + */ + public constructor(date: Date); + + /** + * ##### Overview + * + * Parses a `DataAPIDate` from a string in the format `[+-]?YYY(Y+)-MM-DD`. + * + * See {@link DataAPIDate} for more info about the exact format. + * + * @example + * ```ts + * new DataAPIDate('2004-09-14') // 2004-09-14 + * + * date('-2004-09-14') // -2004-09-14 + * + * date('+123456-09-14') // 123456-09-14 + * ``` + * + * @param date - The date to parse + * @param strict - Whether to throw an error if the date is invalid + */ + public constructor(date: string, strict?: boolean); + + /** + * ##### Overview + * + * Creates a `DataAPIDate` from a year, a month, and a date. + * + * The year may be negative. The month and day are both 1-indexed. + * + * The date must be valid for the given month, otherwise an exception will be thrown. + * + * @example + * ```ts + * new DataAPIDate(2004, 9, 14) // 2004-09-14 + * + * date(-200004, 9, 14) // -200004-09-14 + * ``` + * + * @param year - The year to use + * @param month - The month to use (1-indexed) + * @param date - The date to use (1-indexed) + */ + public constructor(year: number, month: number, date: number); + + public constructor(i1: string | Date | number, i2?: number | boolean, i3?: number) { + switch (arguments.length) { + case 1: { + if (typeof i1 === 'string') { + [this.year, this.month, this.date] = parseDateStr(i1, true); + } + else if (i1 instanceof Date) { + if (isNaN(i1.getTime())) { + throw new Error(`Invalid date '${i1.toString()}'; must be a valid (non-NaN) date`); + } + this.year = i1.getFullYear(); + this.month = i1.getMonth() + 1; + this.date = i1.getDate(); + } + else { + throw mkInvArgsErr('new DataAPIDate', [['date', 'string | Date']], i1); + } + break; + } + case 2: { + [this.year, this.month, this.date] = parseDateStr(i1, i2 !== false); + break; + } + case 3: { + if (typeof i1 !== 'number' || typeof i2 !== 'number' || typeof i3 !== 'number') { + throw mkInvArgsErr('new DataAPIDate', [['year', 'number'], ['month', 'number'], ['date', 'number']], i1, i2, i3); + } + validateDate(1, i1, i2, i3); + this.year = i1; + this.month = i2; + this.date = i3; + break; + } + default: { + throw RangeError(`Invalid number of arguments; expected 1 or 3, got ${arguments.length}`); + } } - return { year: +date[0], month: +date[1], date: +date[2] }; + Object.defineProperty(this, $CustomInspect, { + value: () => `DataAPIDate("${this.toString()}")`, + }); } /** + * ##### Overview + * * Converts this `DataAPIDate` to a `Date` object * * If no `base` date/time is provided to use the time from, the time component is set to be the current time. * + * @example + * ```ts + * date('1970-01-01').toDate(new Date('12:00:00')) // 1969-12-31T18:30:00.000Z + * + * date('1970-01-01').toDate() // 1970-01-01TZ + * ``` + * * @param base - The base date/time to use for the time component * * @returns The `Date` object representing this `DataAPIDate` @@ -125,25 +322,222 @@ export class DataAPIDate implements TableCodec { base = new Date(); } - const date = this.components(); - if (base instanceof Date) { const ret = new Date(base); - ret.setFullYear(date.year, date.month - 1, date.date); + ret.setFullYear(this.year, this.month - 1, this.date); return ret; } const time = base.components(); - return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + return new Date(this.year, this.month - 1, this.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); } /** + * ##### Overview + * * Returns the string representation of this `DataAPIDate` * + * Note that a `+` is prepended to the year if it is greater than or equal to 10000. + * + * @example + * ```ts + * date('2004-09-14').toString() // '2004-09-14' + * + * date(-2004, 9, 14).toString() // '-2004-09-14' + * + * date('123456-01-01').toString() // '+123456-01-01' + * ``` + * * @returns The string representation of this `DataAPIDate` */ public toString(): string { - return this.#date; + return `${this.year >= 10000 ? '+' : ''}${this.year.toString().padStart(4, '0')}-${this.month.toString().padStart(2, '0')}-${this.date.toString().padStart(2, '0')}`; + } + + /** + * ##### Overview + * + * Compares this `DataAPIDate` to another `DataAPIDate` + * + * @example + * ```ts + * date('2004-09-14').compare(date(2004, 9, 14)) // 0 + * + * date('2004-09-14').compare(date(2004, 9, 15)) // -1 + * + * date('2004-09-15').compare(date(2004, 9, 14)) // 1 + * ``` + * + * @param other - The other `DataAPIDate` to compare to + * + * @returns `0` if the dates are equal, `-1` if this date is before the other, and `1` if this date is after the other + */ + public compare(other: DataAPIDate): -1 | 0 | 1 { + if (this.year !== other.year) { + return this.year < other.year ? -1 : 1; + } + + if (this.month !== other.month) { + return this.month < other.month ? -1 : 1; + } + + if (this.date !== other.date) { + return this.date < other.date ? -1 : 1; + } + + return 0; + } + + /** + * ##### Overview + * + * Checks if this `DataAPIDate` is equal to another `DataAPIDate` + * + * @example + * ```ts + * date('2004-09-14').equals(date(2004, 9, 14)) // true + * + * date('2004-09-14').equals(date(2004, 9, 15)) // false + * + * date('2004-09-15').equals(date(2004, 9, 14)) // false + * ``` + * + * @param other - The other `DataAPIDate` to compare to + * + * @returns `true` if the dates are equal, and `false` otherwise + */ + public equals(other: DataAPIDate): boolean { + return (other as unknown instanceof DataAPIDate) && this.compare(other) === 0; } } + +/** + * ##### Overview + * + * A shorthand function-object for {@link DataAPIDate}. May be used anywhere when creating new `DataAPIDate`s. + * + * See {@link DataAPIDate} and its methods for information about input parameters, formats, functions, etc. + * + * @example + * ```ts + * // equiv. to `new DataAPIDate('2004-09-14')` + * date('2004-09-14') + * + * // equiv. to `new DataAPIDate(2004, 9, 14)` + * date(2004, 9, 14) + * + * // equiv. to `DataAPIDate.now()` + * date.now() + * ``` + * + * @public + */ +export const date = Object.assign( + (...params: [string] | [Date] | [number, number, number]) => new DataAPIDate(...<[any]>params), + { + now: DataAPIDate.now, + utcnow: DataAPIDate.utcnow, + ofEpochDay: DataAPIDate.ofEpochDay, + ofYearDay: DataAPIDate.ofYearDay, + }, +); + +const parseDateStr = (str: unknown, strict: boolean): [number, number, number] => { + if (typeof str !== 'string') { + throw mkInvArgsErr('DataAPIDate.parse', [['date', 'string']], str); + } + + return (strict) + ? parseDateStrict(str) + : parseDateQuick(str); +}; + +const parseDateQuick = (str: string): [number, number, number] => { + const sign = (str[0] === '-') ? -1 : 1; + const startIndex = (str[0] === '+' || str[0] === '-') ? 1 : 0; + + const yearStr = str.substring(startIndex, str.indexOf('-', startIndex + 1)); + const yearStrEnd = startIndex + yearStr.length; + + const year = parseInt(yearStr, 10); + const month = parseInt(str.slice(yearStrEnd + 1, yearStr.length + 4), 10); + const date = parseInt(str.slice(yearStrEnd + 4, yearStr.length + 7), 10); + + return [sign * year, month, date]; +}; + +const DateRegex = /^([-+])?(\d{4,})-(\d{2})-(\d{2})$/; + +const parseDateStrict = (str: string): [number, number, number] => { + const match = str.match(DateRegex); + + if (!match) { + throw new Error(`Invalid date string '${str}'; must be in the format [+-]?YYY(Y+)-MM-DD, with zero-padded numbers as necessary`); + } + + const sign = (match[1] === '-') ? -1 : 1; + const ymd = [sign * parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)] as [number, number, number]; + + validateDate(sign, ymd[0], ymd[1], ymd[2]); + return ymd; +}; + +const isLeapYear = (year: number): boolean => { + return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); +}; + +const DaysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; +const DaysInMonthLeap = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + +const validateDate = (sign: number, year: number, month: number, date: number): void => { + if (!Number.isInteger(year) || !Number.isInteger(month) || !Number.isInteger(date)) { + throw new TypeError(`Invalid year: ${year}, month: ${month}, and/or date: ${date}; must be integers`); + } + + if (month < 1 || 12 < month) { + throw new RangeError(`Invalid month: ${month}; month must be between 1 and 12 (DataAPIDate's month is NOT zero-indexed)`); + } + + const dim = isLeapYear(year) ? DaysInMonthLeap : DaysInMonth; + + if (date <= 0 || dim[month - 1] < date) { + throw new RangeError(`Invalid date: ${date}; must be between 1 and ${dim[month - 1]} for month ${month} in year ${year}`); + } + + if (sign < 0 && year === 0) { + throw new RangeError(`Invalid year: ${year}; year may not be 0 for negative dates`); + } +}; + +const ofYearDay = (year: unknown, dayOfYear: unknown): Date => { + if (typeof year !== 'number' || typeof dayOfYear !== 'number') { + throw mkInvArgsErr('DataAPIDate.ofYearDay', [['year', 'number'], ['dayOfYear', 'number']], year, dayOfYear); + } + + if (dayOfYear < 1 || 365 + (isLeapYear(year) ? 1 : 0) < dayOfYear) { + throw new RangeError(`Invalid dayOfYear: ${dayOfYear}; must be between 1 and ${365 + (isLeapYear(year) ? 1 : 0)} for year ${year}`); + } + + const date = new Date(); + date.setUTCFullYear(year, 0, dayOfYear); + return date; +}; + +const ofEpochDay = (epochDays: unknown): Date => { + if (typeof epochDays !== 'number') { + throw mkInvArgsErr('DataAPIDate.ofEpochDay', [['epochDays', 'number']], epochDays); + } + + if (!Number.isInteger(epochDays)) { + throw new TypeError(`Invalid epochDays: ${epochDays}; must be an integer`); + } + + const date = new Date(epochDays * MillisecondsPerDay); + + if (isNaN(date.getTime())) { + throw new RangeError(`Invalid epochDays: ${epochDays}; must be within range -100_000_000..=100_000_000`); + } + + return date; +}; diff --git a/src/documents/datatypes/time.ts b/src/documents/datatypes/time.ts index 499256e9..f636dd3c 100644 --- a/src/documents/datatypes/time.ts +++ b/src/documents/datatypes/time.ts @@ -146,9 +146,7 @@ export class DataAPITime implements TableCodec { return ret; } - const date = base.components(); - - return new Date(date.year, date.month - 1, date.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + return new Date(base.year, base.month - 1, base.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); } /** diff --git a/src/documents/utils.ts b/src/documents/utils.ts index 25487026..2451faee 100644 --- a/src/documents/utils.ts +++ b/src/documents/utils.ts @@ -82,3 +82,27 @@ export const normalizedSort = (sort: SomeDoc): Sort => { return ret; }; + +/** + * @internal + */ +export const betterTypeOf = (value: unknown): string => { + if (value === null) { + return 'null'; + } + + if (typeof value === 'object') { + return value.constructor?.name ?? 'Object'; + } + + return typeof value; +}; + +/** + * @internal + */ +export const mkInvArgsErr = (exp: string, params: [string, string][], ...got: unknown[]): TypeError => { + const names = params.map(([name]) => name).join(', '); + const types = params.map(([, type]) => type).join(', '); + return new TypeError(`Invalid argument(s) for \`${exp}(${names})\`; expected (${types}), got (${got.map(betterTypeOf).join(' | ')})`); +}; diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts index aa934a10..e3ef2a64 100644 --- a/tests/integration/documents/tables/datatypes.test.ts +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -18,9 +18,11 @@ import { DataAPIBlob, DataAPIResponseError, DataAPIVector, - date, duration, + date, + duration, SomeRow, - Table, time, + Table, + time, uuid, vector, } from '@/src/documents'; @@ -297,14 +299,7 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { it('should handle different date insertion cases', async (key) => { const colAsserter = mkColumnAsserter(key, 'date'); - await colAsserter.notOk('2000-00-01'); - await colAsserter.notOk('2000-01-00'); - await colAsserter.notOk('2000/01/01'); - await colAsserter.notOk('2000-01-32'); - await colAsserter.notOk('2000-02-30'); await colAsserter.notOk('+2000-01-01'); - await colAsserter.notOk('-0000-01-01'); - await colAsserter.notOk(3123123); await colAsserter.ok('0000-01-01', date); await colAsserter.ok('1970-01-01', date); @@ -313,7 +308,7 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.ok(date('+500000-12-31')); await colAsserter.ok(date('-500000-12-31')); await colAsserter.ok(date(new Date('1970-01-01T23:59:59.999Z')), _ => date('1970-01-01')); - await colAsserter.ok(date({ year: 1970, month: 1, date: 1 })); + await colAsserter.ok(date(1970, 1, 1)); }); it('should handle different timestamp insertion cases', async (key) => { diff --git a/tests/integration/documents/tables/find-one.test.ts b/tests/integration/documents/tables/find-one.test.ts index f0d2c2e8..6f595e3b 100644 --- a/tests/integration/documents/tables/find-one.test.ts +++ b/tests/integration/documents/tables/find-one.test.ts @@ -78,7 +78,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), bigint: 1231233n, - date: new DataAPIDate(), + date: DataAPIDate.now(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, duration: new DataAPIDuration('P1D'), @@ -120,7 +120,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr assert.strictEqual(found.blob.asBase64(), doc.blob.asBase64()); assert.ok(found.date); - assert.deepStrictEqual(found.date.components(), doc.date.components()); + assert.deepStrictEqual(found.date, doc.date); assert.ok(found.decimal); assert.strictEqual(found.decimal.toString(), doc.decimal.toString()); diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index ac7c9cf3..b4f5ac65 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -46,7 +46,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), bigint: 1231233n, - date: new DataAPIDate(), + date: DataAPIDate.now(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, duration: new DataAPIDuration('1d'), diff --git a/tests/integration/documents/tables/update-one.test.ts b/tests/integration/documents/tables/update-one.test.ts index 7e3c7483..d84c339f 100644 --- a/tests/integration/documents/tables/update-one.test.ts +++ b/tests/integration/documents/tables/update-one.test.ts @@ -62,7 +62,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } ascii: 'highway_star', blob: new DataAPIBlob(Buffer.from('smoke_on_the_water')), bigint: 1231233n, - date: new DataAPIDate(), + date: DataAPIDate.now(), decimal: BigNumber('12.34567890123456789012345678901234567890'), double: 123.456, duration: new DataAPIDuration('P1D'), @@ -107,7 +107,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } assert.strictEqual(found.blob.asBase64(), doc.blob.asBase64()); assert.ok(found.date); - assert.deepStrictEqual(found.date.components(), doc.date.components()); + assert.deepStrictEqual(found.date, doc.date); assert.ok(found.decimal); assert.strictEqual(found.decimal.toString(), doc.decimal.toString()); diff --git a/tests/unit/documents/datatypes/date.test.ts b/tests/unit/documents/datatypes/date.test.ts new file mode 100644 index 00000000..464bdf40 --- /dev/null +++ b/tests/unit/documents/datatypes/date.test.ts @@ -0,0 +1,107 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import assert from 'assert'; +import { DataAPIDate, date } from '@/src/documents'; +import { describe, it } from '@/tests/testlib'; + +describe('unit.documents.datatypes.date', () => { + describe('construction', () => { + const assertDateOk = (params: any, ymd: unknown = params) => { + const date = params instanceof DataAPIDate ? params : new DataAPIDate(...params as [any]); + assert.deepStrictEqual([date.year, date.month, date.date], ymd); + }; + + const assertDateNotOk = (params: any, err: any = Error) => { + assert.throws(() => new DataAPIDate(...params as [any]), err); + }; + + const notOkStrings = { + '-0000-01-01': [RangeError, [ -0, 1, 1]], + '2000-00-01': [RangeError, [2000, 0, 1]], + '2000-01-00': [RangeError, [2000, 1, 0]], + '2000/01/01': [Error, [ NaN, 0, NaN]], + '2000-01-32': [RangeError, [2000, 1, 32]], + '2000-02-30': [RangeError, [2000, 2, 30]], + '1999-02-29': [RangeError, [1999, 2, 29]], + '-200-01-01': [Error, [ -200, 1, 1]], + '200-01-01': [Error, [ 200, 1, 1]], + '2000-1-01': [Error, [ 2000, 1, 1]], + '2000-01-1': [Error, [ 2000, 1, 1]], + '2000-01': [Error, [ 2000, 1, NaN]], + }; + + const okStrings = { + '-20000-01-01': [-20000, 1, 1], + '+20000-01-01': [ 20000, 1, 1], + '20000-01-01': [ 20000, 1, 1], + '+2000-01-01': [ 2000, 1, 1], + '2000-01-01': [ 2000, 1, 1], + '2000-01-31': [ 2000, 1, 31], + '2000-02-29': [ 2000, 2, 29], + '2004-02-29': [ 2004, 2, 29], + }; + + it('should parse a strict DataAPIDate from a valid string', () => { + for (const [str, [err]] of Object.entries(notOkStrings)) { + assertDateNotOk([str], err); + } + for (const [str, ymd] of Object.entries(okStrings)) { + assertDateOk([str], ymd); + } + }); + + it('should parse an unvalidated DataAPIDate from a string', () => { + for (const [str, [, ymd]] of Object.entries(notOkStrings)) { + assertDateOk([str, false], ymd); + } + for (const [str, ymd] of Object.entries(okStrings)) { + assertDateOk([str, false], ymd); + } + }); + + it('should convert a Date to a DataAPIDate', () => { + assertDateOk([new Date(200000, 11, 1)], [200000, 12, 1]); + assertDateOk([new Date(-20000, 0, 9)], [-20000, 1, 9]); + assertDateOk([new Date('2000-01-31T12:59:59Z')], [ 2000, 1, 31]); + assertDateOk([new Date('2000-01-01T00:00:00Z')], [ 2000, 1, 1]); + }); + + it('should create a DataAPIDate from year+month+day', () => { + assertDateNotOk([2000, .1, 1], TypeError); + assertDateNotOk([2000, 1, .1], TypeError); + assertDateNotOk([.002, 1, 1], TypeError); + assertDateNotOk([2000, 0, 1], RangeError); + assertDateNotOk([2000, 13, 1], RangeError); + assertDateNotOk([-200, 2, 29], RangeError); + assertDateNotOk([1900, 2, 29], RangeError); + assertDateNotOk([2001, 2, 29], RangeError); + assertDateNotOk([2001, 2, 0], RangeError); + + assertDateOk([-204, 2, 29]); + assertDateOk([2000, 1, 1]); + assertDateOk([2000, 2, 29]); + assertDateOk([2004, 2, 29]); + }); + + it('should get the current date', () => { + assertDateOk(date.now(), [new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate()]); + }); + + it('should get the current utc date', () => { + assertDateOk(date.utcnow(), [new Date().getUTCFullYear(), new Date().getUTCMonth() + 1, new Date().getUTCDate()]); + }); + }); +}); From 9d087f2bec1185f0f5aa670c2fd0c0634ed6c491 Mon Sep 17 00:00:00 2001 From: toptobes Date: Wed, 8 Jan 2025 23:10:37 +0530 Subject: [PATCH 31/44] wip overhaul of times --- src/documents/datatypes/date.ts | 12 +- src/documents/datatypes/time.ts | 249 ++++++++++++------ .../documents/tables/datatypes.test.ts | 10 +- .../documents/tables/find-one.test.ts | 4 +- .../documents/tables/insert-one.test.ts | 2 +- .../documents/tables/update-one.test.ts | 4 +- tests/unit/documents/datatypes/time.test.ts | 100 +++++++ 7 files changed, 283 insertions(+), 98 deletions(-) create mode 100644 tests/unit/documents/datatypes/time.test.ts diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts index ef90fadb..00cf0379 100644 --- a/src/documents/datatypes/date.ts +++ b/src/documents/datatypes/date.ts @@ -48,7 +48,7 @@ const MillisecondsPerDay = 1000 * 60 * 60 * 24; * new DataAPIDate(new Date('2004-09-14T12:00:00.000Z')) // '2004-09-14' * * // Parse a date given the above date-string format - * new DataAPIDate('+2004-09-14) + * new DataAPIDate('+2004-09-14') * * // Create a `DataAPIDate` from a year, a month, and a date * new DataAPIDate(2004, 9, 14) @@ -151,7 +151,7 @@ export class DataAPIDate implements TableCodec { * * Creates a `DataAPIDate` from the number of days since the epoch (may be negative). * - * The number may be negative, but must be an integer within the range `-100_000_000..=100_000_000`. + * The number may be negative, but must be an integer within the range `[-100_000_000, 100_000_000]`. * * @example * ```ts @@ -290,7 +290,7 @@ export class DataAPIDate implements TableCodec { break; } default: { - throw RangeError(`Invalid number of arguments; expected 1 or 3, got ${arguments.length}`); + throw RangeError(`Invalid number of arguments; expected 1..=3, got ${arguments.length}`); } } @@ -328,9 +328,7 @@ export class DataAPIDate implements TableCodec { return ret; } - const time = base.components(); - - return new Date(this.year, this.month - 1, this.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + return new Date(this.year, this.month - 1, this.date, base.hours, base.minutes, base.seconds, base.nanoseconds / 1_000_000); } /** @@ -536,7 +534,7 @@ const ofEpochDay = (epochDays: unknown): Date => { const date = new Date(epochDays * MillisecondsPerDay); if (isNaN(date.getTime())) { - throw new RangeError(`Invalid epochDays: ${epochDays}; must be within range -100_000_000..=100_000_000`); + throw new RangeError(`Invalid epochDays: ${epochDays}; must be within range [-100_000_000, 100_000_000]`); } return date; diff --git a/src/documents/datatypes/time.ts b/src/documents/datatypes/time.ts index f636dd3c..853a43eb 100644 --- a/src/documents/datatypes/time.ts +++ b/src/documents/datatypes/time.ts @@ -15,46 +15,7 @@ import { $CustomInspect } from '@/src/lib/constants'; import { DataAPIDate, TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; - -/** - * A shorthand function for `new DataAPITime(time?)` - * - * If no time is provided, it defaults to the current time. - * - * @public - */ -export const time = (time?: string | Date | PartialDataAPITimeComponents) => new DataAPITime(time); - -/** - * Represents the time components that make up a `DataAPITime` - * - * @public - */ -export interface DataAPITimeComponents { - /** - * The hour of the time - */ - hours: number, - /** - * The minute of the time - */ - minutes: number, - /** - * The second of the time - */ - seconds: number, - /** - * The nanosecond of the time - */ - nanoseconds: number -} - -/** - * Represents the time components that make up a `DataAPITime`, with the nanoseconds being optional - * - * @public - */ -export type PartialDataAPITimeComponents = (Omit & { nanoseconds?: number }); +import { mkInvArgsErr } from '@/src/documents/utils'; /** * Represents a `time` column for Data API tables. @@ -66,7 +27,10 @@ export type PartialDataAPITimeComponents = (Omit { - readonly #time: string; + readonly hours: number; + readonly minutes: number; + readonly seconds: number; + readonly nanoseconds: number; /** * Implementation of `$SerializeForTable` for {@link TableCodec} @@ -82,46 +46,69 @@ export class DataAPITime implements TableCodec { return ctx.done(new DataAPITime(value)); } - /** - * Creates a new `DataAPITime` instance from various formats. - * - * @param input - The input to create the `DataAPITime` from - */ - public constructor(input?: string | Date | PartialDataAPITimeComponents) { - input ||= new Date(); - - if (typeof input === 'string') { - this.#time = input; - } else if (input instanceof Date) { - this.#time = DataAPITime.#initTime(input.getHours(), input.getMinutes(), input.getSeconds(), input.getMilliseconds()); - } else { - this.#time = DataAPITime.#initTime(input.hours, input.minutes, input.seconds, input.nanoseconds ? input.nanoseconds.toString().padStart(9, '0') : ''); - } + public static now(): DataAPITime { + return new DataAPITime(new Date()); + } - Object.defineProperty(this, $CustomInspect, { - value: () => `DataAPITime("${this.#time}")`, - }); + public static utcnow(): DataAPITime { + return new DataAPITime(...ofNanoOfDay((Date.now() % 86_400_000) * 1_000_000)); } - static #initTime(hours: number, minutes: number, seconds: number, fractional?: unknown): string { - return `${hours < 10 ? '0' : ''}${hours}:${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}${fractional ? `.${fractional}` : ''}`; + public static ofNanoOfDay(nanoOfDay: number): DataAPITime { + return new DataAPITime(...ofNanoOfDay(nanoOfDay)); } - /** - * Returns the {@link DataAPITimeComponents} that make up this `DataAPITime` - * - * @returns The components of the time - */ - public components(): DataAPITimeComponents { - const [timePart, fractionPart] = this.#time.split('.'); - const [hours, mins, secs] = timePart.split(':'); - - return { - hours: +hours, - minutes: +mins, - seconds: +secs, - nanoseconds: +fractionPart.padEnd(9, '0'), - }; + public static ofSecondOfDay(secondOfDay: number): DataAPITime { + return new DataAPITime(...ofSecondOfDay(secondOfDay)); + } + + public constructor(time: Date); + + public constructor(time: string, strict?: boolean); + + public constructor(hours: number, minutes: number, seconds?: number, nanoseconds?: number); + + public constructor(i1: Date | string | number, i2?: boolean | number, i3?: number, i4?: number) { + switch (arguments.length) { + case 1: { + if (typeof i1 === 'string') { + [this.hours, this.minutes, this.seconds, this.nanoseconds] = parseTimeStr(i1, true); + } + else if (i1 instanceof Date) { + this.hours = i1.getHours(); + this.minutes = i1.getMinutes(); + this.seconds = i1.getSeconds(); + this.nanoseconds = i1.getMilliseconds() * 1_000_000; + } + else { + throw mkInvArgsErr('new DataAPITime', [['time', 'string | Date']], i1); + } + break; + } + case 2: case 3: case 4: { + if (typeof i1 === 'string') { + [this.hours, this.minutes, this.seconds, this.nanoseconds] = parseTimeStr(i1, i2 !== false); + } + else if (typeof i1 === 'number' && typeof i2 === 'number' && (!i3 || typeof i3 as unknown === 'number') && (!i4 || typeof i4 as unknown === 'number')) { + this.hours = i1; + this.minutes = i2; + this.seconds = i3 || 0; + this.nanoseconds = i4 || 0; + validateTime(this.hours, this.minutes, this.seconds, this.nanoseconds); + } + else { + throw mkInvArgsErr('new DataAPIDate', [['hour', 'number'], ['minute', 'number'], ['second', 'number?'], ['nanosecond', 'number?']], i1, i2, i3, i4); + } + break; + } + default: { + throw RangeError(`Invalid number of arguments; expected 1..=4, got ${arguments.length}`); + } + } + + Object.defineProperty(this, $CustomInspect, { + value: () => `DataAPITime("${this.toString()}")`, + }); } /** @@ -138,15 +125,13 @@ export class DataAPITime implements TableCodec { base = new Date(); } - const time = this.components(); - if (base instanceof Date) { const ret = new Date(base); - ret.setHours(time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + ret.setHours(this.hours, this.minutes, this.seconds, this.nanoseconds / 1_000_000); return ret; } - return new Date(base.year, base.month - 1, base.date, time.hours, time.minutes, time.seconds, time.nanoseconds / 1_000_000); + return new Date(base.year, base.month - 1, base.date, this.hours, this.minutes, this.seconds, this.nanoseconds / 1_000_000); } /** @@ -155,6 +140,108 @@ export class DataAPITime implements TableCodec { * @returns The string representation of this `DataAPITime` */ public toString() { - return this.#time; + return `${this.hours < 10 ? '0' : ''}${this.hours}:${this.minutes < 10 ? '0' : ''}${this.minutes}:${this.seconds < 10 ? '0' : ''}${this.seconds}.${this.nanoseconds.toString().padStart(9, '0')}`; } } + +/** + * A shorthand function for `new DataAPITime(time?)` + * + * If no time is provided, it defaults to the current time. + * + * @public + */ +export const time = Object.assign( + (...params: [string] | [Date] | [number, number, number?, number?]) => new DataAPITime(...<[any]>params), + { + now: DataAPITime.now, + utcnow: DataAPITime.utcnow, + ofNanoOfDay: DataAPITime.ofNanoOfDay, + ofSecondOfDay: DataAPITime.ofSecondOfDay, + }, +); + +const parseTimeStr = (str: unknown, strict: boolean): [number, number, number, number] => { + if (typeof str !== 'string') { + throw mkInvArgsErr('DataAPIDate.parse', [['date', 'string']], str); + } + + return (strict) + ? parseTimeStrict(str) + : parseTimeQuick(str); +}; + +const parseTimeQuick = (str: string): [number, number, number, number] => { + const hour = parseInt(str.slice(0, 2), 10); + const minute = parseInt(str.slice(3, 5), 10); + + const second = (str.length > 5) + ? parseInt(str.slice(6, 8), 10) + : 0; + + const nanoseconds = (str.length > 9) + ? parseInt(str.slice(9), 10) * Math.pow(10, 9 - (str.length - 9)) + : 0; + + return [hour, minute, second, nanoseconds]; +}; + +const TimeRegex = /^(\d\d):(\d\d)(?::(\d\d(?:\.(\d{0,9}))?))?$/; + +const parseTimeStrict = (str: string): [number, number, number, number] => { + const match = str.match(TimeRegex); + + if (!match) { + throw Error(`Invalid time: '${str}'; must match HH:MM[:SS[.NNNNNNNNN]]`); + } + + const time: [number, number, number, number] = [ + parseInt(match[1], 10), + parseInt(match[2], 10), + parseInt(match[3] || '0', 10), + parseInt(match[4] || '0', 10) * Math.pow(10, 9 - (match[4]?.length ?? 0)), + ]; + + validateTime(...time); + return time; +}; + +const validateTime = (hours: number, minutes: number, seconds: number, nanoseconds: number) => { + if (!Number.isInteger(hours) || !Number.isInteger(minutes) || !Number.isInteger(seconds) || !Number.isInteger(nanoseconds)) { + throw new TypeError(`Invalid hour: ${hours}, minute: ${minutes}, second: ${seconds}, and/or nanosecond: ${nanoseconds}; must be integers`); + } + + if (hours < 0 || hours > 23) { + throw RangeError(`Invalid hour: ${hours}; must be in range [0, 23]`); + } + + if (minutes < 0 || minutes > 59) { + throw RangeError(`Invalid minute: ${minutes}; must be in range [0, 59]`); + } + + if (seconds < 0 || seconds > 59) { + throw RangeError(`Invalid second: ${seconds}; must be in range [0, 59]`); + } + + if (nanoseconds < 0 || nanoseconds > 999_999_999) { + throw RangeError(`Invalid nanosecond: ${nanoseconds}; must be in range [0, 999,999,999]`); + } +}; + +const ofSecondOfDay = (s: number): [number, number, number, number] => { + const hours = ~~(s / 3_600); + s -= hours * 3_600; + const minutes = ~~(s / 60); + s -= minutes * 60; + return [hours, minutes, s, 0]; +}; + +const ofNanoOfDay = (ns: number): [number, number, number, number] => { + const hours = ~~(ns / 3_600_000_000_000); + ns -= hours * 3_600_000_000_000; + const minutes = ~~(ns / 60_000_000_000); + ns -= minutes * 60_000_000_000; + const seconds = ~~(ns / 1_000_000_000); + ns -= seconds * 1_000_000_000; + return [hours, minutes, seconds, ns]; +}; diff --git a/tests/integration/documents/tables/datatypes.test.ts b/tests/integration/documents/tables/datatypes.test.ts index e3ef2a64..cd128ad1 100644 --- a/tests/integration/documents/tables/datatypes.test.ts +++ b/tests/integration/documents/tables/datatypes.test.ts @@ -287,13 +287,13 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.notOk('12:34:56Z+05:30'); await colAsserter.notOk(3123123); - await colAsserter.ok('23:59:00.', _ => time('23:00:00')); // S + await colAsserter.ok('23:59:00.', _ => time('23:59:00')); // S await colAsserter.ok('23:59', _ => time('23:59:00')); // S await colAsserter.ok('00:00:00.000000000', time); await colAsserter.ok(time('23:59:59.999999999')); - await colAsserter.ok(time(new Date('1970-01-01T23:59:59.999Z')), _ => time('23:59:59.999')); - await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59 })); - await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59, nanoseconds: 120012 })); + await colAsserter.ok(time(new Date('1970-01-01T23:59:59.999')), _ => time('23:59:59.999')); + // await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59 })); + // await colAsserter.ok(time({ hours: 23, minutes: 59, seconds: 59, nanoseconds: 120012 })); }); it('should handle different date insertion cases', async (key) => { @@ -307,7 +307,7 @@ parallel('integration.documents.tables.datatypes', ({ table, table_ }) => { await colAsserter.ok(date('9999-12-31')); await colAsserter.ok(date('+500000-12-31')); await colAsserter.ok(date('-500000-12-31')); - await colAsserter.ok(date(new Date('1970-01-01T23:59:59.999Z')), _ => date('1970-01-01')); + await colAsserter.ok(date(new Date('1970-01-01T23:59:59.999')), _ => date('1970-01-01')); await colAsserter.ok(date(1970, 1, 1)); }); diff --git a/tests/integration/documents/tables/find-one.test.ts b/tests/integration/documents/tables/find-one.test.ts index 6f595e3b..09a208a6 100644 --- a/tests/integration/documents/tables/find-one.test.ts +++ b/tests/integration/documents/tables/find-one.test.ts @@ -87,7 +87,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr list: [uuid, uuid], set: new Set([uuid, uuid, uuid]), smallint: 123, - time: new DataAPITime(), + time: DataAPITime.now(), timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), @@ -139,7 +139,7 @@ parallel('integration.documents.tables.find-one', { truncate: 'colls:before', dr assert.deepStrictEqual([...found.set].map(u => u.toString()), [...doc.set].map(u => u.toString())); assert.ok(found.time); - assert.deepStrictEqual(found.time.components(), doc.time.components()); + assert.deepStrictEqual(found.time, doc.time); assert.ok(found.timestamp); assert.deepStrictEqual(found.timestamp, doc.timestamp); diff --git a/tests/integration/documents/tables/insert-one.test.ts b/tests/integration/documents/tables/insert-one.test.ts index b4f5ac65..5d6f112a 100644 --- a/tests/integration/documents/tables/insert-one.test.ts +++ b/tests/integration/documents/tables/insert-one.test.ts @@ -55,7 +55,7 @@ parallel('integration.documents.tables.insert-one', { truncate: 'tables:before' list: [UUID.v4(), UUID.v7()], set: new Set([UUID.v4(), UUID.v7(), UUID.v7()]), smallint: 123, - time: new DataAPITime(), + time: DataAPITime.now(), timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), diff --git a/tests/integration/documents/tables/update-one.test.ts b/tests/integration/documents/tables/update-one.test.ts index d84c339f..f80dd1ec 100644 --- a/tests/integration/documents/tables/update-one.test.ts +++ b/tests/integration/documents/tables/update-one.test.ts @@ -71,7 +71,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } list: [uuid, uuid], set: new Set([uuid, uuid, uuid]), smallint: 123, - time: new DataAPITime(), + time: DataAPITime.now(), timestamp: new Date(), tinyint: 123, uuid: UUID.v4(), @@ -126,7 +126,7 @@ parallel('integration.documents.tables.update-one', { truncate: 'colls:before' } assert.deepStrictEqual([...found.set].map(u => u.toString()), [...doc.set].map(u => u.toString())); assert.ok(found.time); - assert.deepStrictEqual(found.time.components(), doc.time.components()); + assert.deepStrictEqual(found.time, doc.time); assert.ok(found.timestamp); assert.deepStrictEqual(found.timestamp, doc.timestamp); diff --git a/tests/unit/documents/datatypes/time.test.ts b/tests/unit/documents/datatypes/time.test.ts new file mode 100644 index 00000000..40065630 --- /dev/null +++ b/tests/unit/documents/datatypes/time.test.ts @@ -0,0 +1,100 @@ +// Copyright DataStax, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// noinspection DuplicatedCode + +import assert from 'assert'; +import { DataAPITime, time } from '@/src/documents'; +import { describe, it } from '@/tests/testlib'; + +describe('unit.documents.datatypes.time', () => { + describe('construction', () => { + const assertTimeOk = (params: any, exp: unknown = params) => { + const time = params instanceof DataAPITime ? params : new DataAPITime(...params as [any]); + assert.deepStrictEqual([time.hours, time.minutes, time.seconds, time.nanoseconds], exp); + }; + + const assertTimeNotOk = (params: any, err: any = Error) => { + assert.throws(() => new DataAPITime(...params as [any]), err); + }; + + const notOkStrings = { + '11': [Error, [ 11, NaN, 0, 0]], + '2000-11-11': [Error, [ 20, 0, 1, 100000000]], + '24:00:00': [RangeError, [ 24, 0, 0, 0]], + '00:60:00': [RangeError, [ 0, 60, 0, 0]], + '00:00:60': [RangeError, [ 0, 0, 60, 0]], + '00:00:00.0000000000': [Error, [ 0, 0, 0, 0]], + '1:22:33': [Error, [ 1, 2, 3, 0]], + '-1:22:33': [Error, [ -1, 22, 33, 0]], + '12:34:56Z+05:30': [Error, [ 12, 34, 56, 5000]], + 'asdfdsaf': [Error, [NaN, NaN, NaN, 0]], + }; + + const okStrings = { + '11:11:11.111111111': [11, 11, 11, 111111111], + '11:22:22.100000000': [11, 22, 22, 100000000], + '11:33:33.1': [11, 33, 33, 100000000], + '00:00:00.': [ 0, 0, 0, 0], + '01:00:01': [ 1, 0, 1, 0], + '01:01:00': [ 1, 1, 0, 0], + '10:10': [10, 10, 0, 0], + }; + + it('should parse a strict DataAPIDate from a valid string', () => { + for (const [str, [err]] of Object.entries(notOkStrings)) { + assertTimeNotOk([str], err); + } + for (const [str, ymd] of Object.entries(okStrings)) { + assertTimeOk([str], ymd); + } + }); + + it('should parse an unvalidated DataAPIDate from a string', () => { + for (const [str, [, time]] of Object.entries(notOkStrings)) { + assertTimeOk([str, false], time); + } + for (const [str, time] of Object.entries(okStrings)) { + assertTimeOk([str, false], time); + } + }); + + it('should convert a Date to a DataAPITime', () => { + assertTimeOk([new Date('2000-01-31T12:59:59')], [12, 59, 59, 0]); + assertTimeOk([new Date('2000-01-01T00:00:00')], [ 0, 0, 0, 0]); + }); + + it('should create a DataAPITime from hour+month+secs?+ns?', () => { + assertTimeNotOk([.002, 1, 1, 1], TypeError); + assertTimeNotOk([2000, .1, 1, 1], TypeError); + assertTimeNotOk([2000, 1, .1, 1], TypeError); + assertTimeNotOk([2000, 1, 1, .1], TypeError); + assertTimeNotOk([-200, 1, 1, 1], RangeError); + assertTimeNotOk([2000, -1, 1, 1], RangeError); + assertTimeNotOk([2000, 1, -1, 1], RangeError); + assertTimeNotOk([2000, 1, 1, -1], RangeError); + + assertTimeOk([12, 34 ], [12, 34, 0, 0]); + assertTimeOk([12, 34, 56 ], [12, 34, 56, 0]); + assertTimeOk([12, 34, 56, 78], [12, 34, 56, 78]); + }); + + it('should get the current date', () => { + assert.ok(time.now()); + }); + + it('should get the current utc date', () => { + assert.ok(time.utcnow()); + }); + }); +}); From ecc215f6b23f240ce3eaef8a8dae87a814760968 Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 9 Jan 2025 09:51:30 +0530 Subject: [PATCH 32/44] documentation for time stuff --- src/documents/datatypes/date.ts | 35 ++-- src/documents/datatypes/time.ts | 301 +++++++++++++++++++++++++++++++- 2 files changed, 314 insertions(+), 22 deletions(-) diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts index 00cf0379..f78837bc 100644 --- a/src/documents/datatypes/date.ts +++ b/src/documents/datatypes/date.ts @@ -44,8 +44,8 @@ const MillisecondsPerDay = 1000 * 60 * 60 * 24; * * @example * ```ts - * // Convert a native JS `Date` to a `DataAPIDate` (extracting only the date) - * new DataAPIDate(new Date('2004-09-14T12:00:00.000Z')) // '2004-09-14' + * // Convert a native JS `Date` to a `DataAPIDate` (extracting only the local date) + * new DataAPIDate(new Date('2004-09-14T12:00:00.000')) // '2004-09-14' * * // Parse a date given the above date-string format * new DataAPIDate('+2004-09-14') @@ -70,6 +70,7 @@ const MillisecondsPerDay = 1000 * 60 * 60 * 24; * * You may use the {@link date} shorthand function-object anywhere when creating new `DataAPIDate`s. * + * @example * ```ts * // equiv. to `new DataAPIDate('2004-09-14')` * date('2004-09-14') @@ -149,17 +150,17 @@ export class DataAPIDate implements TableCodec { /** * ##### Overview * - * Creates a `DataAPIDate` from the number of days since the epoch (may be negative). + * Creates a `DataAPIDate` from the number of days since the epoch. * * The number may be negative, but must be an integer within the range `[-100_000_000, 100_000_000]`. * * @example * ```ts - * DataAPIDate.ofEpochDay(0) // 1970-01-01 + * DataAPIDate.ofEpochDay(0) // '1970-01-01' * - * date.ofEpochDay(12675) // 2004-09-14 + * date.ofEpochDay(12675) // '2004-09-14' * - * date.ofEpochDay(0-1) // 1969-12-31 + * date.ofEpochDay(-1) // '1969-12-31' * ``` * * @param epochDays - The number of days since the epoch (may be negative) @@ -200,16 +201,16 @@ export class DataAPIDate implements TableCodec { /** * ##### Overview * - * Converts a native JS `Date` to a `DataAPIDate` (extracting only the date). + * Converts a native JS `Date` to a `DataAPIDate` (extracting only the local date). * * @example * ```ts - * new DataAPIDate(new Date('2004-09-14T12:00:00.000Z')) // '2004-09-14' + * new DataAPIDate(new Date('2004-09-14T12:00:00.000')) // '2004-09-14' * * date(new Date('-200004-09-14')) // '200004-09-14' * ``` * - * @param date - The date to convert + * @param date - The `Date` object to convert */ public constructor(date: Date); @@ -222,15 +223,15 @@ export class DataAPIDate implements TableCodec { * * @example * ```ts - * new DataAPIDate('2004-09-14') // 2004-09-14 + * new DataAPIDate('2004-09-14') // '2004-09-14' * - * date('-2004-09-14') // -2004-09-14 + * date('-2004-09-14') // '-2004-09-14' * - * date('+123456-09-14') // 123456-09-14 + * date('+123456-09-14') // '123456-09-14' * ``` * * @param date - The date to parse - * @param strict - Whether to throw an error if the date is invalid + * @param strict - Uses a faster parser which doesn't perform any validity or format checks if `false` */ public constructor(date: string, strict?: boolean); @@ -245,9 +246,9 @@ export class DataAPIDate implements TableCodec { * * @example * ```ts - * new DataAPIDate(2004, 9, 14) // 2004-09-14 + * new DataAPIDate(2004, 9, 14) // '2004-09-14' * - * date(-200004, 9, 14) // -200004-09-14 + * date(-200004, 9, 14) // '-200004-09-14' * ``` * * @param year - The year to use @@ -308,9 +309,9 @@ export class DataAPIDate implements TableCodec { * * @example * ```ts - * date('1970-01-01').toDate(new Date('12:00:00')) // 1969-12-31T18:30:00.000Z + * date('1970-01-01').toDate(new Date('12:00:00')) // '1970-01-01T12:00:00' * - * date('1970-01-01').toDate() // 1970-01-01TZ + * date('1970-01-01').toDate() // '1970-01-01T' * ``` * * @param base - The base date/time to use for the time component diff --git a/src/documents/datatypes/time.ts b/src/documents/datatypes/time.ts index 853a43eb..a40e7c06 100644 --- a/src/documents/datatypes/time.ts +++ b/src/documents/datatypes/time.ts @@ -18,12 +18,73 @@ import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables import { mkInvArgsErr } from '@/src/documents/utils'; /** + * ##### Overview + * * Represents a `time` column for Data API tables. * - * You may use the {@link time} function as a shorthand for creating a new `DataAPITime`. + * ##### Format + * + * `time`s consist of an hour, a minute, and optional second and nanosecond components. + * + * - The hour is a number from 0 to 23, and must be positive. + * - The minute is a number from 0 to 59. + * - The second is a number from 0 to 59, and will default to 0 if not provided. + * - The nanosecond is a fractional component of the second, and will default to 0 if not provided. + * - It is a number up to 9 digits long. + * - If any digits are omitted, they are assumed to be 0. + * - e.g. `12:34:56.789` is equivalent to `12:34:56.789000000`. + * - Seconds must be provided if nanoseconds are provided. + * + * Together, the format would be as such: `HH:MM[:SS[.NNNNNNNNN]]`. + * + * ##### Creation + * + * There are a number of different ways to initialize a `DataAPITime`: + * + * @example + * ```ts + * // Convert a native JS `Date` to a `DataAPITime` (extracting only the local time) + * new DataAPITIme(new Date('2004-09-14T12:00:00.000')) // '12:00:00.000000000' + * + * // Parse a time given the above time-string format + * new DataAPITime('12:34:56.78') // '12:34:56.780000000' + * + * // Create a `DataAPIDate` from an hour, a minute, and optional second and nanosecond components + * new DataAPITime(12, 34, 56, 78) // '12:34:56.000000078' + * + * // Get the current time (using the local timezone) + * DataAPITime.now() + * + * // Get the current time (using UTC) + * DataAPITime.utcnow() + * + * // Create a `DataAPITime` from the number of nanoseconds since the start of the day + * DataAPITime.ofNanoOfDay(12_345_678_912_345) // '03:25:45.678912345' + * + * // Create a `DataAPITime` from the number of seconds since the start of the day + * DataAPITime.ofSecondOfDay(12_345) // '03:25:45.000000000' + * ``` + * + * ##### The `time` shorthand + * + * You may use the {@link time} shorthand function-object anywhere when creating new `DataAPITime`s. + * + * @example + * ```ts + * // equiv. to `new DataAPITime('12:34:56')` + * time('12:34:56') + * + * // equiv. to `new DataAPITime(12, 34, 56)` + * time(12, 34, 56) + * + * // equiv. to `DataAPITime.now()` + * time.now() + * ``` * * See the official DataStax documentation for more information. * + * @see time + * * @public */ export class DataAPITime implements TableCodec { @@ -46,26 +107,148 @@ export class DataAPITime implements TableCodec { return ctx.done(new DataAPITime(value)); } + /** + * ##### Overview + * + * Returns the current time in the local timezone. + * + * Equivalent to `new DataAPITime(new Date())`. + * + * @example + * ```ts + * const now = time.now(); + * // or + * const now = DataAPITime.now() + * ``` + * + * @returns The current time in the local timezone + */ public static now(): DataAPITime { return new DataAPITime(new Date()); } + /** + * ##### Overview + * + * Returns the current time in UTC. + * + * Uses `Date.now()` under the hood. + * + * @example + * ```ts + * const now = time.utcnow(); + * // or + * const now = DataAPITime.utcnow() + * ``` + * + * @returns The current time in UTC + */ public static utcnow(): DataAPITime { return new DataAPITime(...ofNanoOfDay((Date.now() % 86_400_000) * 1_000_000)); } + /** + * ##### Overview + * + * Creates a `DataAPITime` from the number of nanoseconds since the start of the day . + * + * The number must be a positive integer in the range [0, 86,399,999,999,999]. + * + * @example + * ```ts + * DataAPITime.ofNanoOfDay(0) // '00:00:00.000000000' + * + * date.ofNanoOfDay(12_345_678_912_345) // '03:25:45.678912345' + * ``` + * + * @param nanoOfDay - The number of nanoseconds since the start of the day + * + * @returns The `DataAPITime` representing the given number of nanoseconds + */ public static ofNanoOfDay(nanoOfDay: number): DataAPITime { return new DataAPITime(...ofNanoOfDay(nanoOfDay)); } + /** + * ##### Overview + * + * Creates a `DataAPITime` from the number of seconds since the start of the day. + * + * The number must be a positive integer in the range [0, 86,399]. + * + * @example + * ```ts + * DataAPITime.ofSecondOfDay(0) // '00:00:00.000000000' + * + * DataAPITime.ofSecondOfDay(12_345) // '03:25:45.000000000' + * ``` + * + * @param secondOfDay - The number of seconds since the start of the day + * + * @returns The `DataAPITime` representing the given number of seconds + */ public static ofSecondOfDay(secondOfDay: number): DataAPITime { return new DataAPITime(...ofSecondOfDay(secondOfDay)); } + /** + * ##### Overview + * + * Converts a native JS `Date` to a `DataAPITime` (extracting only the local time). + * + * @example + * ```ts + * new DataAPITime(new Date('2004-09-14T12:00:00.000')) // '12:00:00.000000000' + * + * time(new Date('12:34:56.78')) // '12:34:56.780000000' + * ``` + * + * @param time - The `Date` object to convert + */ public constructor(time: Date); + /** + * ##### Overview + * + * Parses a `DataAPITime` from a string in the format `HH:MM[:SS[.NNNNNNNNN]]`. + * + * See {@link DataAPITime} for more info about the exact format. + * + * @example + * ```ts + * new DataAPITime('12:00') // '12:00:00.000000000' + * + * time('12:34:56.78') // '12:34:56.780000000' + * ``` + * + * @param time - The time string to parse + * @param strict - Uses a faster parser which doesn't perform any validity or format checks if `false` + */ public constructor(time: string, strict?: boolean); + /** + * ##### Overview + * + * Creates a `DataAPITime` from an hour, a minute, and optional second and nanosecond components. + * + * All components must be zero-indexed positive integers within the following ranges: + * - `hour`: [0, 23] + * - `minute`: [0, 59] + * - `second`: [0, 59] + * - `nanosecond`: [0, 999,999,999] + * + * @example + * ```ts + * new DataAPIDate(20, 15) // '20:15:00.000000000' + * + * date(12, 12, 12, 12) // '12:12:12.000000012' + * ``` + * + * @param hours - The hour to use + * @param minutes - The minute to use + * @param seconds - The second to use (defaults to 0) + * @param nanoseconds - The nanosecond to use (defaults to 0) + */ public constructor(hours: number, minutes: number, seconds?: number, nanoseconds?: number); public constructor(i1: Date | string | number, i2?: boolean | number, i3?: number, i4?: number) { @@ -116,6 +299,13 @@ export class DataAPITime implements TableCodec { * * If no `base` date/time is provided to use the date from, the date component is set to be the current date. * + * @example + * ```ts + * time('12:00:00').toDate(new Date('1970-01-01')) // '1970-01-01T12:00:00.000' + * + * time('12:00:00').toDate() // 'T12:00:00.000' + * ``` + * * @param base - The base date/time to use for the date component * * @returns The `Date` object representing this `DataAPITime` @@ -137,17 +327,100 @@ export class DataAPITime implements TableCodec { /** * Returns the string representation of this `DataAPITime` * + * Note that it'll contain the second & nanosecond components, even if they weren't provided. + * + * @example + * ```ts + * time('12:00').toString() // '12:00:00.000000000' + * + * time(12, 34, 56, 78).toString() // '12:34:56.000000078' + * ``` + * * @returns The string representation of this `DataAPITime` */ public toString() { return `${this.hours < 10 ? '0' : ''}${this.hours}:${this.minutes < 10 ? '0' : ''}${this.minutes}:${this.seconds < 10 ? '0' : ''}${this.seconds}.${this.nanoseconds.toString().padStart(9, '0')}`; } + + /** + * ##### Overview + * + * Compares this `DataAPITime` to another `DataAPITime` + * + * @example + * ```ts + * time('12:00').compare(time(12, 0)) // 0 + * + * time('12:00').compare(time(12, 1)) // -1 + * + * time('12:01').compare(time(12, 0)) // 1 + * ``` + * + * @param other - The other `DataAPITime` to compare to + * + * @returns `0` if the times are equal, `-1` if this time is before the other, and `1` if this time is after the other + */ + public compare(other: DataAPITime): -1 | 0 | 1 { + if (this.hours !== other.hours) { + return this.hours < other.hours ? -1 : 1; + } + + if (this.minutes !== other.minutes) { + return this.minutes < other.minutes ? -1 : 1; + } + + if (this.seconds !== other.seconds) { + return this.seconds < other.seconds ? -1 : 1; + } + + if (this.nanoseconds !== other.nanoseconds) { + return this.nanoseconds < other.nanoseconds ? -1 : 1; + } + + return 0; + } + + /** + * ##### Overview + * + * Checks if this `DataAPITime` is equal to another `DataAPITime` + * + * @example + * ```ts + * time('12:00').equals(time(12, 0)) // true + * + * time('12:00').equals(time(12, 1)) // false + * + * time('12:00').equals('12:00:00.000000000') // true + * ``` + * + * @param other - The other `DataAPITime` to compare to + * + * @returns `true` if the times are equal, and `false` otherwise + */ + public equals(other: DataAPITime | string): boolean { + return (other as unknown instanceof DataAPITime) && this.compare(other as DataAPITime) === 0; + } } /** - * A shorthand function for `new DataAPITime(time?)` + * ##### Overview + * + * A shorthand function-object for {@link DataAPITime}. May be used anywhere when creating new `DataAPITime`s. + * + * See {@link DataAPITime} and its methods for information about input parameters, formats, functions, etc. + * + * @example + * ```ts + * // equiv. to `new DataAPITime('12:34:56')` + * time('12:34:56') * - * If no time is provided, it defaults to the current time. + * // equiv. to `new DataAPITime(12, 34) + * time(12, 34) + * + * // equiv. to `DataAPITime.now()` + * time.now() + * ``` * * @public */ @@ -228,20 +501,38 @@ const validateTime = (hours: number, minutes: number, seconds: number, nanosecon } }; -const ofSecondOfDay = (s: number): [number, number, number, number] => { +const ofSecondOfDay = (s: unknown): [number, number, number, number] => { + if (typeof s !== 'number') { + throw mkInvArgsErr('DataAPITime.ofSecondOfDay', [['secondOfDay', 'number']], s); + } + + if (s < 0 || 86_399 < s) { + throw RangeError(`Invalid number of seconds: ${s}; must be in range [0, 86,399]`); + } + const hours = ~~(s / 3_600); s -= hours * 3_600; const minutes = ~~(s / 60); s -= minutes * 60; + return [hours, minutes, s, 0]; }; -const ofNanoOfDay = (ns: number): [number, number, number, number] => { +const ofNanoOfDay = (ns: unknown): [number, number, number, number] => { + if (typeof ns !== 'number') { + throw mkInvArgsErr('DataAPITime.ofNanoOfDay', [['nanoOfDay', 'number']], ns); + } + + if (ns < 0 || 86_399_999_999_999 < ns) { + throw RangeError(`Invalid number of nanoseconds: ${ns}; must be in range [0, 86,399,999,999,999]`); + } + const hours = ~~(ns / 3_600_000_000_000); ns -= hours * 3_600_000_000_000; const minutes = ~~(ns / 60_000_000_000); ns -= minutes * 60_000_000_000; const seconds = ~~(ns / 1_000_000_000); ns -= seconds * 1_000_000_000; + return [hours, minutes, seconds, ns]; }; From e4662c1be0ce9d320f84d4c1d4a917a23bacd46c Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 9 Jan 2025 09:58:46 +0530 Subject: [PATCH 33/44] run build report --- etc/astra-db-ts.api.md | 69 +++++++++++++++++++-------------- src/documents/datatypes/date.ts | 17 ++++++++ src/documents/datatypes/time.ts | 23 +++++++++++ 3 files changed, 80 insertions(+), 29 deletions(-) diff --git a/etc/astra-db-ts.api.md b/etc/astra-db-ts.api.md index d248db75..4abcfb85 100644 --- a/etc/astra-db-ts.api.md +++ b/etc/astra-db-ts.api.md @@ -858,19 +858,22 @@ export interface DataAPICreateKeyspaceOptions extends WithTimeout<'keyspaceAdmin // @public export class DataAPIDate implements TableCodec { - static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (DataAPIDate | undefined)?]; + static [$DeserializeForTable](_: unknown, value: string, ctx: TableDesCtx): readonly [0, (DataAPIDate | undefined)?]; [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; - constructor(input?: string | Date | DataAPIDateComponents); - components(): DataAPIDateComponents; + constructor(date: Date); + constructor(date: string, strict?: boolean); + constructor(year: number, month: number, date: number); + compare(other: DataAPIDate): -1 | 0 | 1; + readonly date: number; + equals(other: DataAPIDate): boolean; + readonly month: number; + static now(): DataAPIDate; + static ofEpochDay(epochDays: number): DataAPIDate; + static ofYearDay(year: number, dayOfYear: number): DataAPIDate; toDate(base?: Date | DataAPITime): Date; toString(): string; -} - -// @public -export interface DataAPIDateComponents { - date: number; - month: number; - year: number; + static utcnow(): DataAPIDate; + readonly year: number; } // @public @@ -963,18 +966,21 @@ export class DataAPIResponseError extends DataAPIError { export class DataAPITime implements TableCodec { static [$DeserializeForTable](_: unknown, value: any, ctx: TableDesCtx): readonly [0, (DataAPITime | undefined)?]; [$SerializeForTable](ctx: TableSerCtx): readonly [0, (string | undefined)?]; - constructor(input?: string | Date | PartialDataAPITimeComponents); - components(): DataAPITimeComponents; + constructor(time: Date); + constructor(time: string, strict?: boolean); + constructor(hours: number, minutes: number, seconds?: number, nanoseconds?: number); + compare(other: DataAPITime): -1 | 0 | 1; + equals(other: DataAPITime | string): boolean; + readonly hours: number; + readonly minutes: number; + readonly nanoseconds: number; + static now(): DataAPITime; + static ofNanoOfDay(nanoOfDay: number): DataAPITime; + static ofSecondOfDay(secondOfDay: number): DataAPITime; + readonly seconds: number; toDate(base?: Date | DataAPIDate): Date; toString(): string; -} - -// @public -export interface DataAPITimeComponents { - hours: number; - minutes: number; - nanoseconds: number; - seconds: number; + static utcnow(): DataAPITime; } // @public @@ -1012,7 +1018,12 @@ export type DataAPIVectorLike = number[] | { } | Float32Array | DataAPIVector; // @public -export const date: (date?: string | Date | DataAPIDateComponents) => DataAPIDate; +export const date: ((...params: [string] | [Date] | [number, number, number]) => DataAPIDate) & { + now: typeof DataAPIDate.now; + utcnow: typeof DataAPIDate.utcnow; + ofEpochDay: typeof DataAPIDate.ofEpochDay; + ofYearDay: typeof DataAPIDate.ofYearDay; +}; // @public export class Db { @@ -1423,7 +1434,7 @@ export interface GenericUpdateOneOptions extends WithTimeout<'generalMethodTimeo export type GenericUpdateResult = (GuaranteedUpdateResult & UpsertedUpdateResult) | (GuaranteedUpdateResult & NoUpsertUpdateResult); // @public (undocumented) -export type GetCollNumRepFn = (path: string[]) => CollNumRep; +export type GetCollNumRepFn = (path: readonly string[]) => CollNumRep; // @public export interface GuaranteedUpdateResult { @@ -1655,11 +1666,6 @@ export type OneOrMany = T | readonly T[]; // @public export type OpaqueHttpClient = any; -// @public -export type PartialDataAPITimeComponents = (Omit & { - nanoseconds?: number; -}); - // @public (undocumented) export type PathCodec = { serialize?: Fns['serialize']; @@ -1713,7 +1719,7 @@ export interface ScalarCreateTableColumnDefinition { } // @public (undocumented) -export type SerDesFn = (key: string, value: any, ctx: Ctx) => readonly [0 | 1 | 2, any?, string?] | 'Return ctx.done(val?), ctx.recurse(val?), ctx.continue(), or void'; +export type SerDesFn = (key: string, value: any, ctx: Ctx) => readonly [0 | 1 | 2, any?] | 'Return ctx.done(val?), ctx.recurse(val?), ctx.continue(), or void'; // @public export interface SetCreateTableColumnDefinition { @@ -2002,7 +2008,12 @@ export interface TableVectorIndexOptions { } // @public -export const time: (time?: string | Date | PartialDataAPITimeComponents) => DataAPITime; +export const time: ((...params: [string] | [Date] | [number, number, number?, number?]) => DataAPITime) & { + now: typeof DataAPITime.now; + utcnow: typeof DataAPITime.utcnow; + ofNanoOfDay: typeof DataAPITime.ofNanoOfDay; + ofSecondOfDay: typeof DataAPITime.ofSecondOfDay; +}; // @public export type TimedOutCategories = OneOrMany | 'provided'; diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts index f78837bc..ca9b1c09 100644 --- a/src/documents/datatypes/date.ts +++ b/src/documents/datatypes/date.ts @@ -89,8 +89,25 @@ const MillisecondsPerDay = 1000 * 60 * 60 * 24; * @public */ export class DataAPIDate implements TableCodec { + /** + * The year component of this `DataAPIDate`. + * + * May be negative. + */ readonly year: number; + + /** + * The month component of this `DataAPIDate`. + * + * Must be between 1-12. + */ readonly month: number; + + /** + * The date component of this `DataAPIDate`. + * + * Must be a valid day for the given month. + */ readonly date: number; /** diff --git a/src/documents/datatypes/time.ts b/src/documents/datatypes/time.ts index a40e7c06..bc60da43 100644 --- a/src/documents/datatypes/time.ts +++ b/src/documents/datatypes/time.ts @@ -88,9 +88,32 @@ import { mkInvArgsErr } from '@/src/documents/utils'; * @public */ export class DataAPITime implements TableCodec { + /** + * The hour component of this `DataAPITime`. + * + * Must be between 0-23. + */ readonly hours: number; + + /** + * The minute component of this `DataAPITime`. + * + * Must be between 0-59. + */ readonly minutes: number; + + /** + * The second component of this `DataAPITime`. + * + * Must be between 0-59. + */ readonly seconds: number; + + /** + * The nanosecond component of this `DataAPITime`. + * + * Must be between 0-999,999,999. + */ readonly nanoseconds: number; /** From 98a9e66d0ec01e1855d6b2db2db8ad16ab688ddd Mon Sep 17 00:00:00 2001 From: toptobes Date: Thu, 9 Jan 2025 10:26:55 +0530 Subject: [PATCH 34/44] work for repl scirpt --- package-lock.json | 53 +++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + scripts/repl.sh | 57 ++++++++++++++++++++++++++++++++++++++--------- scripts/test.sh | 1 - 4 files changed, 101 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3c97fd4c..06bdc4c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "globals": "^15.9.0", "nyc": "^15.1.0", "strip-comments": "^2.0.1", + "synchronized-promise": "^0.3.1", "ts-mocha": "^10.0.0", "tsc-alias": "^1.8.8", "typescript": "^5.6.2", @@ -1542,6 +1543,16 @@ "node": ">=8" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1793,6 +1804,21 @@ "node": ">= 8" } }, + "node_modules/deasync": { + "version": "0.1.30", + "resolved": "https://registry.npmjs.org/deasync/-/deasync-0.1.30.tgz", + "integrity": "sha512-OaAjvEQuQ9tJsKG4oHO9nV1UHTwb2Qc2+fadB0VeVtD0Z9wiG1XPGLJ4W3aLhAoQSYTaLROFRbd5X20Dkzf7MQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^1.7.1" + }, + "engines": { + "node": ">=0.11.0" + } + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", @@ -2274,6 +2300,13 @@ "node": ">=16.0.0" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -3424,6 +3457,13 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "dev": true, + "license": "MIT" + }, "node_modules/node-preload": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-preload/-/node-preload-0.2.1.tgz", @@ -4377,6 +4417,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/synchronized-promise": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/synchronized-promise/-/synchronized-promise-0.3.1.tgz", + "integrity": "sha512-Iy+JzrERSUrwpOHUDku8HHIddk8V6iLG9bPIzboP2i5RYkn2eSmRB8waSaX7Rc/+DUUsnFsoOHrmniwOp9BOgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "deasync": "^0.1.15" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index 76de8901..b8bbfd0e 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "globals": "^15.9.0", "nyc": "^15.1.0", "strip-comments": "^2.0.1", + "synchronized-promise": "^0.3.1", "ts-mocha": "^10.0.0", "tsc-alias": "^1.8.8", "typescript": "^5.6.2", diff --git a/scripts/repl.sh b/scripts/repl.sh index 0b7a9c87..69d288c4 100755 --- a/scripts/repl.sh +++ b/scripts/repl.sh @@ -24,6 +24,26 @@ while [ $# -gt 0 ]; do "-l" | "-logging") export LOG_ALL_TO_STDOUT=true ;; + *) + if [ "$1" != "--help" ] && [ "$1" != "-help" ] && [ "$1" != "-h" ]; then + echo "Invalid flag $1" + echo + fi + echo "Usage: sh scripts/repl.sh [-local] [-l | -logging]" + echo + echo "* -local: Sets the environment to 'hcd' and attempts to use a locally running stargate instance" + echo "* -l | -logging: Logs helpful events to stdout" + echo + echo "Useful commands:" + echo + echo "cl: Clears the console" + echo "cda: Deletes all documents in the test collection" + echo "tda: Deletes all documents in the test table" + echo "cfa: Finds all documents in the test collection" + echo "tfa: Finds all documents in the test table" + echo "cif(doc): Inserts a row into the test collection and returns the inserted document" + echo "tif(row): Inserts a row into the test table and returns the inserted document" + exit esac shift done @@ -33,6 +53,7 @@ node -i -e " require('./node_modules/dotenv/config'); const $ = require('./dist'); + const sp = require('synchronized-promise') require('util').inspect.defaultOptions.depth = null; let client = new $.DataAPIClient(process.env.CLIENT_DB_TOKEN, { environment: process.env.CLIENT_DB_ENVIRONMENT, logging: [{ events: 'all', emits: 'event' }] }); @@ -41,10 +62,6 @@ node -i -e " const isAstra = process.env.CLIENT_DB_ENVIRONMENT === 'astra'; - // if (!isAstra) { - // await dbAdmin.createKeyspace('default_keyspace', { updateDbKeyspace: true }); - // } - let admin = (isAstra) ? client.admin() : null; @@ -65,27 +82,47 @@ node -i -e " }, }); - Object.defineProperty(this, 'cdm', { + Object.defineProperty(this, 'cda', { get() { - return coll.deleteMany(); + return sp(() => coll.deleteMany({}))(); }, }); - Object.defineProperty(this, 'tdm', { + Object.defineProperty(this, 'tda', { get() { - return table.deleteMany(); + return sp(() => table.deleteMany({}))(); }, }); Object.defineProperty(this, 'cfa', { get() { - return coll.find({}).toArray(); + return sp(() => coll.find({}).toArray())(); }, }); Object.defineProperty(this, 'tfa', { get() { - return table.find({}).toArray(); + return sp(() => table.find({}).toArray())(); }, }); + + const cif = sp(async (doc) => { + const { insertedId } = await coll.insertOne(doc); + return await coll.findOne({ _id: insertedId }); + }); + + const tif = sp(async (row) => { + row = { text: $.UUID.v4().toString(), int: 0, ...row }; + await table.insertOne(row); + return await table.findOne({ text: row.text, int: row.int }); + }); + + const originalEmit = process.emit; + + process.emit = function (name, data, ...args) { + if (name === 'warning' && typeof data === 'object' && data.name === 'DeprecationWarning') { + return false; + } + return originalEmit.apply(process, arguments); + }; " diff --git a/scripts/test.sh b/scripts/test.sh index 3d7f011d..7e5d8bc8 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -181,7 +181,6 @@ while [ $# -gt 0 ]; do echo " $(tput setaf 4)(12)$(tput setaf 9) $(tput bold)Skip tests setup to save time (prelude.test.ts)$(tput sgr0)" echo echo " By default, the test script will run a \"prelude\" script that sets up the database for the tests. This can be skipped to save some time, using this flag, if the DB is already setup (enough), and you just want to run some tests really quickly." - echo exit ;; esac From 4ea6a978a8aaa977831e83f6b012d2633f91e17f Mon Sep 17 00:00:00 2001 From: toptobes Date: Fri, 10 Jan 2025 00:36:08 +0530 Subject: [PATCH 35/44] DataAPIDuration massive overhaul --- src/documents/datatypes/date.ts | 2 +- src/documents/datatypes/duration.ts | 1402 ++++++++++++++++++++++++++- src/documents/datatypes/time.ts | 4 +- 3 files changed, 1388 insertions(+), 20 deletions(-) diff --git a/src/documents/datatypes/date.ts b/src/documents/datatypes/date.ts index ca9b1c09..9aba9e17 100644 --- a/src/documents/datatypes/date.ts +++ b/src/documents/datatypes/date.ts @@ -461,7 +461,7 @@ export const date = Object.assign( const parseDateStr = (str: unknown, strict: boolean): [number, number, number] => { if (typeof str !== 'string') { - throw mkInvArgsErr('DataAPIDate.parse', [['date', 'string']], str); + throw mkInvArgsErr('DataAPIDate', [['date', 'string']], str); } return (strict) diff --git a/src/documents/datatypes/duration.ts b/src/documents/datatypes/duration.ts index 1c8ca78a..07d2cd94 100644 --- a/src/documents/datatypes/duration.ts +++ b/src/documents/datatypes/duration.ts @@ -15,59 +15,1425 @@ import { $CustomInspect } from '@/src/lib/constants'; import { TableCodec, TableDesCtx, TableSerCtx } from '@/src/documents'; import { $DeserializeForTable, $SerializeForTable } from '@/src/documents/tables/ser-des/constants'; +import { mkInvArgsErr } from '@/src/documents/utils'; +import { Ref } from '@/src/lib/types'; -/** - * A shorthand function for `new DataAPIDuration(duration)` - * - * @public - */ -export const duration = (duration: string) => new DataAPIDuration(duration); +const NS_PER_HOUR = 3_600_000_000_000n; +const NS_PER_MIN = 60_000_000_000n; +const NS_PER_SEC = 1_000_000_000n; +const NS_PER_MS = 1_000_000n; +const NS_PER_US = 1_000n; /** + * #### Overview + * * Represents a `duration` column for Data API tables. * - * You may use the {@link duration} function as a shorthand for creating a new `DataAPIDuration`. + * #### Format + * + * The duration may be one of four different formats: + * + * ###### Standard duration format + * + * Matches `-?()+`, where the unit is one of: + * - `y` (years; 12 months) + * - `mo` (months) + * - `w` (weeks; 7 days) + * - `d` (days) + * - `h` (hours; 3,600,000,000,000 nanoseconds) + * - `m` (minutes; 60,000,000,000 nanoseconds) + * - `s` (seconds; 1,000,000,000 nanoseconds) + * - `ms` (milliseconds; 1,000,000 nanoseconds) + * - `us` or `µs` (microseconds; 1,000 nanoseconds) + * - `ns` (nanoseconds) + * + * At least one of the above units must be present, and they must be in the order shown above. + * + * Units in this format are case-insensitive. + * + * @example + * ```ts + * duration('1y2mo3w4d5h6m7s8ms9us10ns'); + * duration('-2w'); + * duration('0s'); + * ``` + * + * ###### ISO 8601 duration format + * + * Matches `-?P[T