@stacksjs/orm 0.70.44 → 0.70.53

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import type { InferModelAttributes, ModelAttributes, ModelDefinition } from 'bun-query-builder';
1
+ import type { AttributeKeys, ColumnName, FillableKeys, InferModelAttributes, ModelAttributes, ModelDefinition } from '@stacksjs/query-builder';
2
2
  /**
3
3
  * Extract the raw ModelDefinition from a defineModel() return value.
4
4
  * Uses the getDefinition() accessor that defineModel() provides.
@@ -8,6 +8,7 @@ export type Def<T> = T extends { getDefinition: () => infer D extends ModelDefin
8
8
  * Extract foreign key columns from belongsTo relations.
9
9
  * e.g., belongsTo: ['Customer', 'Coupon'] → { customer_id: number, coupon_id: number }
10
10
  */
11
+ // eslint-disable-next-line ts/no-empty-object-type
11
12
  export type BelongsToForeignKeys<TDef> = TDef extends { readonly belongsTo: readonly (infer R extends string)[] }
12
13
  ? { [K in R as `${Lowercase<K>}_id`]: number }
13
14
  : {}
@@ -17,24 +18,72 @@ export type BelongsToForeignKeys<TDef> = TDef extends { readonly belongsTo: read
17
18
  * @example
18
19
  * import type { ModelRow } from '@stacksjs/orm'
19
20
  * import type Post from '../models/Post'
20
- * type PostJsonResponse = ModelRow<Post>
21
+ * type PostJsonResponse = ModelRow<typeof Post>
21
22
  */
22
23
  export type ModelRow<T> = ModelAttributes<Def<T>> & BelongsToForeignKeys<Def<T>>;
24
+ /**
25
+ * Same as {@link ModelRow} but with every field optional. Useful for
26
+ * partial-projection reads (`select('id', 'name')`) and test fixtures
27
+ * that don't bother populating every column.
28
+ */
29
+ export type ModelRowLoose<T> = Partial<ModelRow<T>>;
23
30
  /**
24
31
  * Insertable data type: model attributes + FK columns, all optional.
25
32
  *
26
33
  * @example
27
34
  * import type { NewModelData } from '@stacksjs/orm'
28
35
  * import type Post from '../models/Post'
29
- * type NewPost = NewModelData<Post>
36
+ * type NewPost = NewModelData<typeof Post>
30
37
  */
31
38
  export type NewModelData<T> = Partial<InferModelAttributes<Def<T>> & BelongsToForeignKeys<Def<T>>>;
39
+ /**
40
+ * Strict insertable shape: only attributes marked `fillable: true` in
41
+ * the model definition (plus belongsTo foreign keys), partial because
42
+ * many fillable columns have factory defaults at the DB layer.
43
+ *
44
+ * Use this when you want compile-time enforcement that consumers
45
+ * can't pass non-fillable fields to `create()` / `insert()`.
46
+ * {@link NewModelData} is the looser sibling that allows any attribute.
47
+ */
48
+ export type ModelCreateData<T> = Partial<Pick<InferModelAttributes<Def<T>>, Extract<FillableKeys<Def<T>>, keyof InferModelAttributes<Def<T>>>> & BelongsToForeignKeys<Def<T>>>;
49
+ /** Loose variant of {@link ModelCreateData} — same shape as {@link NewModelData}, aliased for naming-parity with the row types. */
50
+ export type ModelCreateDataLoose<T> = NewModelData<T>;
32
51
  /**
33
52
  * Updateable data type: model attributes + FK columns, all optional.
34
53
  *
35
54
  * @example
36
55
  * import type { UpdateModelData } from '@stacksjs/orm'
37
56
  * import type Post from '../models/Post'
38
- * type PostUpdate = UpdateModelData<Post>
57
+ * type PostUpdate = UpdateModelData<typeof Post>
39
58
  */
40
59
  export type UpdateModelData<T> = Partial<InferModelAttributes<Def<T>> & BelongsToForeignKeys<Def<T>>>;
60
+ /**
61
+ * Just the attribute records flagged `fillable: true` — useful for
62
+ * code-generators and any helper that needs the typed shape of a
63
+ * model's fillable-attribute config (e.g., admin form schemas).
64
+ */
65
+ export type InferFillableAttributes<T> = {
66
+ [K in Extract<FillableKeys<Def<T>>, keyof Def<T>['attributes']>]: Def<T>['attributes'][K]
67
+ }
68
+ /**
69
+ * Every valid column name for the model (attributes + system fields
70
+ * added by traits like `id`, `uuid`, `created_at`). Useful for
71
+ * constraining query builders that accept a `column` parameter.
72
+ */
73
+ export type InferColumnNames<T> = ColumnName<Def<T>>;
74
+ /**
75
+ * Attribute keys whose `type` is declared as `'number'` in the model
76
+ * definition. Used to constrain aggregate methods (`sum`, `avg`,
77
+ * `min`, `max`) so they can't be called against string columns.
78
+ *
79
+ * Models that don't declare an explicit `type` per attribute (the
80
+ * common case — most validation rules are inferred from
81
+ * `schema.number()` chains, not declared on `type`) fall back to
82
+ * `AttributeKeys<Def<T>>` here. Tighten by declaring `type: 'number'`
83
+ * on the attribute spec when narrowing matters.
84
+ */
85
+ export type InferNumericColumns<T> = {
86
+ [K in AttributeKeys<Def<T>>]: Def<T>['attributes'][K] extends { type: 'number' } ? K : never
87
+ }[AttributeKeys<Def<T>>] extends infer R
88
+ ? [R] extends [never] ? AttributeKeys<Def<T>> : R
89
+ : never;
@@ -0,0 +1,68 @@
1
+ import type { CursorPaginator, Paginator, SimplePaginator } from './paginator';
2
+ /**
3
+ * Resolve `[perPage, page]` for `Model.paginate(...)`. Precedence:
4
+ *
5
+ * 1. Explicit positional arg from the caller (`Model.paginate(20, 3)`).
6
+ * 2. `?per_page=` / `?page=` from the active request (auto-magic).
7
+ * 3. Defaults: `perPage = 15`, `page = 1`.
8
+ *
9
+ * `perPage` from the query string is clamped to `[1, DEFAULT_MAX_PER_PAGE]`.
10
+ * Explicit positional `perPage` is trusted as-is (the caller is the app,
11
+ * not an untrusted client).
12
+ *
13
+ * `page` is clamped to `>= 1` from both sources to avoid `OFFSET -X`-style
14
+ * negatives reaching the driver.
15
+ */
16
+ export declare function resolvePageArgs(perPageArg?: number, pageArg?: number): ResolvedPageArgs;
17
+ /**
18
+ * Resolve cursor args for `Model.cursorPaginate(...)`. Precedence
19
+ * mirrors {@link resolvePageArgs} — explicit > `?cursor=` > null.
20
+ *
21
+ * Cursor values that arrive via the query string (always strings) are
22
+ * fed through {@link parseCursor} so a composite cursor encoded as
23
+ * `JSON.stringify([val1, val2])` reaches bqb as a real array — bqb's
24
+ * `sql(cursor)` interpolation expects an array when the `column` arg
25
+ * is an array (composite-key pagination). Without this parse, the
26
+ * cursor round-trip breaks: serialize → string → wire → string →
27
+ * bqb thinks it's a primitive → wrong query.
28
+ */
29
+ export declare function resolveCursorArgs(perPageArg?: number, cursorArg?: string | number | unknown[] | null): { perPage: number, cursor: unknown };
30
+ /**
31
+ * Parse a cursor value into the shape bqb's `cursorPaginate` expects.
32
+ *
33
+ * - `null` / `undefined` → `null` (first page, no WHERE clause)
34
+ * - Already an array → returned as-is (composite cursor, native form)
35
+ * - String starting with `[` → JSON-parsed back into an array
36
+ * (composite cursor that was serialized for the wire format)
37
+ * - All other strings / primitives → returned as-is (single-column
38
+ * cursor)
39
+ *
40
+ * This is the missing piece in the wire-format round-trip:
41
+ * {@link toCursorPaginator} encodes composite cursors with
42
+ * `JSON.stringify` so they survive a URL query param round-trip, and
43
+ * this function decodes them on the way back in.
44
+ */
45
+ export declare function parseCursor(value: string | number | unknown[] | null | undefined): unknown;
46
+ /**
47
+ * Fill in `path`, `prev_page_url`, `next_page_url` (and `first_page_url`
48
+ * / `last_page_url` for the full {@link Paginator}) from the active
49
+ * request URL. Mutates the paginator in place and returns it for chaining.
50
+ *
51
+ * No-op when no request is in scope — keeps CLI / queue / cron callers
52
+ * unaffected. Preserves all OTHER query params on the request so search
53
+ * filters survive across page navigations (`?status=active&page=2` →
54
+ * `next_page_url` includes `status=active`).
55
+ */
56
+ export declare function enrichPaginatorUrls<T>(paginator: Paginator<T>): Paginator<T>;
57
+ export declare function enrichPaginatorUrls<T>(paginator: SimplePaginator<T>): SimplePaginator<T>;
58
+ export declare function enrichPaginatorUrls<T>(paginator: CursorPaginator<T>): CursorPaginator<T>;
59
+ /** Test helper — reset the lazy-import cache (for tests that mock the
60
+ * router module after first access). */
61
+ export declare function __resetRequestContextCache(): void;
62
+ /**
63
+ * Resolved pagination args ready to pass to bqb's `paginate(perPage, page)`.
64
+ */
65
+ export declare interface ResolvedPageArgs {
66
+ perPage: number
67
+ page: number
68
+ }
@@ -0,0 +1,99 @@
1
+ /** Duck-typed shape check — used by the response serializer (P4) to
2
+ * detect a paginator-shaped return value from an action. */
3
+ export declare function isPaginator<T = unknown>(value: unknown): value is Paginator<T>;
4
+ /** Duck-typed shape check for the simple paginator. */
5
+ export declare function isSimplePaginator<T = unknown>(value: unknown): value is SimplePaginator<T>;
6
+ /** Duck-typed shape check for the cursor paginator. */
7
+ export declare function isCursorPaginator<T = unknown>(value: unknown): value is CursorPaginator<T>;
8
+ /**
9
+ * Convert bun-query-builder's `paginate()` output (`{ data, meta:
10
+ * { perPage, page, total, lastPage } }`) into the canonical Stacks
11
+ * shape. P1 of the migration — the URL fields stay undefined until
12
+ * P2 wires the request-context resolver.
13
+ */
14
+ export declare function toPaginator<T>(result: { data: T[], meta: { perPage: number, page: number, total: number, lastPage: number } }): Paginator<T>;
15
+ /** Convert bqb's `simplePaginate()` output to the canonical
16
+ * {@link SimplePaginator} shape. */
17
+ export declare function toSimplePaginator<T>(result: { data: T[], meta: { perPage: number, page: number, hasMore: boolean } }): SimplePaginator<T>;
18
+ /** Convert bqb's `cursorPaginate()` output to the canonical
19
+ * {@link CursorPaginator} shape. Cursors are serialized to strings
20
+ * via JSON — composite cursors (arrays) survive the round-trip. */
21
+ export declare function toCursorPaginator<T>(result: { data: T[], meta: { perPage: number, nextCursor: unknown, prevCursor: unknown } }): CursorPaginator<T>;
22
+ /**
23
+ * Canonical paginator types + adapters (stacksjs/stacks#1905, P1 from #1910).
24
+ *
25
+ * Four pagination shapes used to ship across the framework — bqb's SQL
26
+ * client `{ data, meta }`, bqb's sync ORM `{ data, total, page, ... }`,
27
+ * the search-engine's `{ hits, total, page, perPage }`, and a fifth
28
+ * declared-but-not-implemented Stacks shape (`{ data, paging, next_cursor }`)
29
+ * that the runtime never actually produced. This module unifies them
30
+ * to the Laravel-flavored, snake_case shapes below.
31
+ *
32
+ * P1 ships only `data` + counts; the URL fields are declared but left
33
+ * `undefined` by default. P2 (stacksjs/stacks#1906) wires them in when
34
+ * a request is in scope.
35
+ *
36
+ * Shape choices:
37
+ * - snake_case for JSON friendliness (matches Laravel's serialized
38
+ * paginator + the existing REST convention across the Stacks API)
39
+ * - `from` / `to` are 1-indexed offsets of the first/last row on the
40
+ * current page; `null` when the page is empty
41
+ * - `has_more_pages` is the cheap boolean apps actually want — saves
42
+ * `current_page < last_page` checks everywhere
43
+ */
44
+ /**
45
+ * Full paginator — knows the total row count so it can compute
46
+ * `last_page`, supports "jump to page N" UI patterns. Costs an extra
47
+ * `COUNT(*)` query; use {@link SimplePaginator} or {@link CursorPaginator}
48
+ * for very large tables.
49
+ */
50
+ export declare interface Paginator<T> {
51
+ data: T[]
52
+ current_page: number
53
+ per_page: number
54
+ total: number
55
+ last_page: number
56
+ from: number | null
57
+ to: number | null
58
+ has_more_pages: boolean
59
+ prev_page_url?: string | null
60
+ next_page_url?: string | null
61
+ first_page_url?: string
62
+ last_page_url?: string
63
+ path?: string
64
+ }
65
+ /**
66
+ * Simple paginator — drops the `COUNT(*)` query. Faster on large tables
67
+ * but can't render "jump to page 5" UIs since `last_page` / `total` are
68
+ * unknown. Suitable for infinite-scroll feeds.
69
+ */
70
+ export declare interface SimplePaginator<T> {
71
+ data: T[]
72
+ current_page: number
73
+ per_page: number
74
+ has_more_pages: boolean
75
+ prev_page_url?: string | null
76
+ next_page_url?: string | null
77
+ path?: string
78
+ }
79
+ /**
80
+ * Cursor (keyset) paginator — uses `WHERE id > :cursor` ordering rather
81
+ * than `LIMIT/OFFSET`, so query cost stays constant regardless of how
82
+ * deep the user has paged. The trade-off is no random-access ("jump to
83
+ * page N"); only prev/next.
84
+ *
85
+ * Cursor values are opaque to the caller — serialize as JSON, send to
86
+ * the client, accept back unchanged. Implementations (cursorPaginate)
87
+ * decide the cursor shape (single column value, base64 tuple for
88
+ * composite keys, etc.).
89
+ */
90
+ export declare interface CursorPaginator<T> {
91
+ data: T[]
92
+ per_page: number
93
+ next_cursor: string | null
94
+ prev_cursor: string | null
95
+ has_more_pages: boolean
96
+ prev_page_url?: string | null
97
+ next_page_url?: string | null
98
+ path?: string
99
+ }
@@ -20,6 +20,14 @@ export declare function setAuditUser(id: number | string | null): void;
20
20
  * the model's write methods.
21
21
  */
22
22
  export declare function createAuditMethods(modelName: string): AuditHelpers;
23
+ /**
24
+ * Resolve the transactional opt-in from a model's `traits.useAudit`
25
+ * declaration. Accepts both `true` (default, best-effort) and
26
+ * `{ transactional: true }` (audit failures roll back the user's
27
+ * write). Centralized so the wrapper functions below have a single
28
+ * boolean to check.
29
+ */
30
+ export declare function resolveAuditOptions(useAudit: unknown): { transactional: boolean };
23
31
  /**
24
32
  * Wire the audit trait into a model's static surface. Wraps `create`,
25
33
  * `update`, and `delete` so each one writes a `model_audits` row after a
@@ -31,15 +39,24 @@ export declare function createAuditMethods(modelName: string): AuditHelpers;
31
39
  * we wrap the final composed function rather than something that gets
32
40
  * shadowed later.
33
41
  *
42
+ * **Transactional opt-in (stacksjs/stacks#1876 X-2):** pass
43
+ * `{ transactional: true }` to wrap each create/update/delete in a
44
+ * `db.transaction(...)` so audit-row write failures roll back the
45
+ * underlying user write. Default is best-effort (audit failures are
46
+ * logged but don't abort the operation) — appropriate for a debug /
47
+ * change-log use case but NOT for compliance scenarios where a missing
48
+ * audit entry is itself a failure mode.
49
+ *
34
50
  * @example
35
51
  * ```ts
36
- * // Inside defineModel():
37
- * if (definition.traits?.useAudit) {
38
- * applyAudit(baseModel, definition)
39
- * }
52
+ * // Best-effort (default): audit failures don't abort writes.
53
+ * defineModel({ traits: { useAudit: true } })
54
+ *
55
+ * // Transactional (compliance): audit failures roll back the write.
56
+ * defineModel({ traits: { useAudit: { transactional: true } } })
40
57
  * ```
41
58
  */
42
- export declare function applyAudit(baseModel: Record<string, unknown>, modelName: string, primaryKey?: string): void;
59
+ export declare function applyAudit(baseModel: Record<string, unknown>, modelName: string, primaryKey?: string, opts?: { transactional?: boolean }): void;
43
60
  export declare interface AuditHelpers {
44
61
  audits: (id: number | string) => Promise<Array<Record<string, unknown>>>
45
62
  }
@@ -1 +1,37 @@
1
+ import type Stripe from 'stripe';
1
2
  export declare function createBillableMethods(_tableName: string): void;
3
+ /**
4
+ * Stored subscription row shape — narrower than `Record<string, unknown>`
5
+ * so callers get autocompletion on the common fields without us
6
+ * importing the full `subscriptions` model type (which would pull in
7
+ * a circular @stacksjs/orm dependency). Keep in sync with the schema
8
+ * in `database/src/custom/subscriptions.ts`.
9
+ */
10
+ export declare interface StoredSubscriptionRow {
11
+ id: number
12
+ user_id: number
13
+ type: string
14
+ provider_id: string
15
+ provider_status: string
16
+ provider_price_id?: string
17
+ unit_price?: number
18
+ quantity?: number
19
+ trial_ends_at?: string
20
+ ends_at?: string
21
+ provider_type?: string
22
+ last_used_at?: string
23
+ }
24
+ /**
25
+ * Return shape for `newSubscription` / `updateSubscription` — pairs
26
+ * the Stripe-side subscription with the most-recent payment intent so
27
+ * callers can immediately handle 3DS / SCA flows without a second
28
+ * round-trip to Stripe.
29
+ */
30
+ export declare interface NewSubscriptionResult {
31
+ subscription: Stripe.Response<Stripe.Subscription>
32
+ paymentIntent?: Stripe.PaymentIntent
33
+ }
34
+ export declare interface ActiveSubscriptionResult {
35
+ subscription: StoredSubscriptionRow
36
+ providerSubscription: Stripe.Response<Stripe.Subscription>
37
+ }
@@ -1 +1,5 @@
1
+ // `db` is a Proxy whose methods are typed via bun-query-builder's generics —
2
+ // resolution to the concrete invocation here can leave methods marked
3
+ // `T | undefined` under strict null checks. Cast through `any` so the trait
4
+ // helpers can call the runtime-defined methods without a guard at every site.
1
5
  export declare function createCategorizableMethods(tableName: string): void;
@@ -1,3 +1,4 @@
1
+ import type { CursorPaginator, Paginator, SimplePaginator } from './paginator';
1
2
  import type { Operator } from './subquery';
2
3
  export declare interface OrmDriver<T = unknown> {
3
4
  find: (id: number) => Promise<T | undefined>
@@ -39,11 +40,14 @@ export declare interface SelectedQuery<TTable, TJson, K extends string> {
39
40
  get(): Promise<SelectedResult<TJson, K>[]>
40
41
  latest(column?: keyof TTable): Promise<SelectedResult<TJson, K> | undefined>
41
42
  oldest(column?: keyof TTable): Promise<SelectedResult<TJson, K> | undefined>
42
- paginate(options?: { limit?: number, offset?: number, page?: number }): Promise<{
43
- data: SelectedResult<TJson, K>[]
44
- paging: { total_records: number, page: number, total_pages: number }
45
- next_cursor: number | null
46
- }>
43
+ paginate(perPage?: number, page?: number): Promise<Paginator<SelectedResult<TJson, K>>>
44
+ simplePaginate(perPage?: number, page?: number): Promise<SimplePaginator<SelectedResult<TJson, K>>>
45
+ cursorPaginate(
46
+ perPage?: number,
47
+ cursor?: string | number | unknown[] | null,
48
+ column?: keyof TTable | (keyof TTable)[],
49
+ direction?: 'asc' | 'desc',
50
+ ): Promise<CursorPaginator<SelectedResult<TJson, K>>>
47
51
  chunk(size: number, callback: (models: SelectedResult<TJson, K>[]) => Promise<void>): Promise<void>
48
52
  pluck<PK extends Extract<K | 'id', keyof TJson>>(field: PK): Promise<TJson[PK][]>
49
53
  max(field: keyof TTable): Promise<number>
@@ -14,7 +14,7 @@ export declare function getGuardedAttributes(model: Model): string[];
14
14
  export declare function getFillableAttributes(model: Model, otherModelRelations: RelationConfig[]): string[];
15
15
  export declare function extractFields(model: Model, modelFile: string): Promise<ModelElement[]>;
16
16
  export declare function mapEntity(attribute: ModelElement): string | undefined;
17
- export declare function extractImports(filePath: string): string[];
17
+ export declare function extractImports(filePath: string): Promise<string[]>;
18
18
  export declare function extractAttributesFromModel(filePath: string): Promise<AttributesElements>;
19
19
  export declare function findCoreModel(modelName: string): string;
20
20
  export declare function findUserModel(modelName: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/orm",
3
3
  "type": "module",
4
- "version": "0.70.44",
4
+ "version": "0.70.53",
5
5
  "description": "The Stacks ORM integration",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -29,6 +29,10 @@
29
29
  "bun": "./src/index.ts",
30
30
  "import": "./dist/index.js"
31
31
  },
32
+ "./traits/*": {
33
+ "bun": "./src/traits/*.ts",
34
+ "types": "./dist/src/traits/*.d.ts"
35
+ },
32
36
  "./*": {
33
37
  "bun": "./src/*",
34
38
  "import": "./dist/*"
@@ -46,14 +50,14 @@
46
50
  "prepublishOnly": "bun run build"
47
51
  },
48
52
  "dependencies": {
49
- "@stacksjs/ts-validation": "^0.4.9",
50
- "bun-query-builder": "^0.1.21"
53
+ "@stacksjs/ts-validation": "^0.5.0",
54
+ "bun-query-builder": "^0.1.38"
51
55
  },
52
56
  "devDependencies": {
53
- "@stacksjs/build": "^0.70.44",
54
- "@stacksjs/config": "^0.70.44",
57
+ "@stacksjs/build": "workspace:*",
58
+ "@stacksjs/config": "0.70.53",
55
59
  "better-dx": "^0.2.12",
56
- "@stacksjs/query-builder": "^0.70.44",
57
- "@stacksjs/types": "^0.70.44"
60
+ "@stacksjs/query-builder": "0.70.53",
61
+ "@stacksjs/types": "0.70.53"
58
62
  }
59
63
  }