npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@open-kingdom/crm-backend-data-access-contacts

v0.0.2-17

Published

A NestJS module providing persistence and REST endpoints for the two foundational CRM entities — **companies** and **contacts**. Both entities are owner-scoped (each row carries an `ownerId` foreign key into `users`), support soft-deletion via `isArchived

Downloads

76

Readme

@open-kingdom/crm-backend-data-access-contacts

A NestJS module providing persistence and REST endpoints for the two foundational CRM entities — companies and contacts. Both entities are owner-scoped (each row carries an ownerId foreign key into users), support soft-deletion via isArchived, and are guarded by RBAC permissions on the companies and contacts resources.

This is a data-access library: it owns the Drizzle table definitions, the services, and the REST controllers, but performs no cross-entity orchestration. Higher-level workflows (lead conversion, opportunity pipeline) live in @open-kingdom/crm-backend-feature-crm.


Exports

| Export | Kind | Description | | --------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | | DataAccessContactsModule | class | Standard NestJS module. Registers CompaniesController and ContactsController and exports CompaniesService + ContactsService. | | CompaniesService | class | Injectable service for company CRUD, search, archive/restore, and ownership checks. | | ContactsService | class | Injectable service for contact CRUD and search. | | CompaniesController | class | REST controller mounted at /companies. | | ContactsController | class | REST controller mounted at /contacts. | | companies | BetterSQLite3Table | Drizzle table definition for the companies table. | | contacts | BetterSQLite3Table | Drizzle table definition for the contacts table. | | CompaniesTableName | 'companies' | String constant for use in typed schema composition. | | ContactsTableName | 'contacts' | String constant for use in typed schema composition. | | Company / NewCompany | type | Inferred $inferSelect / $inferInsert types. | | Contact / NewContact | type | Inferred $inferSelect / $inferInsert types. | | CompanyFilter | interface | Filter object accepted by CompaniesService.findAll(). | | ContactFilter | interface | Filter object accepted by ContactsService.findAll(). | | CompanyDto / ContactDto | class | Swagger response DTOs. | | CreateCompanyDto / UpdateCompanyDto | class | Swagger request DTOs for companies. | | CreateContactDto / UpdateContactDto | class | Swagger request DTOs for contacts. |


Drizzle Schemas

companies Table

| Column | Type | Constraints | Description | | --------------- | ------------------- | ---------------------------- | ----------------------------------------------------------------- | | id | integer | Primary key, auto-increment | | | name | text | Not null | Company name. | | website | text | Nullable | | | primary_phone | text | Nullable | | | industry | text | Nullable | Free-form; suggested values come from the industry lookup list. | | status | text | Not null, default 'active' | Suggested values come from the company_status lookup list. | | location | text | Nullable | | | company_size | text | Nullable | | | revenue_range | text | Nullable | | | notes_summary | text | Nullable | Short summary surfaced in list views. | | owner_id | integer | Not null, FK → users.id | Owner user. | | is_archived | integer | Not null, default 0 | Soft-delete flag (0/1). | | created_at | integer timestamp | Not null, default now | | | updated_at | integer timestamp | Not null, default now | Bumped on every update. |

Indexes: companies_owner_idx (owner_id), companies_name_idx (name), companies_status_idx (status).

contacts Table

| Column | Type | Constraints | Description | | ----------------- | ------------------- | ----------------------------- | ----------------------------------- | | id | integer | Primary key, auto-increment | | | first_name | text | Not null | | | last_name | text | Not null | | | email | text | Nullable | Primary email (not unique). | | phone | text | Nullable | | | secondary_phone | text | Nullable | | | secondary_email | text | Nullable | | | job_title | text | Nullable | | | company_id | integer | Nullable, FK → companies.id | Optional employing company. | | lead_source | text | Nullable | From the lead_source lookup list. | | tags | text | Nullable | Free-form, comma-separated. | | mailing_address | text | Nullable | | | notes_summary | text | Nullable | | | status | text | Not null, default 'active' | | | owner_id | integer | Not null, FK → users.id | Owner user. | | is_archived | integer | Not null, default 0 | Soft-delete flag. | | created_at | integer timestamp | Not null, default now | | | updated_at | integer timestamp | Not null, default now | |

Indexes: contacts_owner_idx (owner_id), contacts_company_idx (company_id), contacts_email_idx (email), contacts_last_first_idx (last_name, first_name).


Module Registration

DataAccessContactsModule is a standard module — no forRoot(). It assumes the database is already wired via DatabaseSetupModule.register() in the root composition module, and that the companies and contacts tables are present in the registered schema object. App-level wiring is expected to live in demo-scaffold-backend-feature-root-schema (or its equivalent).

import { DataAccessContactsModule } from '@open-kingdom/crm-backend-data-access-contacts';

@Module({
  imports: [DataAccessContactsModule],
})
export class SomeFeatureModule {}

CompaniesService and ContactsService are exported and can be injected by any module that imports DataAccessContactsModule.

Schema composition

Add the tables to the root schema module:

import { companies, contacts, CompaniesTableName, ContactsTableName } from '@open-kingdom/crm-backend-data-access-contacts';

const schema = {
  // …
  [CompaniesTableName]: companies,
  [ContactsTableName]: contacts,
};

DatabaseSetupModule.register({ schema, filename: 'app.db' });

Configuration

No module-level configuration. The services resolve their database via the global DB_TAG token.


CompaniesService API

constructor(private companies: CompaniesService) {}

| Method | Parameters | Returns | Description | | -------------------- | ------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | findAll | filter?: CompanyFilter | Promise<Company[]> | Lists companies, ordered by name. Excludes archived rows unless filter.includeArchived is true. | | findById | id: number | Promise<Company \| undefined> | Lookup by primary key. | | create | input: CreateCompanyDto, defaultOwnerId: number | Promise<Company> | Inserts. ownerId defaults to defaultOwnerId (typically the authenticated user); status defaults to 'active'. | | update | id: number, input: UpdateCompanyDto | Promise<Company> | Patches the row, ignoring undefined fields. Bumps updatedAt. Throws NotFoundException if missing. | | archive | id: number | Promise<Company> | Sets isArchived = 1. | | restore | id: number | Promise<Company> | Sets isArchived = 0. | | assertOwnerOrAdmin | id: number, userId: number, isAdmin: boolean | Promise<Company> | Throws NotFoundException if the company doesn't exist, ForbiddenException if userId isn't the owner and isAdmin is false. |

CompanyFilter shape: { ownerId?, status?, search?, includeArchived? }. search runs a LIKE %search% across name, website, and industry.


ContactsService API

constructor(private contacts: ContactsService) {}

| Method | Parameters | Returns | Description | | ---------- | ------------------------------------------------- | ------------------------------- | ----------------------------------------------------------------------------------------------------------- | | findAll | filter?: ContactFilter | Promise<Contact[]> | Lists contacts, ordered by lastName, firstName. Excludes archived rows unless filter.includeArchived. | | findById | id: number | Promise<Contact \| undefined> | Lookup by primary key. | | create | input: CreateContactDto, defaultOwnerId: number | Promise<Contact> | Inserts. ownerId defaults to defaultOwnerId; status defaults to 'active'. | | update | id: number, input: UpdateContactDto | Promise<Contact> | Patches, ignoring undefined fields. Bumps updatedAt. | | archive | id: number | Promise<Contact> | Soft-delete. | | restore | id: number | Promise<Contact> | Un-archive. |

ContactFilter shape: { ownerId?, companyId?, status?, search?, includeArchived? }. search runs a LIKE %search% across firstName, lastName, email, and phone.


REST Endpoints

All endpoints require authentication (@ApiBearerAuth('JWT-auth')) and an RBAC permission via @RequirePermission (@open-kingdom/shared-backend-util-rbac).

Companies — /companies

| Method | Path | Permission | Description | | ------- | ------------------------ | ------------------ | ------------------------------------------------------------------------- | | GET | /companies | companies:read | List, with ownerId, status, search, includeArchived query params. | | GET | /companies/:id | companies:read | Get one. | | POST | /companies | companies:create | Create. Owner defaults to authenticated user. | | PATCH | /companies/:id | companies:update | Patch. | | POST | /companies/:id/archive | companies:update | Soft-delete. | | POST | /companies/:id/restore | companies:update | Un-archive. |

Contacts — /contacts

| Method | Path | Permission | Description | | ------- | ----------------------- | ----------------- | -------------------------------------------------------------------------------------- | | GET | /contacts | contacts:read | List, with ownerId, companyId, status, search, includeArchived query params. | | GET | /contacts/:id | contacts:read | Get one. | | POST | /contacts | contacts:create | Create. Owner defaults to authenticated user. | | PATCH | /contacts/:id | contacts:update | Patch. | | POST | /contacts/:id/archive | contacts:update | Soft-delete. | | POST | /contacts/:id/restore | contacts:update | Un-archive. |


Usage Example

import { Injectable } from '@nestjs/common';
import { CompaniesService, ContactsService } from '@open-kingdom/crm-backend-data-access-contacts';

@Injectable()
export class AccountSummaryService {
  constructor(private readonly companies: CompaniesService, private readonly contacts: ContactsService) {}

  async summarize(companyId: number) {
    const company = await this.companies.findById(companyId);
    if (!company) return null;
    const employees = await this.contacts.findAll({ companyId });
    return { company, contactCount: employees.length };
  }
}

Testing

nx test crm-backend-data-access-contacts