Skip to main content
BaseModelClass is the base class that all Models inherit from, providing core functionality for database access, query execution, and transaction management.

Database Access Methods

getDB(preset)

Returns a Knex database connection.
getDB(preset: DBPreset): Knex
Parameters:
  • preset: "r" (read) or "w" (write)
Usage examples:
async findCustomQuery(): Promise<User[]> {
  const rdb = this.getDB("r");
  
  return rdb("users")
    .select("*")
    .where("is_active", true);
}
DBPreset types:
  • "r": Read - Read-only (SELECT)
  • "w": Write - Writable (INSERT, UPDATE, DELETE)
Separating read/write allows distributing read load in database replication.

getPuri(preset)

Returns a Puri query builder. Automatically uses transaction connection when a transaction is active.
getPuri(preset: DBPreset): PuriWrapper
Parameters:
  • preset: "r" (read) or "w" (write)
Usage examples:
async findActive(): Promise<User[]> {
  const rdb = this.getPuri("r");
  
  return rdb
    .table("users")
    .where("is_active", true)
    .orderBy("created_at", "desc")
    .many();
}
getPuri() automatically detects transaction context. When called within @transactional() decorator, it returns the transaction connection.

Subset Query Methods

getSubsetQueries(subset)

Returns a query builder for a specific Subset.
getSubsetQueries<T extends TSubsetKey>(
  subset: T
): {
  qb: Puri;
  onSubset: <S>(subset: S) => Puri;
}
Return values:
  • qb: Query builder (for adding conditions)
  • onSubset: Subset-specific type casting function
Usage examples:
async findMany<T extends UserSubsetKey>(
  subset: T,
  params: UserListParams
): Promise<ListResult<UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);
  
  // Add conditions
  if (params.keyword) {
    qb.whereLike("users.email", `%${params.keyword}%`);
  }
  
  // Execute
  return this.executeSubsetQuery({ subset, qb, params });
}
onSubset() is for type checking. It actually returns the same qb object, so it doesn’t affect performance.
// These two codes are identical
qb.where("users.id", 1);
onSubset("A").where("users.id", 1);

executeSubsetQuery(params)

Executes a Subset query and returns results. Automatically handles pagination, Loader, Hydration, and Enhancer.
executeSubsetQuery<T extends TSubsetKey>(
  params: {
    subset: T;
    qb: Puri;
    params: {
      num: number;
      page: number;
      queryMode?: "list" | "count" | "both";
    };
    enhancers?: EnhancerMap;
    debug?: boolean;
    optimizeCountQuery?: boolean;
  }
): Promise<ListResult<TSubsetMapping[T]>>
Parameters:
ParameterTypeDescriptionDefault
subsetstringSubset keyrequired
qbPuriQuery builderrequired
params.numnumberPage sizerequired
params.pagenumberPage number (starts from 1)required
params.queryModestringQuery mode"both"
enhancersobjectVirtual field calculation functions-
debugbooleanQuery debugging outputfalse
optimizeCountQuerybooleanCOUNT query optimizationfalse
queryMode options:
ModeReturn ValueUse Case
"both"{ rows, total }Normal list query (default)
"list"{ rows }When total is not needed
"count"{ total }When only count is needed
Usage examples:
async findMany<T extends UserSubsetKey>(
  subset: T,
  params: UserListParams
): Promise<ListResult<UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);
  
  if (params.keyword) {
    qb.whereLike("users.email", `%${params.keyword}%`);
  }
  
  const enhancers = this.createEnhancers({
    A: (row) => row,
    SS: (row) => row,
  });
  
  return this.executeSubsetQuery({
    subset,
    qb,
    params: {
      num: params.num ?? 24,
      page: params.page ?? 1,
    },
    enhancers,
  });
}
Execution order:
  1. Execute COUNT query (calculate total)
  2. Execute LIST query (apply pagination)
  3. Execute Loader (load HasMany, ManyToMany data)
  4. Hydrate (flat object → nested object conversion)
  5. Apply Enhancer (calculate virtual fields)
  6. Remove Internal fields

createEnhancers(enhancers)

Helper function to create Enhancer objects. Provides type validation and inference.
createEnhancers<T extends TSubsetKey>(
  enhancers: EnhancerMap<T>
): EnhancerMap<T>
What is an Enhancer? An Enhancer is a function that adds virtual fields to query results or transforms data. Usage examples:
const enhancers = this.createEnhancers({
  A: (row) => ({
    ...row,
    full_name: `${row.first_name} ${row.last_name}`,
    age: calculateAge(row.birth_date),
  }),
  SS: (row) => row,  // No transformation
});

await this.executeSubsetQuery({
  subset: "A",
  qb,
  params,
  enhancers,
});
Enhancers are called for each row, so avoid heavy operations. If needed, use Loaders or separate APIs.

Utility Methods

getInsertedIds(wdb, rows, tableName, unqKeyFields, chunkSize)

Retrieves IDs of inserted records. Useful after upsert since it queries based on Unique keys.
getInsertedIds(
  wdb: Knex,
  rows: Record<string, unknown>[],
  tableName: string,
  unqKeyFields: string[],
  chunkSize?: number
): Promise<number[]>
Parameters:
ParameterTypeDescriptionDefault
wdbKnexDatabase connection-
rowsobject[]Inserted records-
tableNamestringTable name-
unqKeyFieldsstring[]Unique key field names-
chunkSizenumberNumber to query at once500
Usage examples:
async saveUsers(users: User[]): Promise<number[]> {
  const wdb = this.getDB("w");
  
  // Upsert by email
  await wdb("users")
    .insert(users)
    .onConflict("email")
    .merge();
  
  // Query IDs by email
  const ids = await this.getInsertedIds(
    wdb,
    users,
    "users",
    ["email"]
  );
  
  return ids;
}

