npm package: @qezor/userstore
Description: Owner-scoped app data storage, tag indexing, and search helpers built for DynamoDB-style stores.
Author: Abdul Ahad
Latest version: 1.0.4

# @qezor/userstore

`@qezor/userstore` is for owner-scoped app data that is:
- not identity
- not profile
- not accounting

It is useful for:
- user file refs
- temporary presigned URL records
- activity snapshots
- preferences and interests
- recommendation or algorithm signals
- app-specific user data
- records that belong to a user inside a wider team/workspace/org scope

It also supports tag-based discovery helpers like:
- users interested in `tech`
- users interested in both `tech` and `dynamodb`
- listing all file records for a user
- listing all owner refs inside a workspace or group scope
- owner-scoped pagination with opaque cursors
- status-group counts over owner and scope records
- multi-scope membership-aware filtering

## Installation

```bash
npm install @qezor/userstore
```

## Precise Imports

```js
const { ownerPk, recordSk, tagIndexPk, scopePk } = require("@qezor/userstore/keys")
const { encodeCursor, decodeCursor } = require("@qezor/userstore/cursor")
const { normalizeMemberships, hasMembership } = require("@qezor/userstore/memberships")
const { groupStatusCounts } = require("@qezor/userstore/status")
const { parseRecordSk, parseTagIndexSk, parseScopeIndexSk } = require("@qezor/userstore/keys")
const { normalizeTags } = require("@qezor/userstore/tags")
const { createOwnerRecord, createTagIndexRecords, createScopeIndexRecords } = require("@qezor/userstore/models")
const { createOwnerStoreRepository } = require("@qezor/userstore/store")
const { createOwnerSearchRepository } = require("@qezor/userstore/search")
```

## Record Example

```js
const record = createOwnerRecord({
  ownerType: "user",
  ownerId: "u_123",
  scopeType: "workspace",
  scopeId: "workspace_9",
  memberships: [
    { scopeType: "org", scopeId: "org_7" },
    { scopeType: "project", scopeId: "project_2" },
  ],
  namespace: "drive",
  kind: "file_ref",
  recordId: "file_1",
  createdAt: 1712345678,
  updatedAt: 1712345678,
  status: "active",
  refs: {
    fileId: "file_1",
    objectKey: "avatars/u_123/a.png",
  },
  tags: ["files", "image"],
})
```

## Search Example

```js
const owners = await search.findOwnersByAllTags("feed", ["tech", "dynamodb"])
```

## Query Recipes

```js
await store.listRecordsByOwner("user", "u_123", {
  namespace: "drive",
  kind: "file_ref",
  status: "active",
  activeAt: Math.floor(Date.now() / 1000),
})

const page1 = await store.listRecordsByOwnerPage("user", "u_123", {
  namespace: "drive",
  kind: "file_ref",
  limit: 25,
})

const page2 = await store.listRecordsByOwnerPage("user", "u_123", {
  namespace: "drive",
  kind: "file_ref",
  limit: 25,
  cursor: page1.nextCursor,
})

await store.listScopeRefs("workspace", "workspace_9", {
  ownerType: "user",
  kind: "file_ref",
  status: "active",
})

await store.listScopeRefsAcrossScopes([
  { scopeType: "workspace", scopeId: "workspace_9" },
  { scopeType: "project", scopeId: "project_2" },
], {
  ownerType: "user",
  status: "active",
})

await store.countRecordsByStatus("user", "u_123", {
  namespace: "drive",
  kind: "file_ref",
  pageLimit: 100,
  maxPages: 25,
})

await search.findOwnerRecordsByAllTags("user", "u_123", "feed", ["tech", "dynamodb"], {
  kind: "interest",
  status: "active",
  scopeType: "workspace",
  scopeId: "workspace_9",
})

await search.findOwnersByAllTagsInScopes(
  "feed",
  ["tech", "dynamodb"],
  [
    { scopeType: "workspace", scopeId: "workspace_9" },
    { scopeType: "project", scopeId: "project_2" },
  ],
  { kind: "interest", status: "active" },
)

await search.countRecordsByStatusForAllTags("feed", ["tech", "dynamodb"], {
  scopesAny: [{ scopeType: "workspace", scopeId: "workspace_9" }],
})
```

## Notes

- this package is for owner data and owner search
- billing, credits, invoices, and quota economics belong in `@qezor/ledgerkit`
- identity and profile belong in service-specific packages
- key parsing helpers are included so projected DynamoDB items can still be interpreted safely
- `scopeType` and `scopeId` let records belong to an owner inside a broader container like a workspace, org, team, or project
- `memberships` let one record participate in multiple scopes without losing direct owner identity
- page methods return opaque `nextCursor` values so consumers do not need DynamoDB key knowledge
- count methods are explicit recipes for dashboards and quotas, and expose `truncated`/`nextCursor` when page caps are reached