diff --git a/lib/model.js b/lib/model.js index 6e4d573..8d962df 100644 --- a/lib/model.js +++ b/lib/model.js @@ -130,7 +130,7 @@ class BaseModel { const validate = schema[kSchemaCompiled]; const valid = validate(params); if (!valid) { - const e = new Error(`Document does not match schema for ${schema.name}: ${validate.errors[0]?.instancePath ?? ''} ${validate.errors[0]?.message}.`); + const e = new Error(`Document does not match schema for ${schema.name}: ${validate.errors[0].instancePath} ${validate.errors[0].message}.`); e.validationErrors = validate.errors; throw e; } @@ -321,7 +321,7 @@ class BaseModel { if(!BaseModel.#queryMany_options_validate(options)){ throw new Error(`Invalid options: ${inspect(BaseModel.#queryMany_options_validate.errors, {breakLength:Infinity})}.`); } - let {rawQueryOptions, rawFetchOptions, ...otherOptions} = options ?? {}; + let {rawQueryOptions, rawFetchOptions, ...otherOptions} = options; // returns an array of models (possibly empty) const rawQuery = BaseModel.#convertQuery(this, query, Object.assign({startAfter: otherOptions.startAfter, limit: otherOptions.limit}, rawQueryOptions)); @@ -355,7 +355,7 @@ class BaseModel { throw new Error(`Invalid options: ${inspect(BaseModel.#queryManyIds_options_validate.errors, {breakLength:Infinity})}.`); } // options are as queryMany, except Ids are returned, so there are no rawFetchOptions - let {rawQueryOptions, ...otherOptions} = options ?? {}; + let {rawQueryOptions, ...otherOptions} = options; otherOptions = Object.assign({limit: 50}, otherOptions); const rawQuery = BaseModel.#convertQuery(this, query, Object.assign({startAfter: otherOptions.startAfter, limit: otherOptions.limit}, rawQueryOptions)); const results = []; @@ -418,7 +418,7 @@ class BaseModel { const marshall = schema[kSchemaMarshall]; const marshallValid = marshall(properties); if (!marshallValid) { - const e = new Error(`Document does not match schema for ${schema.name}: ${marshall.errors[0]?.instancePath ?? ''} ${marshall.errors[0]?.message}.`); + const e = new Error(`Document does not match schema for ${schema.name}: ${marshall.errors[0].instancePath} ${marshall.errors[0].message}.`); e.validationErrors = schema[kSchemaCompiled].errors; throw e; } @@ -555,9 +555,9 @@ class BaseModel { // if we've loaded a model of a different type, and there is nothing let e; if (params.type !== DerivedModel[kModelSchema].name) { - e = new Error(`Document does not match schema for ${schema.name}. The loaded document has a different type "${params.type}", and the schema is incompatible: ${unmarshall.errors[0]?.instancePath ?? ''} ${unmarshall.errors[0]?.message}.`); + e = new Error(`Document does not match schema for ${schema.name}. The loaded document has a different type "${params.type}", and the schema is incompatible: ${unmarshall.errors[0].instancePath} ${unmarshall.errors[0].message}.`); } else { - e = new Error(`Document does not match schema for ${schema.name}: ${unmarshall.errors[0]?.instancePath ?? ''} ${unmarshall.errors[0]?.message}.`); + e = new Error(`Document does not match schema for ${schema.name}: ${unmarshall.errors[0].instancePath} ${unmarshall.errors[0].message}.`); } e.validationErrors = unmarshall.errors; throw e; @@ -570,8 +570,7 @@ class BaseModel { // get an instance of this schema by id static async #getById(DerivedModel, id, rawOptions) { - // only the ConsistentRead option is supported - const { ConsistentRead, abortSignal } = rawOptions ?? {}; + const { ConsistentRead, abortSignal } = rawOptions; const table = DerivedModel[kModelTable]; const schema = DerivedModel[kModelSchema]; /* c8 ignore next 3 */ @@ -602,8 +601,7 @@ class BaseModel { // get an array of instances of this schema by id static async #getByIds(DerivedModel, ids, rawOptions) { - // only the ConsistentRead option is supported - const { ConsistentRead, abortSignal } = rawOptions ?? {}; + const { ConsistentRead, abortSignal } = rawOptions; const table = DerivedModel[kModelTable]; const schema = DerivedModel[kModelSchema]; /* c8 ignore next 3 */ @@ -672,7 +670,7 @@ class BaseModel { // ids of the stored records that match the query. Use getById to get the // object for each ID. static async* #rawQueryIds(DerivedModel, rawQuery, options) { - const {limit, abortSignal} = options?? {}; + const {limit, abortSignal} = options; const table = DerivedModel[kModelTable]; const schema = DerivedModel[kModelSchema]; /* c8 ignore next 3 */ @@ -708,7 +706,7 @@ class BaseModel { // query for instances of this schema, an async generator returning arrays of ids matching the query, up to options.limit. // rawQueryIdsBatchIterator does NOT set the TableName, unlike other #rawQuery* APIs. static async* #rawQueryIdsBatchIterator(DerivedModel, rawQuery, options) { - const {limit, abortSignal} = options?? {}; + const {limit, abortSignal} = options; const table = DerivedModel[kModelTable]; const schema = DerivedModel[kModelSchema]; /* c8 ignore next 3 */ @@ -719,7 +717,7 @@ class BaseModel { ...(abortSignal && {abortSignal}) }; let response; - let limitRemaining = limit ?? Infinity; + let limitRemaining = limit; do { const command = new QueryCommand(rawQuery); DerivedModel[kModelLogger].trace({command}, 'rawQueryIdsBatch'); @@ -835,7 +833,7 @@ class BaseModel { // ExpressionAttributeNames and ExpressionAttributeValues), but in // practise this is not useful without additional projected attributes // on indexes, so I'm not sure this should be supported. - const {ExpressionAttributeNames, ExpressionAttributeValues, startAfter, ...otherOptions} = options ?? {}; + const {ExpressionAttributeNames, ExpressionAttributeValues, startAfter, ...otherOptions} = options; const table = DerivedModel[kModelTable]; const schema = DerivedModel[kModelSchema]; const queryEntries = this.#queryEntries(query); @@ -882,7 +880,7 @@ class BaseModel { for (const v of entry.values) { const valid = defaultIgnoringAjv.validate(keySchema, v); if (!valid) { - const e = new Error(`Value does not match schema for ${entry.key}: ${defaultIgnoringAjv.errors[0]?.instancePath ?? ''} ${defaultIgnoringAjv.errors[0]?.message}.`); + const e = new Error(`Value does not match schema for ${entry.key}: ${defaultIgnoringAjv.errors[0].instancePath} ${defaultIgnoringAjv.errors[0].message}.`); e.validationErrors = defaultIgnoringAjv.errors; throw e; } diff --git a/lib/schema.js b/lib/schema.js index eb3695d..41dc0f9 100644 --- a/lib/schema.js +++ b/lib/schema.js @@ -142,7 +142,7 @@ class Schema { [kSchemaUnMarshall] = null; constructor(name, schemaSource, options) { - const { index, generateId, versioning } = options ?? {}; + const { index, generateId, versioning } = options; if (['object', 'undefined'].includes(typeof schemaSource) === false) { throw new Error('Invalid schema: must be an object or undefined.'); } diff --git a/test/model.js b/test/model.js new file mode 100644 index 0000000..b5234f3 --- /dev/null +++ b/test/model.js @@ -0,0 +1,181 @@ +const t = require('tap'); + +const clientOptions = { + endpoint: 'http://localhost:8000' +}; + +const DynamoDMConstructor = require('../'); +const DynamoDM = DynamoDMConstructor({clientOptions, logger:{level:'error'}}); + +t.test('model:', async t => { + const table = DynamoDM.Table({ name: 'test-table-models'}); + const FooSchema = DynamoDM.Schema('namespace.foo', { + properties: { + id: DynamoDM.DocIdField, + fooVal: {type: 'number'}, + blob: DynamoDM.Binary, + padding: DynamoDM.Binary + } + }); + const BarSchema = DynamoDM.Schema('ambiguous.bar', { + properties: { + id: DynamoDM.DocIdField, + barVal: {type: 'number'}, + barValStr: {type: 'string'}, + blob: DynamoDM.Binary + }, + required:['barVal'] + }, { + index: { + barValRange: { + hashKey: 'type', + sortKey: 'barVal' + }, + barValRangeStr: { + hashKey: 'type', + sortKey: 'barValStr' + } + } + }); + const AmbiguousSchema = DynamoDM.Schema('ambiguous', { + properties: { + id: DynamoDM.DocIdField, + } + }); + const Foo = table.model(FooSchema); + table.model(BarSchema); + table.model(AmbiguousSchema); + + const all_foos = []; + for (let i = 0; i < 50; i++ ) { + // padd the Foo items out to 350KBk each, so that we can test bumping up against dynamoDB's 16MB response limit + let foo = new Foo({fooVal:i, blob: Buffer.from(`hello query ${i}`), padding: Buffer.alloc(3.5E5)}); + all_foos.push(foo); + await foo.save(); + } + + t.after(async () => { + await table.deleteTable(); + table.destroyConnection(); + }); + + + t.test('getById', async t => { + t.rejects(Foo.getById(null), 'should reject null id'); + t.rejects(Foo.getById('someid', {foo:1}), 'should reject invalid option'); + t.rejects(Foo.getById(''), 'should reject empty id'); + t.rejects(Foo.getById(123), 'should reject numeric id'); + t.end(); + }); + + t.test('getById options', async t => { + t.test('ConsistentRead: true', async t => { + const foo = await Foo.getById(all_foos[0].id, {ConsistentRead: true}); + t.ok(foo, 'should return a foo'); + t.equal(foo.constructor, Foo, 'should return the correct type'); + t.end(); + }); + t.test('ConsistentRead: false', async t => { + const foo = await Foo.getById(all_foos[0].id, {ConsistentRead: false}); + t.ok(foo, 'should return a foo'); + t.equal(foo.constructor, Foo, 'should return the correct type'); + t.end(); + }); + + t.test('empty options', async t => { + const foo = await Foo.getById(all_foos[0].id, {}); + t.ok(foo, 'should return a foo'); + t.equal(foo.constructor, Foo, 'should return the correct type'); + t.end(); + }); + + t.end(); + }); + + t.test('table.getById', async t => { + t.rejects(table.getById('blegh.someid'), new Error('Table has no matching model type for id "blegh.someid", so it cannot be loaded.'), 'should reject unknown type'); + t.rejects(table.getById('ambiguous.bar.someid'), new Error('Table has multiple ambiguous model types for id "ambiguous.bar.someid", so it cannot be loaded generically.'), 'should reject ambiguous type'); + const foo = await table.getById(all_foos[0].id); + t.equal(foo.constructor, Foo, 'should get the correct type'); + t.equal(foo.id, all_foos[0].id, 'should get the correct document'); + }); + + t.test('getByIds', async t => { + t.rejects(Foo.getByIds([null]), 'should reject null id'); + t.rejects(Foo.getByIds(['someid'], {foo:1}), 'should reject invalid option'); + t.match(await Foo.getByIds(['nonexistent']), [null], 'should return null for nonexistent id'); + t.match(await Foo.getByIds(['nonexistent', all_foos[0].id]), [null, all_foos[0]], 'should return null along with extant model'); + const foos = await Foo.getByIds(all_foos.map(f => f.id)); + t.equal(foos.length, all_foos.length, 'should return all models'); + t.match(foos.map(f => f?.id), all_foos.map(f => f?.id), 'should return all models in order'); + t.rejects(Foo.getByIds(''), new Error('Invalid ids: must be array of strings of nonzero length.'), 'should reject non-array argument'); + t.end(); + }); + + t.test('getByIds options', async t => { + t.test('ConsistentRead: true', async t => { + const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {ConsistentRead: true}); + t.equal(foos.length, 2, 'should return the right number of foos'); + t.equal(foos[0].constructor, Foo, 'should return the correct type'); + t.end(); + }); + t.test('ConsistentRead: false', async t => { + const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {ConsistentRead: false}); + t.equal(foos.length, 2, 'should return the right number of foos'); + t.equal(foos[0].constructor, Foo, 'should return the correct type'); + t.end(); + }); + + t.test('empty options', async t => { + const foos = await Foo.getByIds([all_foos[0].id, all_foos[1].id], {}); + t.equal(foos.length, 2, 'should return the right number of foos'); + t.equal(foos[0].constructor, Foo, 'should return the correct type'); + t.end(); + }); + t.end(); + }); + + t.test('getByIds exceeding retries', async t => { + const table2 = DynamoDM.Table({ name: 'test-table-models', retry: { maxRetries:0 }}); + t.after(async () => { table2.destroyConnection(); }); + const Foo2 = table2.model(FooSchema); + t.rejects(Foo2.getByIds(all_foos.map(f => f.id)), {message:'Request failed: maximum retries exceeded.'}, 'getByIds with a large number of large responses should require retries for BatchGetCommand.'); + t.end(); + }); + + t.test('aborting getByIds', async t => { + const ac0 = new AbortController(); + ac0.abort(new Error('my reason 0 ')); + // the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future) + t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController that is already aborted'); + + const ac1 = new AbortController(); + // the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future) + t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal immediately'); + ac1.abort(new Error('my reason')); + + const ac2 = new AbortController(); + t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal asynchronously'); + setTimeout(() => { + ac2.abort(new Error('my reason 2')); + }, 1); + t.end(); + }); + + t.test('aborting getById', async t => { + const ac0 = new AbortController(); + ac0.abort(new Error('my reason 0 ')); + t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController that is already aborted'); + + const ac1 = new AbortController(); + t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal immediately'); + ac1.abort(new Error('my reason')); + + const ac2 = new AbortController(); + t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal asynchronously'); + setTimeout(() => { + ac2.abort(new Error('my reason 2')); + }, 1); + t.end(); + }); +}); diff --git a/test/queries.js b/test/queries.js index 4d6cb2e..f678b04 100644 --- a/test/queries.js +++ b/test/queries.js @@ -178,7 +178,7 @@ t.test('queries:', async t => { // t.end(); // }); - await t.test('rawQueryOneId', async t => { + t.test('rawQueryOneId', async t => { t.test('on type index', async t => { const foo_id = await Foo.rawQueryOneId({ IndexName:'type', @@ -218,79 +218,7 @@ t.test('queries:', async t => { }); }); - await t.test('getById', t => { - t.rejects(Foo.getById(null), 'should reject null id'); - t.rejects(Foo.getById('someid', {foo:1}), 'should reject invalid option'); - t.rejects(Foo.getById(''), 'should reject empty id'); - t.rejects(Foo.getById(123), 'should reject numeric id'); - t.end(); - }); - - await t.test('table.getById', async t => { - t.rejects(table.getById('blegh.someid'), new Error('Table has no matching model type for id "blegh.someid", so it cannot be loaded.'), 'should reject unknown type'); - t.rejects(table.getById('ambiguous.bar.someid'), new Error('Table has multiple ambiguous model types for id "ambiguous.bar.someid", so it cannot be loaded generically.'), 'should reject ambiguous type'); - const foo = await table.getById(all_foos[0].id); - t.equal(foo.constructor, Foo, 'should get the correct type'); - t.equal(foo.id, all_foos[0].id, 'should get the correct document'); - }); - - await t.test('getByIds', async t => { - await t.rejects(Foo.getByIds([null]), 'should reject null id'); - t.rejects(Foo.getByIds(['someid'], {foo:1}), 'should reject invalid option'); - t.match(await Foo.getByIds(['nonexistent']), [null], 'should return null for nonexistent id'); - t.match(await Foo.getByIds(['nonexistent', all_foos[0].id]), [null, all_foos[0]], 'should return null along with extant model'); - const foos = await Foo.getByIds(all_foos.map(f => f.id)); - t.equal(foos.length, all_foos.length, 'should return all models'); - t.match(foos.map(f => f?.id), all_foos.map(f => f?.id), 'should return all models in order'); - await t.rejects(Foo.getByIds(''), new Error('Invalid ids: must be array of strings of nonzero length.'), 'should reject non-array argument'); - t.end(); - }); - - t.test('getByIds exceeding retries', async t => { - const table2 = DynamoDM.Table({ name: 'test-table-queries', retry: { maxRetries:0 }}); - t.after(async () => { table2.destroyConnection(); }); - const Foo2 = table2.model(FooSchema); - t.rejects(Foo2.getByIds(all_foos.map(f => f.id)), {message:'Request failed: maximum retries exceeded.'}, 'getByIds with a large number of large responses should require retries for BatchGetCommand.'); - t.end(); - }); - - t.test('aborting getByIds', async t => { - const ac0 = new AbortController(); - ac0.abort(new Error('my reason 0 ')); - // the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future) - t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController that is already aborted'); - - const ac1 = new AbortController(); - // the AWS SDk doesn't propagate the abort reason (but it would be nice if it did in the future) - t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal immediately'); - ac1.abort(new Error('my reason')); - - const ac2 = new AbortController(); - t.rejects(Foo.getByIds(all_foos.map(f => f.id), {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getByIds should be abortable with an AbortController signal asynchronously'); - setTimeout(() => { - ac2.abort(new Error('my reason 2')); - }, 1); - t.end(); - }); - - t.test('aborting getById', async t => { - const ac0 = new AbortController(); - ac0.abort(new Error('my reason 0 ')); - t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac0.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController that is already aborted'); - - const ac1 = new AbortController(); - t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac1.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal immediately'); - ac1.abort(new Error('my reason')); - - const ac2 = new AbortController(); - t.rejects(Foo.getById(all_foos[0].id, {abortSignal: ac2.signal}), {name:'AbortError', message:'Request aborted'}, 'getById should be abortable with an AbortController signal asynchronously'); - setTimeout(() => { - ac2.abort(new Error('my reason 2')); - }, 1); - t.end(); - }); - - await t.test('rawQueryManyIds', async t => { + t.test('rawQueryManyIds', async t => { t.test('on type index', async t => { const foo_ids = await Foo.rawQueryManyIds({ IndexName:'type', @@ -323,7 +251,7 @@ t.test('queries:', async t => { t.end(); }); - await t.test('rawQueryIteratorIds', async t => { + t.test('rawQueryIteratorIds', async t => { t.test('on type index', async t => { const foo_ids = await arrayFromAsync(Foo.rawQueryIteratorIds({ IndexName:'type', @@ -421,7 +349,7 @@ t.test('queries:', async t => { t.end(); }); - await t.test('query api', async t => { + t.test('query api', async t => { // TODO: // * tests for $gt $lt conditions on hash-only indexes to make sure we reject with a nice error (currently the error comes from dynamodb doc client and is not very helpful) t.test('queryMany', async t => { @@ -455,6 +383,14 @@ t.test('queries:', async t => { t.equal(nb[0].constructor, (new IndexedNumberAndBinary()).constructor, 'should have correct constructor'); t.equal(nb[0].num, 7, 'should return the matching item'); }); + t.test('with options={}', async t => { + const foos = await Foo.queryMany({ type: 'namespace.foo' }, {}); + t.equal(foos.length, all_foos.length, 'should return all N of this type'); + }); + t.test('with limit', async t => { + const foos = await Foo.queryMany({ type: 'namespace.foo' }, {rawQueryOptions: {}, rawFetchOptions: {}, limit: all_foos.length}); + t.equal(foos.length, all_foos.length, 'should return all N of this type'); + }); t.test('invalid queries', async t => { t.rejects(Foo.queryMany({type: 'namespace.foo', fooVal:3 }), {message:'Unsupported query: "{ type: \'namespace.foo\', fooVal: 3 }". No index found for query fields [type, fooVal]'}, 'rejects non-queryable extra parameters'); t.rejects(IndexedNumberAndBinary.queryMany({'blob': Buffer.from('hello query 3'), 'num':7, id:'123' }), {message:'Unsupported query: "{ blob: , num: 7, id: \'123\' }" Queries must have at most two properties to match against index hash and range attributes.'}, 'rejects more than two parameters'); @@ -593,6 +529,8 @@ t.test('queries:', async t => { t.test('queryManyIds', async t => { const allFooIds = await Foo.queryManyIds({ type: 'namespace.foo' }); t.match(allFooIds.sort(), all_foos.map(f => f.id).sort(), 'should return all IDs'); + t.resolves(Foo.queryManyIds({ type: 'namespace.foo' }, {}), 'should accept empty options'); + t.resolves(Foo.queryManyIds({ type: 'namespace.foo' }, {limit:3}), 'should accept limit option'); t.end(); }); @@ -668,28 +606,36 @@ t.test('queries:', async t => { t.rejects(Foo.queryOne({ type: 'namespace.foo' }, {invalidOption:123}), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/additionalProperties', keyword: 'additionalProperties', params: { additionalProperty: 'invalidOption' }, message: 'must NOT have additional properties' } ]."}, 'rejects invalid option'); t.rejects(Foo.queryOne({ type: 'namespace.foo' }, {limit:2}), {message:"Invalid options: [ { instancePath: '/limit', schemaPath: '#/properties/limit/const', keyword: 'const', params: { allowedValue: 1 }, message: 'must be equal to constant' } ]"}, 'rejects incorrect option value'); t.rejects(Foo.queryOne({ type: 'namespace.foo' }, {startAfter:'somestring'}), {message:"Invalid options: [ { instancePath: '/startAfter', schemaPath: '#/properties/startAfter/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects incorrect option type'); + t.rejects(Foo.queryOne({ type: 'namespace.foo' }, null), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects null options'); + t.rejects(Foo.queryOne({ type: 'namespace.foo' }, true), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects truthy invalid options'); t.end(); }); t.test('queryOneId', async t => { t.rejects(Foo.queryOneId({ type: 'namespace.foo' }, {rawFetchOptions:{}}), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/additionalProperties', keyword: 'additionalProperties', params: { additionalProperty: 'rawFetchOptions' }, message: 'must NOT have additional properties' } ]."}, 'rejects invalid option'); t.rejects(Foo.queryOneId({ type: 'namespace.foo' }, {limit:2}), {message:"Invalid options: [ { instancePath: '/limit', schemaPath: '#/properties/limit/const', keyword: 'const', params: { allowedValue: 1 }, message: 'must be equal to constant' } ]"}, 'rejects incorrect option value'); t.rejects(Foo.queryOneId({ type: 'namespace.foo' }, {startAfter:'somestring'}), {message:"Invalid options: [ { instancePath: '/startAfter', schemaPath: '#/properties/startAfter/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects incorrect option type'); + t.rejects(Foo.queryOneId({ type: 'namespace.foo' }, null), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects null options'); + t.rejects(Foo.queryOneId({ type: 'namespace.foo' }, true), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects truthy invalid options'); t.end(); }); t.test('queryMany', async t => { t.rejects(Foo.queryMany({ type: 'namespace.foo' }, {invalidOption:123}), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/additionalProperties', keyword: 'additionalProperties', params: { additionalProperty: 'invalidOption' }, message: 'must NOT have additional properties' } ]."}, 'rejects invalid option'); t.rejects(Foo.queryMany({ type: 'namespace.foo' }, {startAfter:'somestring'}), {message:"Invalid options: [ { instancePath: '/startAfter', schemaPath: '#/properties/startAfter/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects incorrect option type'); + t.rejects(Foo.queryMany({ type: 'namespace.foo' }, null), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects null options'); + t.rejects(Foo.queryMany({ type: 'namespace.foo' }, true), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects truthy invalid options'); t.end(); }); t.test('queryManyIds', async t => { t.rejects(Foo.queryManyIds({ type: 'namespace.foo' }, {rawFetchOptions:{}}), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/additionalProperties', keyword: 'additionalProperties', params: { additionalProperty: 'rawFetchOptions' }, message: 'must NOT have additional properties' } ]."}, 'rejects invalid option'); t.rejects(Foo.queryManyIds({ type: 'namespace.foo' }, {startAfter:'somestring'}), {message:"Invalid options: [ { instancePath: '/startAfter', schemaPath: '#/properties/startAfter/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects incorrect option type'); + t.rejects(Foo.queryManyIds({ type: 'namespace.foo' }, null), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects null options'); + t.rejects(Foo.queryManyIds({ type: 'namespace.foo' }, true), {message:"Invalid options: [ { instancePath: '', schemaPath: '#/type', keyword: 'type', params: { type: 'object' }, message: 'must be object' } ]."}, 'rejects truthy invalid options'); t.end(); }); await t.end(); }); - await t.test('add missing indexes', async t => { + t.test('add missing indexes', async t => { const table3 = DynamoDM.Table({ name: 'test-compatible-indexes'}); t.teardown(async () => { await table3.deleteTable(); diff --git a/test/table.js b/test/table.js index 6ae7cfa..1d1e973 100644 --- a/test/table.js +++ b/test/table.js @@ -101,6 +101,47 @@ t.test('waiting for table creation', async t => { t.equal(commandSendResults().length, 3, 'Should wait for success.'); }); +t.test('waiting for table UPDATING', async t => { + const table = DynamoDM.Table({ name: 'test-table-slow-creation', clientOptions}); + table.model(DynamoDM.Schema('emptySchema')); + + const originalSend = table.client.send; + + // return a dummy UPDATING response, which should be considered a + // satisfactory response that the table exists (and must have been + // previously created / updated by a separate process) + const commandSendResults = t.capture(table.docClient, 'send', async function(command){ + if (command instanceof DescribeTableCommand) { + // return a dummy 'UPDATING' response to DescribeTableCommand + return { + Table: { + TableStatus: 'UPDATING', + KeySchema: [{ + AttributeName: 'id', + KeyType: 'HASH' + }], + GlobalSecondaryIndexes: [ + { + IndexName: 'type', + KeySchema: [ + { AttributeName: 'type', KeyType: 'HASH' }, + { AttributeName: 'id', KeyType: 'RANGE'} + ], + Projection: { ProjectionType: 'KEYS_ONLY' }, + } + ] + }, + }; + } + // eslint-disable-next-line + return originalSend.apply(this, arguments); + }); + + await table.ready(); + + t.equal(commandSendResults().length, 2, 'UPDATING should be considered created, requiring only two responses.'); +}); + t.test('table consistency:', async t => { t.test('throws on inconsistent id field names', async t => { const table = DynamoDM.Table({ name: 'test-table-errors'});