Skip to content

Commit

Permalink
feat: only string IDs are now supported
Browse files Browse the repository at this point in the history
Previously both string and number IDs were supported.
This is a step towards simplicity.
  • Loading branch information
kirillgroshkov committed Dec 28, 2023
1 parent 740a195 commit f86be0d
Show file tree
Hide file tree
Showing 7 changed files with 875 additions and 830 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
build-job:
runs-on: ubuntu-latest
steps:
- { uses: actions/checkout@v3, with: { persist-credentials: false } }
- { uses: actions/setup-node@v3, with: { node-version: 18, cache: 'yarn' } }
- { uses: actions/checkout@v4, with: { persist-credentials: false } }
- { uses: actions/setup-node@v4, with: { node-version: 20, cache: 'yarn' } }
- run: yarn --frozen-lockfile
- run: yarn build
- name: test
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jobs:
if: "!contains(github.event.head_commit.message, 'skip ci')"
env: { NODE_OPTIONS: '--max-old-space-size=3200' }
steps:
- { uses: actions/checkout@v3, with: { persist-credentials: true } }
- { uses: actions/setup-node@v3, with: { node-version: 18, cache: 'yarn' } }
- { uses: actions/checkout@v4, with: { persist-credentials: true } }
- { uses: actions/setup-node@v4, with: { node-version: 20, cache: 'yarn' } }

