From 11dbd77be6a130851d967b10338db6d5c26a7579 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Tue, 24 Dec 2024 21:39:10 +0000 Subject: [PATCH 1/2] fix: empty object all results --- src/index.ts | 10 ++++++---- test/query.ts | 11 +++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/index.ts b/src/index.ts index dfecf44..3a2de4a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -28,7 +28,7 @@ export type DoubleDb = { patch: (id: string, newDocument: Partial) => Promise; remove: (id: string) => Promise; read: (id: string) => Promise; - query: (queryObject: object) => Promise; + query: (queryObject?: object) => Promise; close: () => Promise; batchInsert: (documents: Document[]) => Promise; upsert: (id: string, document: Document) => Promise; @@ -327,9 +327,11 @@ async function createDoubleDb(dataDirectory: string): Promise { return db.del(id); } - async function query(queryObject: object): Promise { - if (!isObject(queryObject)) { - throw new Error('doubledb.query: queryObject must be an object'); + async function query(queryObject?: object): Promise { + if (!queryObject || Object.keys(queryObject).length === 0) { + const allIds = await getAllIds(); + const results = await Promise.all([...allIds].map(id => read(id))); + return results.filter((doc): doc is Document => doc !== undefined); } let resultIds = new Set(); diff --git a/test/query.ts b/test/query.ts index 3c29b98..b2e44e9 100644 --- a/test/query.ts +++ b/test/query.ts @@ -15,6 +15,17 @@ async function setupTestDb() { return db; } +test('empty query', async () => { + const db = await setupTestDb(); + await db.insert({ value: 'alpha' }); + await db.insert({ value: 'beta' }); + const result = await db.query({}); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].value, 'alpha'); + assert.strictEqual(result[1].value, 'beta'); + await db.close(); +}); + test('$sw operator on string', async () => { const db = await setupTestDb(); await db.insert({ value: 'alpha' }); From c6585a139b58ca45647ec6fbf7436e988b97c0a5 Mon Sep 17 00:00:00 2001 From: Mark Wylde Date: Tue, 24 Dec 2024 22:13:39 +0000 Subject: [PATCH 2/2] feat: implement limit, offset, sort, present --- README.md | 13 +++++++++- src/index.ts | 66 +++++++++++++++++++++++++++++++++++++++++++++----- test/query.ts | 67 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 496c220..fe41b7b 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ The final record will be: ### `.remove(key)` Query the database using a complex query object. This method allows for advanced querying using a combination of fields and operators. -### `.query(queryObject)` +### `.query(queryObject, options)` Query the database using a complex query object. This method allows for advanced querying using a combination of fields and operators. **Example:** @@ -153,6 +153,11 @@ const records = await doubledb.query({ { firstName: { $eq: 'Joe' } }, { firstName: { $eq: 'joe' } } ] +}, { + limit: 10, + offset: 5, + sort: { firstName: 1 }, + project: { firstName: 1, lastName: 1 } }); ``` @@ -195,6 +200,12 @@ const records = await doubledb.query({ - **$exists**: Matches documents where the field exists (or does not exist if set to false). - **$not**: Matches documents that do not match the specified condition. +### Query Options: +- **limit**: Limits the number of returned records. +- **offset**: Skips the first `number` of records. +- **sort**: Sorts the records based on the specified fields. Use `1` for ascending and `-1` for descending. +- **project**: Projects only the specified fields in the returned records. + This query method is powerful and allows combining multiple conditions and operators to fetch the desired records from the database. ### `.batchInsert(documents)` diff --git a/src/index.ts b/src/index.ts index 3a2de4a..0ced148 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,8 @@ type QueryOptions = { lt?: string; lte?: string; gte?: string; + sort?: { [key: string]: 1 | -1 }; + project?: { [key: string]: 1 }; } export type DoubleDb = { @@ -28,7 +30,7 @@ export type DoubleDb = { patch: (id: string, newDocument: Partial) => Promise; remove: (id: string) => Promise; read: (id: string) => Promise; - query: (queryObject?: object) => Promise; + query: (queryObject?: object, options?: { limit?: number; offset?: number; sort?: { [key: string]: 1 | -1 }; project?: { [key: string]: 1 } }) => Promise; close: () => Promise; batchInsert: (documents: Document[]) => Promise; upsert: (id: string, document: Document) => Promise; @@ -327,11 +329,37 @@ async function createDoubleDb(dataDirectory: string): Promise { return db.del(id); } - async function query(queryObject?: object): Promise { + async function query(queryObject?: object, options?: { limit?: number; offset?: number; sort?: { [key: string]: 1 | -1 }; project?: { [key: string]: 1 } }): Promise { if (!queryObject || Object.keys(queryObject).length === 0) { const allIds = await getAllIds(); - const results = await Promise.all([...allIds].map(id => read(id))); - return results.filter((doc): doc is Document => doc !== undefined); + let results = await Promise.all([...allIds].map(id => read(id))); + const offset = options?.offset ?? 0; + const limit = options?.limit ?? results.length; + + if (options?.sort) { + const sortFields = Object.entries(options.sort); + results.sort((a, b) => { + for (const [field, direction] of sortFields) { + if (a[field] < b[field]) return direction === 1 ? -1 : 1; + if (a[field] > b[field]) return direction === 1 ? 1 : -1; + } + return 0; + }); + } + + if (options?.project) { + results = results.map(doc => { + const projected: Document = {}; + for (const field of Object.keys(options.project)) { + if (field in doc) { + projected[field] = doc[field]; + } + } + return projected; + }); + } + + return results.filter((doc): doc is Document => doc !== undefined).slice(offset, offset + limit); } let resultIds = new Set(); @@ -356,8 +384,34 @@ async function createDoubleDb(dataDirectory: string): Promise { isFirstCondition = false; } - const results = await Promise.all([...resultIds].map(id => read(id))); - return results.filter((doc): doc is Document => doc !== undefined); + let results = await Promise.all([...resultIds].map(id => read(id))); + const offset = options?.offset ?? 0; + const limit = options?.limit ?? results.length; + + if (options?.sort) { + const sortFields = Object.entries(options.sort); + results.sort((a, b) => { + for (const [field, direction] of sortFields) { + if (a[field] < b[field]) return direction === 1 ? -1 : 1; + if (a[field] > b[field]) return direction === 1 ? 1 : -1; + } + return 0; + }); + } + + if (options?.project) { + results = results.map(doc => { + const projected: Document = {}; + for (const field of Object.keys(options.project)) { + if (field in doc) { + projected[field] = doc[field]; + } + } + return projected; + }); + } + + return results.filter((doc): doc is Document => doc !== undefined).slice(offset, offset + limit); } async function handleOperators(key: string, operators: object): Promise> { diff --git a/test/query.ts b/test/query.ts index b2e44e9..f79ab01 100644 --- a/test/query.ts +++ b/test/query.ts @@ -21,6 +21,7 @@ test('empty query', async () => { await db.insert({ value: 'beta' }); const result = await db.query({}); assert.strictEqual(result.length, 2); + result.sort((a, b) => a.value < b.value ? -1 : 1); assert.strictEqual(result[0].value, 'alpha'); assert.strictEqual(result[1].value, 'beta'); await db.close(); @@ -162,3 +163,69 @@ test('Complex query with $or', async () => { assert.strictEqual(result[0].category, 'b'); await db.close(); }); + +test('query limit, offset, sort', async () => { + const db = await setupTestDb(); + for (let i = 0; i < 10; i++) { + await db.insert({ value: i }); + } + const result = await db.query({}, { + limit: 3, + offset: 2, + sort: { + value: 1 + } + }); + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].value, 2); + assert.strictEqual(result[1].value, 3); + assert.strictEqual(result[2].value, 4); + + await db.close(); +}); + +test('query with sort option', async () => { + const db = await setupTestDb(); + await db.insert({ value: 'gamma' }); + await db.insert({ value: 'alpha' }); + await db.insert({ value: 'beta' }); + const result = await db.query({}, { sort: { value: 1 } }); + assert.strictEqual(result.length, 3); + assert.strictEqual(result[0].value, 'alpha'); + assert.strictEqual(result[1].value, 'beta'); + assert.strictEqual(result[2].value, 'gamma'); + await db.close(); +}); + +test('query with project option', async () => { + const db = await setupTestDb(); + await db.insert({ value: 'gamma', extra: 'data' }); + await db.insert({ value: 'alpha', extra: 'info' }); + const result = await db.query({}, { project: { value: 1 } }); + + result.sort((a, b) => a.value < b.value ? -1 : 1); + assert.strictEqual(result.length, 2); + assert.strictEqual(Object.keys(result[0]).length, 1); + assert.strictEqual(result[0].value, 'alpha'); + assert.strictEqual(Object.keys(result[1]).length, 1); + assert.strictEqual(result[1].value, 'gamma'); + + await db.close(); +}); + +test('query with sort and project options', async () => { + const db = await setupTestDb(); + await db.insert({ value: 'gamma', extra: 'data' }); + await db.insert({ value: 'alpha', extra: 'info' }); + await db.insert({ value: 'beta', extra: 'details' }); + const result = await db.query({}, { sort: { value: 1 }, project: { value: 1 } }); + assert.strictEqual(result.length, 3); + assert.strictEqual(Object.keys(result[0]).length, 1); + assert.strictEqual(result[0].value, 'alpha'); + assert.strictEqual(Object.keys(result[1]).length, 1); + assert.strictEqual(result[1].value, 'beta'); + assert.strictEqual(Object.keys(result[2]).length, 1); + assert.strictEqual(result[2].value, 'gamma'); + await db.close(); +}); +