@open-kingdom/shared-backend-data-access-configurable-lookups
v0.0.2-17
Published
A NestJS module providing a generic, admin-editable reference-data table — every dropdown / picklist option in the application stored as `(list_key, value, label, sort_order, is_active, is_system)` rows. Domain code reads `findByListKey('opportunity_stage
Readme
@open-kingdom/shared-backend-data-access-configurable-lookups
A NestJS module providing a generic, admin-editable reference-data table — every dropdown / picklist option in the application stored as (list_key, value, label, sort_order, is_active, is_system) rows. Domain code reads findByListKey('opportunity_stage') to render a dropdown; admins POST new entries through the REST API. Rows seeded by code can be marked is_system = 1 to lock their list_key/value against renaming or deactivation.
This library is domain-agnostic: it doesn't know about CRM, billing, or any other vertical. Any feature that needs configurable enums uses it.
Exports
| Export | Kind | Description |
| ------------------------------------------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------- |
| DataAccessConfigurableLookupsModule | class | Standard NestJS module. Registers ConfigurableLookupsController and exports ConfigurableLookupsService. |
| ConfigurableLookupsService | class | Injectable service with read, write, and seed methods. |
| ConfigurableLookupsController | class | REST controller mounted at /configurable-lookups. |
| configurableLookups | BetterSQLite3Table | Drizzle table. |
| ConfigurableLookupsTableName | 'configurable_lookups' | String constant. |
| ConfigurableLookup / NewConfigurableLookup | type | Inferred select/insert types. |
| ConfigurableLookupDto, CreateConfigurableLookupDto, UpdateConfigurableLookupDto | class | Swagger DTOs. |
Drizzle Schema
configurable_lookups Table
| Column | Type | Constraints | Description |
| ------------ | ------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | integer | Primary key, auto-increment | |
| list_key | text | Not null | Snake-case identifier for the list (e.g. 'opportunity_stage'). Conventionally a member of LOOKUP_LIST_KEYS from crm-poly-util-domain when used by the CRM. |
| value | text | Not null | The canonical machine value stored in domain rows (e.g. 'discovery'). |
| label | text | Not null | The human-readable label rendered in dropdowns (e.g. 'Discovery'). |
| sort_order | integer | Not null, default 0 | Display order within a list. Lower numbers come first. |
| is_system | integer | Not null, default 0 | When 1, the row was inserted by code (e.g. via seedDefaults). System rows cannot be renamed, deleted, or deactivated. |
| is_active | integer | Not null, default 1 | Soft-toggle. Inactive rows are excluded from findByListKey() unless includeInactive is set. |
| created_at | integer timestamp | Not null, default now | |
| updated_at | integer timestamp | Not null, default now | Bumped on update. |
Unique index: configurable_lookups_list_value_uq on (list_key, value) — no two rows in the same list can share a value.
Module Registration
DataAccessConfigurableLookupsModule is a standard module. Import it where you want the service available, and add the table to the root schema composition.
import { DataAccessConfigurableLookupsModule } from '@open-kingdom/shared-backend-data-access-configurable-lookups';
@Module({
imports: [DataAccessConfigurableLookupsModule],
})
export class SomeFeatureModule {}import { configurableLookups } from '@open-kingdom/shared-backend-data-access-configurable-lookups';
const schema = {
// …
configurableLookups,
};
DatabaseSetupModule.register({ schema, filename: 'app.db' });Configuration
No module-level configuration. Resolves its database via DB_TAG.
ConfigurableLookupsService API
constructor(private lookups: ConfigurableLookupsService) {}| Method | Parameters | Returns | Description |
| -------------------- | ------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| findByListKey | listKey: string, opts?: { includeInactive?: boolean } | Promise<ConfigurableLookup[]> | Lists entries for one list, ordered by sortOrder then id. Filters out isActive === 0 rows unless includeInactive is set. |
| findAll | — | Promise<ConfigurableLookup[]> | Lists all entries, ordered by listKey, sortOrder, id. |
| findById | id: number | Promise<ConfigurableLookup \| undefined> | Lookup by primary key. |
| findByListAndValue | listKey: string, value: string | Promise<ConfigurableLookup \| undefined> | Resolve a (list_key, value) pair to a row. |
| create | input: CreateConfigurableLookupDto | Promise<ConfigurableLookup> | Inserts as is_system = 0. Throws ConflictException if (listKey, value) already exists. |
| update | id: number, input: UpdateConfigurableLookupDto | Promise<ConfigurableLookup> | Patches the row. System-row rules: changing listKey or value is forbidden, and isActive: false is forbidden — only label and sortOrder are editable. Throws ConflictException if the resulting (listKey, value) pair clashes with another row. |
| delete | id: number | Promise<void> | Hard-deletes a non-system row. Throws ForbiddenException if is_system = 1 (deactivate non-system rows instead). |
| seedDefaults | defaults: { listKey, value, label, sortOrder? }[] | Promise<void> | Idempotent insert-if-missing. Inserted rows are tagged is_system = 1. Used during application bootstrap to populate canonical defaults. |
REST Endpoints
All endpoints require authentication and an RBAC permission on the lookups resource.
| Method | Path | Permission | Description |
| -------- | --------------------------- | ---------------- | --------------------------------------------------------------------------------------------------- |
| GET | /configurable-lookups | lookups:read | List entries. Optional query: listKey (filter to one list), includeInactive ('true' / '1'). |
| GET | /configurable-lookups/:id | lookups:read | Get one. |
| POST | /configurable-lookups | lookups:create | Create. Inserted as a non-system row. |
| PATCH | /configurable-lookups/:id | lookups:update | Patch. System-row restrictions apply. |
| DELETE | /configurable-lookups/:id | lookups:delete | Delete (non-system rows only). Returns 204 No Content. |
Seeding Defaults from a Feature Module
The recommended pattern is for each feature module to declare its own default lookup entries and seed them on startup:
import { Injectable, OnApplicationBootstrap } from '@nestjs/common';
import { ConfigurableLookupsService } from '@open-kingdom/shared-backend-data-access-configurable-lookups';
const DEFAULTS = [
{ listKey: 'opportunity_stage', value: 'new', label: 'New', sortOrder: 10 },
{ listKey: 'opportunity_stage', value: 'discovery', label: 'Discovery', sortOrder: 20 },
{ listKey: 'opportunity_stage', value: 'proposal', label: 'Proposal', sortOrder: 30 },
{ listKey: 'opportunity_stage', value: 'negotiation', label: 'Negotiation', sortOrder: 40 },
{ listKey: 'opportunity_stage', value: 'won', label: 'Won', sortOrder: 50 },
{ listKey: 'opportunity_stage', value: 'lost', label: 'Lost', sortOrder: 60 },
];
@Injectable()
export class CrmSeedService implements OnApplicationBootstrap {
constructor(private readonly lookups: ConfigurableLookupsService) {}
async onApplicationBootstrap() {
await this.lookups.seedDefaults(DEFAULTS);
}
}The CRM ships its own default seed in @open-kingdom/crm-backend-feature-crm (CrmSeedService).
Testing
nx test data-access-configurable-lookups