hydrate(rows)

Converts flat records to nested objects. Transforms table__field format from JOIN results to objects.
hydrate<T>(rows: T[]): T[]
Conversion rules:
  • user__name{ user: { name } }
  • user__profile__bio{ user: { profile: { bio } } }
  • If nullable relation’s id is null, the entire object becomes null
Usage examples:
// Flat data
const flatRows = [
  {
    id: 1,
    name: "John",
    user__id: 10,
    user__email: "john@test.com",
    user__profile__bio: "Hello",
  }
];

// Convert to nested object
const nested = this.hydrate(flatRows);
// [
//   {
//     id: 1,
//     name: "John",
//     user: {
//       id: 10,
//       email: "john@test.com",
//       profile: {
//         bio: "Hello"
//       }
//     }
//   }
// ]
hydrate() is automatically called inside executeSubsetQuery(), so you rarely need to call it directly.

omitInternalFields(row, fields)

Removes Internal fields from an object. Also handles nested fields and arrays.
omitInternalFields<T extends object>(
  row: T,
  fields: string[]
): T
Usage example:
const row = {
  id: 1,
  email: "test@test.com",
  password: "hashed_password",
  employee: {
    id: 10,
    salary: 50000,
    department: { name: "IT" }
  }
};

// Remove Internal fields
const cleaned = this.omitInternalFields(row, [
  "password",
  "employee.salary"
]);

// {
//   id: 1,
//   email: "test@test.com",
//   employee: {
//     id: 10,
//     department: { name: "IT" }
//   }
// }
Fields specified with the internal option in Subset are automatically removed in executeSubsetQuery().
{
  "subsets": {
    "A": [
      "id",
      "email",
      { "field": "password", "internal": true },
      { "field": "employee.salary", "internal": true }
    ]
  }
}

destroy()

Closes database connections. Usually called when the application terminates.
async destroy(): Promise<void>
Usage example:
// When application terminates
process.on("SIGTERM", async () => {
  await UserModel.destroy();
  process.exit(0);
});
Calling destroy() closes DB connections for all Models. Use only in test environments.

Practical Usage Patterns

Standard findMany Pattern

async findMany<T extends UserSubsetKey, LP extends UserListParams>(
  subset: T,
  rawParams?: LP
): Promise<ListResult<LP, UserSubsetMapping[T]>> {
  // 1. Set defaults
  const params = {
    num: 24,
    page: 1,
    search: "id" as const,
    orderBy: "id-desc" as const,
    ...rawParams,
  };

  // 2. Get Subset query
  const { qb, onSubset } = this.getSubsetQueries(subset);

  // 3. Add filtering conditions
  if (params.id) {
    qb.whereIn("users.id", asArray(params.id));
  }

  if (params.keyword && params.search) {
    if (params.search === "email") {
      qb.whereLike("users.email", `%${params.keyword}%`);
    } else if (params.search === "username") {
      qb.whereLike("users.username", `%${params.keyword}%`);
    }
  }

  // 4. Sorting
  if (params.orderBy === "id-desc") {
    qb.orderBy("users.id", "desc");
  } else if (params.orderBy === "created_at-desc") {
    qb.orderBy("users.created_at", "desc");
  }

  // 5. Define Enhancers
  const enhancers = this.createEnhancers({
    A: (row) => ({
      ...row,
      full_name: `${row.first_name} ${row.last_name}`,
    }),
    SS: (row) => row,
  });

  // 6. Execute query
  return this.executeSubsetQuery({
    subset,
    qb,
    params,
    enhancers,
  });
}

Complex Filtering Pattern

async findMany<T extends UserSubsetKey>(
  subset: T,
  params: UserListParams
): Promise<ListResult<UserSubsetMapping[T]>> {
  const { qb } = this.getSubsetQueries(subset);

  // Date range filter
  if (params.created_after || params.created_before) {
    if (params.created_after) {
      qb.where("users.created_at", ">=", params.created_after);
    }
    if (params.created_before) {
      qb.where("users.created_at", "<=", params.created_before);
    }
  }

  // Multi-value filter
  if (params.roles && params.roles.length > 0) {
    qb.whereIn("users.role", params.roles);
  }

  // OR conditions
  if (params.keyword) {
    qb.where((builder) => {
      builder
        .whereLike("users.email", `%${params.keyword}%`)
        .orWhereLike("users.username", `%${params.keyword}%`)
        .orWhereLike("users.phone", `%${params.keyword}%`);
    });
  }

  // Complex conditions
  if (params.is_premium_or_admin) {
    qb.where((builder) => {
      builder
        .where("users.role", "admin")
        .orWhere("users.subscription_level", "premium");
    });
  }

  return this.executeSubsetQuery({
    subset,
    qb,
    params: { num: params.num ?? 24, page: params.page ?? 1 },
  });
}

Custom Aggregation Query

async getStatistics(): Promise<UserStatistics> {
  const rdb = this.getDB("r");

  const [stats] = await rdb("users")
    .select([
      rdb.raw("COUNT(*) as total_users"),
      rdb.raw("COUNT(CASE WHEN is_active THEN 1 END) as active_users"),
      rdb.raw("COUNT(CASE WHEN role = 'admin' THEN 1 END) as admin_count"),
      rdb.raw("AVG(age) as average_age"),
    ])
    .first();

  return {
    total_users: Number(stats.total_users),
    active_users: Number(stats.active_users),
    admin_count: Number(stats.admin_count),
    average_age: Number(stats.average_age),
  };
}

Next Steps