# Cache for npm/npx in ~/.npm
- uses: actions/cache@v3
Expand Down
25 changes: 7 additions & 18 deletions src/commondao/common.dao.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,26 @@ import {
import { CommonDB } from '../common.db'
import { CommonDBCreateOptions, CommonDBOptions, CommonDBSaveOptions } from '../db.model'

export interface CommonDaoHooks<
BM extends Partial<ObjectWithId<ID>>,
DBM extends ObjectWithId<ID>,
TM,
ID extends string | number,
> {
export interface CommonDaoHooks<BM extends Partial<ObjectWithId>, DBM extends ObjectWithId, TM> {
/**
* Allows to override the id generation function.
* By default it uses `stringId` from nodejs-lib
* (which uses lowercase alphanumberic alphabet and the size of 16).
*/
createRandomId: () => ID
createRandomId: () => string

/**
* createNaturalId hook is called (tried) first.
* If it doesn't exist - createRandomId is called.
*/
createNaturalId: (obj: DBM | BM) => ID
createNaturalId: (obj: DBM | BM) => string

/**
* It's a counter-part of `createNaturalId`.
* Allows to provide a parser function to parse "natural id" into
* DBM components (e.g accountId and some other property that is part of the id).
*/
parseNaturalId: (id: ID) => Partial<DBM>
parseNaturalId: (id: string) => Partial<DBM>

/**
* It is called only on `dao.create` method.
Expand Down Expand Up @@ -137,10 +132,9 @@ export enum CommonDaoLogLevel {
}

export interface CommonDaoCfg<
BM extends Partial<ObjectWithId<ID>>,
DBM extends ObjectWithId<ID> = Saved<BM>,
BM extends Partial<ObjectWithId>,
DBM extends ObjectWithId = Saved<BM>,
TM = BM,
ID extends string | number = string,
> {
db: CommonDB
table: string
Expand Down Expand Up @@ -187,12 +181,7 @@ export interface CommonDaoCfg<
logStarted?: boolean

// Hooks are designed with inspiration from got/ky interface
hooks?: Partial<CommonDaoHooks<BM, DBM, TM, ID>>

/**
* Defaults to 'string'
*/
idType?: 'string' | 'number'
hooks?: Partial<CommonDaoHooks<BM, DBM, TM>>

/**
* Defaults to true.
Expand Down
97 changes: 51 additions & 46 deletions src/commondao/common.dao.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,18 +78,16 @@ const isCI = !!process.env['CI']
* TM = Transport model (optimized to be sent over the wire)
*/
export class CommonDao<
BM extends Partial<ObjectWithId<ID>>,
DBM extends ObjectWithId<ID> = Saved<BM>,
BM extends Partial<ObjectWithId>,
DBM extends ObjectWithId = Saved<BM>,
TM extends AnyObject = BM,
ID extends string | number = NonNullable<BM['id']>,
> {
constructor(public cfg: CommonDaoCfg<BM, DBM, TM, ID>) {
constructor(public cfg: CommonDaoCfg<BM, DBM, TM>) {
this.cfg = {
// Default is to NOT log in AppEngine and in CI,
// otherwise to log Operations
// e.g in Dev (local machine), Test - it will log operations (useful for debugging)
logLevel: isGAE || isCI ? CommonDaoLogLevel.NONE : CommonDaoLogLevel.OPERATIONS,
idType: 'string',
createId: true,
assignGeneratedIds: false,
created: true,
Expand All @@ -106,16 +104,11 @@ export class CommonDao<
anonymize: dbm => dbm,
onValidationError: err => err,
...cfg.hooks,
} satisfies Partial<CommonDaoHooks<BM, DBM, TM, ID>>,
} satisfies Partial<CommonDaoHooks<BM, DBM, TM>>,
}

if (this.cfg.createId) {
_assert(
this.cfg.idType === 'string',
'db-lib: automatic generation of non-string ids is not supported',
)

this.cfg.hooks!.createRandomId ||= () => stringId() as ID
this.cfg.hooks!.createRandomId ||= () => stringId()
} else {
delete this.cfg.hooks!.createRandomId
}
Expand All @@ -131,8 +124,8 @@ export class CommonDao<

// GET
async getById(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
async getById(id?: ID | null, opt?: CommonDaoOptions): Promise<Saved<BM> | null>
async getById(id?: ID | null, opt: CommonDaoOptions = {}): Promise<Saved<BM> | null> {
async getById(id?: string | null, opt?: CommonDaoOptions): Promise<Saved<BM> | null>
async getById(id?: string | null, opt: CommonDaoOptions = {}): Promise<Saved<BM> | null> {
if (!id) return null
const op = `getById(${id})`
const table = opt.table || this.cfg.table
Expand All @@ -148,14 +141,22 @@ export class CommonDao<
return bm || null
}

async getByIdOrEmpty(id: ID, part: Partial<BM> = {}, opt?: CommonDaoOptions): Promise<Saved<BM>> {
async getByIdOrEmpty(
id: string,
part: Partial<BM> = {},
opt?: CommonDaoOptions,
): Promise<Saved<BM>> {
const bm = await this.getById(id, opt)
if (bm) return bm

return this.create({ ...part, id }, opt)
}

async getByIdAsDBMOrEmpty(id: ID, part: Partial<BM> = {}, opt?: CommonDaoOptions): Promise<DBM> {
async getByIdAsDBMOrEmpty(
id: string,
part: Partial<BM> = {},
opt?: CommonDaoOptions,
): Promise<DBM> {
const dbm = await this.getByIdAsDBM(id, opt)
if (dbm) return dbm

Expand All @@ -164,8 +165,8 @@ export class CommonDao<
}

async getByIdAsDBM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
async getByIdAsDBM(id?: ID | null, opt?: CommonDaoOptions): Promise<DBM | null>
async getByIdAsDBM(id?: ID | null, opt: CommonDaoOptions = {}): Promise<DBM | null> {
async getByIdAsDBM(id?: string | null, opt?: CommonDaoOptions): Promise<DBM | null>
async getByIdAsDBM(id?: string | null, opt: CommonDaoOptions = {}): Promise<DBM | null> {
if (!id) return null
const op = `getByIdAsDBM(${id})`
const table = opt.table || this.cfg.table
Expand All @@ -183,8 +184,8 @@ export class CommonDao<
}

async getByIdAsTM(id: undefined | null, opt?: CommonDaoOptions): Promise<null>
async getByIdAsTM(id?: ID | null, opt?: CommonDaoOptions): Promise<TM | null>
async getByIdAsTM(id?: ID | null, opt: CommonDaoOptions = {}): Promise<TM | null> {
async getByIdAsTM(id?: string | null, opt?: CommonDaoOptions): Promise<TM | null>
async getByIdAsTM(id?: string | null, opt: CommonDaoOptions = {}): Promise<TM | null> {
if (!id) return null
const op = `getByIdAsTM(${id})`
const table = opt.table || this.cfg.table
Expand All @@ -204,7 +205,7 @@ export class CommonDao<
return tm || null
}

async getByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<Saved<BM>[]> {
async getByIds(ids: string[], opt: CommonDaoOptions = {}): Promise<Saved<BM>[]> {
if (!ids.length) return []
const op = `getByIds ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
const table = opt.table || this.cfg.table
Expand All @@ -221,7 +222,7 @@ export class CommonDao<
return bms
}

async getByIdsAsDBM(ids: ID[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
async getByIdsAsDBM(ids: string[], opt: CommonDaoOptions = {}): Promise<DBM[]> {
if (!ids.length) return []
const op = `getByIdsAsDBM ${ids.length} id(s) (${_truncate(ids.slice(0, 10).join(', '), 50)})`
const table = opt.table || this.cfg.table
Expand All @@ -237,23 +238,23 @@ export class CommonDao<
return dbms
}

async requireById(id: ID, opt: CommonDaoOptions = {}): Promise<Saved<BM>> {
async requireById(id: string, opt: CommonDaoOptions = {}): Promise<Saved<BM>> {
const r = await this.getById(id, opt)
if (!r) {
this.throwRequiredError(id, opt)
}
return r
}

async requireByIdAsDBM(id: ID, opt: CommonDaoOptions = {}): Promise<DBM> {
async requireByIdAsDBM(id: string, opt: CommonDaoOptions = {}): Promise<DBM> {
const r = await this.getByIdAsDBM(id, opt)
if (!r) {
this.throwRequiredError(id, opt)
}
return r
}

private throwRequiredError(id: ID, opt: CommonDaoOptions): never {
private throwRequiredError(id: string, opt: CommonDaoOptions): never {
const table = opt.table || this.cfg.table
throw new AppError(`DB row required, but not found in ${table}`, {
table,
Expand Down Expand Up @@ -311,8 +312,8 @@ export class CommonDao<
/**
* Pass `table` to override table
*/
query(table?: string): RunnableDBQuery<BM, DBM, TM, ID> {
return new RunnableDBQuery<BM, DBM, TM, ID>(this, table)
query(table?: string): RunnableDBQuery<BM, DBM, TM> {
return new RunnableDBQuery<BM, DBM, TM>(this, table)
}

async runQuery(q: DBQuery<DBM>, opt?: CommonDaoOptions): Promise<Saved<BM>[]> {
Expand Down Expand Up @@ -614,21 +615,21 @@ export class CommonDao<
)
}

async queryIds(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<ID[]> {
async queryIds(q: DBQuery<DBM>, opt: CommonDaoOptions = {}): Promise<string[]> {
q.table = opt.table || q.table
const { rows } = await this.cfg.db.runQuery(q.select(['id']), opt)
return rows.map(r => r.id)
}

streamQueryIds(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<ID> = {}): ReadableTyped<ID> {
streamQueryIds(q: DBQuery<DBM>, opt: CommonDaoStreamOptions<string> = {}): ReadableTyped<string> {
q.table = opt.table || q.table
opt.errorMode ||= ErrorMode.SUPPRESS

const stream: ReadableTyped<ID> = this.cfg.db
const stream: ReadableTyped<string> = this.cfg.db
.streamQuery<DBM>(q.select(['id']), opt)
.on('error', err => stream.emit('error', err))
.pipe(
transformMapSimple<DBM, ID>(objectWithId => objectWithId.id, {
transformMapSimple<DBM, string>(objectWithId => objectWithId.id, {
errorMode: ErrorMode.SUPPRESS, // cause .pipe() cannot propagate errors
}),
)
Expand All @@ -638,8 +639,8 @@ export class CommonDao<

async streamQueryIdsForEach(
q: DBQuery<DBM>,
mapper: AsyncMapper<ID, void>,
opt: CommonDaoStreamForEachOptions<ID> = {},
mapper: AsyncMapper<string, void>,
opt: CommonDaoStreamForEachOptions<string> = {},
): Promise<void> {
q.table = opt.table || q.table
opt.errorMode ||= ErrorMode.SUPPRESS
Expand All @@ -650,11 +651,11 @@ export class CommonDao<

await _pipeline([
this.cfg.db.streamQuery<DBM>(q.select(['id']), opt),
transformMapSimple<DBM, ID>(objectWithId => {
transformMapSimple<DBM, string>(objectWithId => {
count++
return objectWithId.id
}),
transformMap<ID, void>(mapper, {
transformMap<string, void>(mapper, {
...opt,
predicate: _passthroughPredicate,
}),
Expand Down Expand Up @@ -734,26 +735,26 @@ export class CommonDao<
}
},
deleteByIds: async (
ids: ID[],
ids: string[],
opt: CommonDaoOptions = {},
): Promise<DBDeleteByIdsOperation | undefined> => {
if (!ids.length) return
return {
type: 'deleteByIds',
table: this.cfg.table,
ids: ids as string[],
ids,
opt,
}
},
deleteById: async (
id: ID | null | undefined,
id: string | null | undefined,
opt: CommonDaoOptions = {},
): Promise<DBDeleteByIdsOperation | undefined> => {
if (!id) return
return {
type: 'deleteByIds',
table: this.cfg.table,
ids: [id as string],
ids: [id],
opt,
}
},
Expand Down Expand Up @@ -848,7 +849,7 @@ export class CommonDao<
* 3. Saves (as fast as possible since the read) with the Patch applied, but only if the data has changed.
*/
async patchById(
id: ID,
id: string,
patch: Partial<BM>,
opt: CommonDaoSaveBatchOptions<DBM> = {},
): Promise<Saved<BM>> {
Expand Down Expand Up @@ -1109,8 +1110,8 @@ export class CommonDao<
* @returns number of deleted items
*/
async deleteById(id: undefined | null, opt?: CommonDaoOptions): Promise<0>
async deleteById(id?: ID | null, opt?: CommonDaoOptions): Promise<number>
async deleteById(id?: ID | null, opt: CommonDaoOptions = {}): Promise<number> {
async deleteById(id?: string | null, opt?: CommonDaoOptions): Promise<number>
async deleteById(id?: string | null, opt: CommonDaoOptions = {}): Promise<number> {
if (!id) return 0
this.requireWriteAccess()
this.requireObjectMutability(opt)
Expand All @@ -1122,7 +1123,7 @@ export class CommonDao<
return count
}

async deleteByIds(ids: ID[], opt: CommonDaoOptions = {}): Promise<number> {
async deleteByIds(ids: string[], opt: CommonDaoOptions = {}): Promise<number> {
if (!ids.length) return 0
this.requireWriteAccess()
this.requireObjectMutability(opt)
Expand Down Expand Up @@ -1155,7 +1156,7 @@ export class CommonDao<

await _pipeline([
this.cfg.db.streamQuery<DBM>(q.select(['id']), opt),
transformMapSimple<DBM, ID>(objectWithId => objectWithId.id, {
transformMapSimple<DBM, string>(objectWithId => objectWithId.id, {
errorMode: ErrorMode.SUPPRESS,
}),
transformBuffer<string>({ batchSize }),
Expand Down Expand Up @@ -1188,11 +1189,15 @@ export class CommonDao<
return deleted
}

async updateById(id: ID, patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
async updateById(id: string, patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
return await this.updateByQuery(this.query().filterEq('id', id), patch, opt)
}

async updateByIds(ids: ID[], patch: DBPatch<DBM>, opt: CommonDaoOptions = {}): Promise<number> {
async updateByIds(
ids: string[],
patch: DBPatch<DBM>,
opt: CommonDaoOptions = {},
): Promise<number> {
if (!ids.length) return 0
return await this.updateByQuery(this.query().filterIn('id', ids), patch, opt)
}
Expand Down
4 changes: 2 additions & 2 deletions src/model.util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ export function createdUpdatedFields(
}

export function createdUpdatedIdFields(
existingObject?: Partial<CreatedUpdatedId<string>> | null,
): CreatedUpdatedId<string> {
existingObject?: Partial<CreatedUpdatedId> | null,
): CreatedUpdatedId {
const now = Math.floor(Date.now() / 1000)
return {
created: existingObject?.created || now,
Expand Down
Loading

0 comments on commit f86be0d

Please sign in to comment.