@getstrata/core 2.2.0 → 2.2.2

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/core/contracts/serviceTokens.d.ts +3 -2
  3. package/dist/core/database/baseRepository.d.ts +5 -0
  4. package/dist/core/database/model.d.ts +3 -1
  5. package/dist/core/database/query.d.ts +6 -1
  6. package/dist/core/database/repositoryQuery.d.ts +7 -1
  7. package/dist/core/database/schema/blueprint.d.ts +2 -0
  8. package/dist/core/database/schema/columnDefinition.d.ts +1 -1
  9. package/dist/core/database/types.d.ts +10 -1
  10. package/dist/core/http/requestMetaContext.d.ts +2 -0
  11. package/dist/core/queue/createAppQueue.d.ts +2 -1
  12. package/dist/core/queue/failedJobService.d.ts +3 -1
  13. package/dist/core/queue/index.d.ts +10 -2
  14. package/dist/core/queue/jobRunner.d.ts +7 -1
  15. package/dist/core/queue/publicQueue.d.ts +2 -1
  16. package/dist/core/queue/queueConfig.d.ts +3 -0
  17. package/dist/core/queue/redisQueue.d.ts +5 -7
  18. package/dist/core/queue/redisQueueKeys.d.ts +8 -0
  19. package/dist/core/queue/redisStreams.d.ts +73 -0
  20. package/dist/core/queue/types.d.ts +1 -0
  21. package/dist/core/tenant/publicTenantHost.d.ts +11 -0
  22. package/dist/core/tenant/tenantMiddleware.d.ts +4 -0
  23. package/dist/entries/auth/basicAuthGuard.js +1 -0
  24. package/dist/entries/auth/jwtGuard.js +1 -0
  25. package/dist/entries/auth/sessionGuard.js +1 -0
  26. package/dist/entries/contracts/serviceTokens.js +2 -0
  27. package/dist/entries/database/query.js +70 -1
  28. package/dist/entries/database/repositoryQuery.js +99 -1
  29. package/dist/entries/database/schema.js +9 -0
  30. package/dist/entries/database/sqliteConnection.js +1 -1
  31. package/dist/entries/queue/createAppQueue.js +681 -66
  32. package/dist/entries/queue/failedJobRepository.js +1 -1
  33. package/dist/entries/queue/failedJobService.js +5 -1
  34. package/dist/entries/queue/jobRunner.js +67 -2
  35. package/dist/entries/queue/publicQueue.js +679 -65
  36. package/dist/entries/queue/queueMetrics.js +694 -72
  37. package/dist/entries/queue/redisQueue.js +765 -148
  38. package/dist/framework/public-api.d.ts +2 -2
  39. package/dist/index.js +855 -69
  40. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # @getstrata/core changelog
2
2
 
3
+ ## 2.2.2
4
+
5
+ - orm improvments
6
+
7
+ ## 2.2.1
8
+
9
+ F02 — complete queue durability. Queues have leased reservations, recovery and fenced acknowledgement, but still use Redis lists. Persisted retry schedules, execution deadlines, cancellation and the planned Streams migration remain unfinished. Retries currently wait inside the running worker.
10
+ F10 — production public-route tenancy. Separate anonymous access from development tenant headers, support a trusted-host resolver, reject unknown hosts, and accept forwarded hosts only through configured trusted proxies. The shop still lacks the approved-domain mapping.
11
+ F12 — initial administrator provisioning and migration guidance. Automatic demo seeding is fixed. Secure initial-admin provisioning and a consolidated upgrade/operations guide remain.
12
+ Distributed qualification. Cache generations/fill leases, scheduler ownership, OpenTelemetry, body limits and lifecycle controls now exist. Their implementation should no longer be described as entirely pending, but broader failure and capacity qualification is still required.
13
+
3
14
  ## 2.2.0
4
15
 
5
16
  Consistent model scopes.
@@ -1,9 +1,10 @@
1
1
  import type { AbilityChecker } from "../auth/abilityChecker";
2
2
  import type { ThrottleQuotaPolicy } from "../http/throttleMiddleware";
3
- import type { TenantResolver } from "../tenant/tenantMiddleware";
3
+ import type { PublicTenancyOptions, TenantResolver } from "../tenant/tenantMiddleware";
4
4
  import type { AuthUserDirectory } from "./authUserDirectory";
5
5
  import type { ServiceContainerLike } from "./container";
6
6
  declare const CORE_THROTTLE_QUOTA_POLICY_TOKEN: import("./container").ServiceToken<ThrottleQuotaPolicy>;
7
+ declare const CORE_PUBLIC_TENANCY_TOKEN: import("./container").ServiceToken<PublicTenancyOptions>;
7
8
  declare const CORE_TENANT_RESOLVER_TOKEN: import("./container").ServiceToken<TenantResolver>;
8
9
  declare const CORE_HTTP_CLEANUP_TOKEN: import("./container").ServiceToken<(cleanup: () => void) => void>;
9
10
  declare const CORE_CONFIG_TOKEN = "core.config";
@@ -19,4 +20,4 @@ declare function isAbilityChecker(value: unknown): value is AbilityChecker;
19
20
  declare function isAuthUserDirectory(value: unknown): value is AuthUserDirectory;
20
21
  declare function resolveAbilityChecker(container: ServiceContainerLike): AbilityChecker;
21
22
  declare function resolveAuthUserDirectory(container: ServiceContainerLike): AuthUserDirectory | null;
22
- export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_HTTP_CLEANUP_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_QUEUE_TOKEN, CORE_TENANT_RESOLVER_TOKEN, CORE_THROTTLE_QUOTA_POLICY_TOKEN, CORE_TOKEN_SERVICE_TOKEN, isAbilityChecker, isAuthUserDirectory, resolveAbilityChecker, resolveAuthUserDirectory, };
23
+ export { CORE_ABILITY_CHECKER_TOKEN, CORE_AUTH_TOKEN, CORE_AUTH_USER_DIRECTORY_TOKEN, CORE_CACHE_TOKEN, CORE_CONFIG_TOKEN, CORE_EVENT_BUS_TOKEN, CORE_HTTP_CLEANUP_TOKEN, CORE_POLICY_GATE_TOKEN, CORE_PUBLIC_TENANCY_TOKEN, CORE_QUEUE_TOKEN, CORE_TENANT_RESOLVER_TOKEN, CORE_THROTTLE_QUOTA_POLICY_TOKEN, CORE_TOKEN_SERVICE_TOKEN, isAbilityChecker, isAuthUserDirectory, resolveAbilityChecker, resolveAuthUserDirectory, };
@@ -45,6 +45,11 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
45
45
  incrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
46
46
  decrementById(id: TEntity[PrimaryKey], column: keyof TEntity & string, amount?: number, extra?: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
47
47
  create(values: MutationValues<TEntity>): Promise<TEntity>;
48
+ /** Set-based writes intentionally bypass per-row model observers and write casts. */
49
+ updateWhere(changes: UpdateValues<TEntity, PrimaryKey>, options: QueryOptions<TEntity>): Promise<number>;
50
+ deleteWhere(options: QueryOptions<TEntity>): Promise<number>;
51
+ updateWhereReturning<K extends keyof TEntity & string>(changes: UpdateValues<TEntity, PrimaryKey>, columns: readonly K[], options: QueryOptions<TEntity>): Promise<Pick<TEntity, K>[]>;
52
+ private executeConditionalWrite;
48
53
  updateById(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>): Promise<TEntity | null>;
49
54
  updateByIdOrThrow(id: TEntity[PrimaryKey], changes: UpdateValues<TEntity, PrimaryKey>, errorFactory?: ErrorFactory<TEntity[PrimaryKey]>): Promise<TEntity>;
50
55
  deleteById(id: TEntity[PrimaryKey]): Promise<boolean>;
@@ -4,7 +4,7 @@ import { BelongsToManyRelationQuery, BelongsToRelationQuery, HasManyRelationQuer
4
4
  import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasOneRelation } from "./relationships.ts";
5
5
  import type { RepositoryQuery } from "./repositoryQuery.ts";
6
6
  import type { TableDefinition } from "./table.ts";
7
- import type { ModelWhere, ModelWriteValues, QueryOptions, QueryWhere } from "./types.ts";
7
+ import type { ModelWhere, ModelWriteValues, QueryLockOptions, QueryOptions, QueryWhere } from "./types.ts";
8
8
  type CastType = "date" | "datetime" | "json" | "bool" | "boolean" | "integer" | "int" | "hashed";
9
9
  type LoadedAttributes = Record<string, unknown>;
10
10
  type ModelCasts = Partial<Record<string, CastType>>;
@@ -70,6 +70,8 @@ declare class ModelQuery<TModel extends object = AnyModel, TCounts extends strin
70
70
  /** Qualified/dynamic filters retain existing SQL semantics through an explicit escape hatch. */
71
71
  whereDynamic(input: QueryWhere<object>): this;
72
72
  select<K extends keyof ModelAttributes<TModel> & string>(...columns: readonly K[]): ModelProjection<ModelAttributes<TModel>, K>;
73
+ lockForUpdate(options?: QueryLockOptions): this;
74
+ sharedLock(options?: QueryLockOptions): this;
73
75
  orderBy(orderBy: QueryOptions<ModelAttributes<TModel>>["orderBy"]): this;
74
76
  limit(limit: number): this;
75
77
  offset(offset: number): this;
@@ -68,4 +68,9 @@ declare function buildDeleteByIdQuery<TEntity extends object, PrimaryKey extends
68
68
  text: string;
69
69
  params: unknown[];
70
70
  };
71
- export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
71
+ /** Single-statement writes: predicates and protected scopes stay in the mutation. */
72
+ declare function buildConditionalWriteQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string>(table: TableDefinition<TEntity, PrimaryKey>, operation: "update" | "delete", changes: UpdateValues<TEntity, PrimaryKey>, options: QueryOptions<TEntity>, returning?: readonly (keyof TEntity & string)[]): {
73
+ text: string;
74
+ params: unknown[];
75
+ };
76
+ export { assertSafeProjectionExpression, buildAdvancedWhereClause, buildConditionalWriteQuery, buildCountQuery, buildDeleteByIdQuery, buildGroupedCountQuery, buildIncrementQuery, buildInsertQuery, buildJoinClause, buildOrderByClause, buildProjectionQuery, buildQueryWhereClause, buildRestoreByIdQuery, buildSelectQuery, buildSoftDeleteByIdQuery, buildUpdateQuery, buildUpsertQuery, buildWhereClause, parseQualifiedColumn, qualifyColumn, quoteIdentifier, resolveQualifiedColumn, resolveSoftDeleteColumn, };
@@ -1,7 +1,7 @@
1
1
  import type { PaginatedResult } from "../pagination/index.ts";
2
2
  import type BaseRepository from "./baseRepository.ts";
3
3
  import type { BelongsToManyRelation, BelongsToRelation, HasManyRelation, HasManyThroughRelation, MorphManyRelation, MorphOneRelation, MorphToRelation } from "./relationships.ts";
4
- import type { QueryOptions, QueryWhere } from "./types.ts";
4
+ import type { QueryLockOptions, QueryOptions, QueryWhere, UpdateValues } from "./types.ts";
5
5
  import { WhereBuilder } from "./whereBuilder.ts";
6
6
  type LoadedRow = Record<string, unknown>;
7
7
  declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof TEntity & string> {
@@ -16,6 +16,8 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
16
16
  /** Preserve completed scope predicates separately from subsequent caller OR conditions. */
17
17
  protectWhere(): this;
18
18
  orWhere(input: QueryWhere<TEntity> | ((builder: WhereBuilder<TEntity>) => void)): this;
19
+ lockForUpdate(options?: QueryLockOptions): this;
20
+ sharedLock(options?: QueryLockOptions): this;
19
21
  orderBy(orderBy: QueryOptions<TEntity>["orderBy"]): this;
20
22
  limit(limit: number): this;
21
23
  whereNull(column: keyof TEntity & string): this;
@@ -39,6 +41,10 @@ declare class RepositoryQuery<TEntity extends object, PrimaryKey extends keyof T
39
41
  withHasManyThrough<TFar extends object, LocalKey extends keyof TEntity & string, SecondKey extends keyof TFar & string, Alias extends string>(as: Alias, relation: HasManyThroughRelation<TEntity, TFar, LocalKey, string, string, SecondKey>, farRepository: BaseRepository<TFar, keyof TFar & string>, options?: Omit<QueryOptions<TFar>, "where">): this;
40
42
  withTrashed(): this;
41
43
  onlyTrashed(): this;
44
+ update(changes: UpdateValues<TEntity, PrimaryKey>): Promise<number>;
45
+ updateReturning<K extends keyof TEntity & string>(changes: UpdateValues<TEntity, PrimaryKey>, ...columns: K[]): Promise<Pick<TEntity, K>[]>;
46
+ delete(): Promise<number>;
47
+ private assertNoMutationEagerLoads;
42
48
  get(): Promise<Array<TEntity & LoadedRow>>;
43
49
  first(): Promise<(TEntity & LoadedRow) | null>;
44
50
  count(): Promise<number>;
@@ -30,6 +30,8 @@ declare class Blueprint {
30
30
  readonly droppedIndexes: string[];
31
31
  constructor(table: string, action: BlueprintAction);
32
32
  id(name?: string): ColumnDefinition;
33
+ /** Generated 64-bit primary key; does not change existing id() migrations. */
34
+ bigId(name?: string): ColumnDefinition;
33
35
  string(name: string, length?: number): ColumnDefinition;
34
36
  text(name: string): ColumnDefinition;
35
37
  boolean(name: string): ColumnDefinition;
@@ -1,4 +1,4 @@
1
- type ColumnKind = "id" | "string" | "text" | "boolean" | "integer" | "bigInteger" | "timestamp" | "json" | "jsonb" | "foreignId";
1
+ type ColumnKind = "id" | "bigId" | "string" | "text" | "boolean" | "integer" | "bigInteger" | "timestamp" | "json" | "jsonb" | "foreignId";
2
2
  interface ForeignKeyOptions {
3
3
  referencesTable: string;
4
4
  referencesColumn: string;
@@ -78,7 +78,16 @@ type QuerySelectItem = {
78
78
  query: string;
79
79
  as: string;
80
80
  };
81
+ type QueryLockOptions = {
82
+ wait?: "wait" | "nowait" | "skipLocked";
83
+ /** PostgreSQL table names in this query; aliases are not inferred. */
84
+ of?: readonly string[];
85
+ };
86
+ type QueryLock = QueryLockOptions & {
87
+ mode: "update" | "share";
88
+ };
81
89
  interface QueryOptions<TEntity extends object> {
90
+ lock?: QueryLock;
82
91
  whereNodes?: WhereNode<TEntity>[];
83
92
  where?: QueryWhere<TEntity>;
84
93
  orderBy?: QueryOrder<TEntity> | QueryOrder<TEntity>[] | QueryOrderShorthand<TEntity>;
@@ -93,4 +102,4 @@ interface QueryOptions<TEntity extends object> {
93
102
  }
94
103
  type MutationValues<TEntity> = Partial<TEntity>;
95
104
  type UpdateValues<TEntity, PrimaryKey extends keyof TEntity & string = keyof TEntity & string> = Partial<Omit<TEntity, PrimaryKey>>;
96
- export type { DatabaseComparable, DatabaseScalar, ModelWhere, ModelWriteValues, MutationValues, QueryFilterValue, QueryJoin, QueryJoinOn, QueryOperator, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, UpdateValues, };
105
+ export type { DatabaseComparable, DatabaseScalar, ModelWhere, ModelWriteValues, MutationValues, QueryFilterValue, QueryJoin, QueryJoinOn, QueryLock, QueryLockOptions, QueryOperator, QueryOptions, QueryOrder, QuerySelectItem, QueryWhere, UpdateValues, };
@@ -1,5 +1,7 @@
1
1
  type RequestMeta = {
2
2
  ipAddress: string | null;
3
+ /** Immediate socket peer recorded by the transport; never a forwarded client header. */
4
+ peerAddress?: string | null;
3
5
  userAgent: string | null;
4
6
  request?: Request;
5
7
  /** Registered route template; never a URL supplied by the client. */
@@ -1,5 +1,6 @@
1
1
  import type { Queue } from "./index";
2
2
  import { createFailedJobService, createQueueWorker, createTrackedJob, type FailedJobService } from "./publicQueue";
3
+ import { resolveQueueConfig } from "./queueConfig";
3
4
  declare const FAILED_JOB_SERVICE_TOKEN = "core.failedJobs";
4
5
  declare function createAppQueue(driver: "sync" | "async" | "redis", redisUrl?: string, failedJobs?: FailedJobService, registerJobs?: () => void): Queue;
5
- export { createAppQueue, createFailedJobService, createQueueWorker, createTrackedJob, FAILED_JOB_SERVICE_TOKEN, };
6
+ export { createAppQueue, createFailedJobService, createQueueWorker, createTrackedJob, FAILED_JOB_SERVICE_TOKEN, resolveQueueConfig, };
@@ -5,11 +5,13 @@ declare class FailedJobService {
5
5
  constructor(repository: FailedJobRepository);
6
6
  recordFailure(input: {
7
7
  jobName: string;
8
+ jobId?: string;
8
9
  payload: Record<string, unknown>;
9
10
  exception: string;
10
11
  }): Promise<FailedJobRecord>;
11
12
  listRecent(limit?: number): Promise<FailedJobRecord[]>;
12
- retry(id: number): Promise<FailedJobRecord>;
13
+ /** Admit the replay before deleting its recovery record. Delivery remains at least once. */
14
+ retry(id: number, enqueue: (record: FailedJobRecord) => Promise<void>): Promise<FailedJobRecord>;
13
15
  delete(id: number): Promise<void>;
14
16
  flush(): Promise<number>;
15
17
  }
@@ -1,14 +1,22 @@
1
1
  type QueuePriority = "high" | "default" | "low";
2
+ /** Logical identity is stable across automatic recovery and supported manual replay. */
3
+ interface JobContext {
4
+ readonly jobId: string;
5
+ readonly signal: AbortSignal;
6
+ readonly deadlineAtMs?: number;
7
+ }
2
8
  declare abstract class Job<TPayload extends object = object> {
3
9
  static readonly jobName?: string;
4
10
  readonly maxAttempts?: number;
5
11
  readonly backoffMs?: number;
6
12
  readonly priority?: QueuePriority;
7
13
  constructor();
8
- abstract handle(payload: TPayload): Promise<void>;
14
+ abstract handle(payload: TPayload, context?: JobContext): Promise<void>;
9
15
  }
10
16
  interface Queue {
11
17
  close?(): void | Promise<void>;
18
+ /** Preserve a stored logical identity on deliberate replay; reset execution controls. */
19
+ replay?<TPayload extends object>(job: Job<TPayload>, payload: TPayload, jobId: string): Promise<void>;
12
20
  dispatch<TPayload extends object>(job: Job<TPayload>, payload: TPayload): Promise<void>;
13
21
  }
14
22
  declare class SyncQueue implements Queue {
@@ -24,5 +32,5 @@ declare class AsyncQueue implements Queue {
24
32
  close(): Promise<void>;
25
33
  }
26
34
  declare function createQueue(driver: "sync" | "async"): Queue;
27
- export type { Queue, QueuePriority };
35
+ export type { JobContext, Queue, QueuePriority };
28
36
  export { AsyncQueue, createQueue, Job, SyncQueue };
@@ -3,9 +3,15 @@ interface QueueJobEnvelope {
3
3
  name: string;
4
4
  payload: Record<string, unknown>;
5
5
  attempts?: number;
6
+ jobId?: string;
7
+ deadlineAtMs?: number;
8
+ cancellable?: boolean;
6
9
  }
10
+ declare function parseQueueJobEnvelope(rawPayload: string): QueueJobEnvelope | null;
7
11
  declare function runQueueJob(envelope: QueueJobEnvelope, failedJobs: FailedJobService, options?: {
8
12
  onFailureRecorded?: () => void;
13
+ context?: import("./index").JobContext;
14
+ deferRetry?: (envelope: QueueJobEnvelope, delayMs: number) => Promise<void>;
9
15
  }): Promise<void>;
10
16
  export type { QueueJobEnvelope };
11
- export { runQueueJob };
17
+ export { parseQueueJobEnvelope, runQueueJob };
@@ -4,6 +4,7 @@ import type { Job, Queue } from "./index.ts";
4
4
  import { jobRegistry } from "./jobRegistry.ts";
5
5
  import { runQueueJob } from "./jobRunner.ts";
6
6
  import { QueueWorker, RedisQueue } from "./redisQueue.ts";
7
+ import { RedisStreamsWorker } from "./redisStreams";
7
8
  import { ResilientQueue } from "./resilientQueue.ts";
8
9
  declare function createFailedJobService(): FailedJobService;
9
10
  declare function createTrackedJob<TPayload extends object>(name: string, job: Job<TPayload>): Job<TPayload>;
@@ -12,5 +13,5 @@ declare function createProductionQueue(driver: "sync" | "async" | "redis", optio
12
13
  failedJobs?: FailedJobService;
13
14
  registerJobs?: () => void;
14
15
  }): Queue;
15
- declare function createQueueWorker(redisUrl: string, failedJobs?: FailedJobService): QueueWorker;
16
+ declare function createQueueWorker(redisUrl: string, failedJobs?: FailedJobService): QueueWorker | RedisStreamsWorker;
16
17
  export { createFailedJobService, createProductionQueue, createQueueWorker, createTrackedJob, FailedJobRepository, FailedJobService, jobRegistry, QueueWorker, RedisQueue, ResilientQueue, runQueueJob, };
@@ -7,3 +7,6 @@ declare function resolveQueueConfig(): QueueConfig;
7
7
  declare const queueConfig: QueueConfig;
8
8
  export type { QueueConfig };
9
9
  export { queueConfig, resolveQueueConfig };
10
+ declare function resolveRedisQueueTransport(): "lists" | "streams";
11
+ declare function readQueueVisibilityMs(): number;
12
+ export { readQueueVisibilityMs, resolveRedisQueueTransport };
@@ -1,15 +1,11 @@
1
1
  import { RedisClient } from "bun";
2
2
  import type FailedJobService from "./failedJobService";
3
- import type { Job, Queue, QueuePriority } from "./index";
4
- import { type QueueJobEnvelope } from "./jobRunner";
5
- declare const QUEUE_LIST_KEY: string;
6
- declare const QUEUE_HIGH_KEY: string;
7
- declare const QUEUE_LOW_KEY: string;
3
+ import type { Job, Queue } from "./index";
4
+ import { parseQueueJobEnvelope, type QueueJobEnvelope } from "./jobRunner";
5
+ import { QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, queueKeyForPriority } from "./redisQueueKeys";
8
6
  declare function queueProcessingKey(queueKey: string): string;
9
7
  declare function queueProcessingLeaseKey(queueKey: string): string;
10
8
  declare function reservationExpired(lease: string | null, now: number, visibilityMs: number): boolean;
11
- declare function queueKeyForPriority(priority?: QueuePriority): string;
12
- declare function parseQueueJobEnvelope(rawPayload: string): QueueJobEnvelope | null;
13
9
  type QueueReservation = {
14
10
  id: string;
15
11
  owner: string;
@@ -43,5 +39,7 @@ declare class QueueWorker {
43
39
  close(): void;
44
40
  }
45
41
  declare function countPendingQueueJobs(redisUrl: string): Promise<number>;
42
+ export type { StreamReservation, StreamsEnqueueOptions } from "./redisStreams";
43
+ export { migrateLegacyQueueToStreams, RedisStreamsQueue, RedisStreamsWorker, STREAM_GROUP, streamDeadLetterKey, streamQueueKey, updateStreamReservation, } from "./redisStreams";
46
44
  export type { QueueJobEnvelope, QueueReservation };
47
45
  export { countPendingQueueJobs, parseQueueJobEnvelope, QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, QueueWorker, queueInvalidKey, queueKeyForPriority, queueProcessingKey, queueProcessingLeaseKey, RedisQueue, reclaimExpiredQueueReservations, reservationExpired, reserveQueueJob, updateQueueReservation, };
@@ -0,0 +1,8 @@
1
+ import type { QueuePriority } from "./index";
2
+ declare const QUEUE_LIST_KEY: string;
3
+ declare const QUEUE_HIGH_KEY: string;
4
+ declare const QUEUE_LOW_KEY: string;
5
+ declare function defaultQueueKeys(): readonly [string, string, string];
6
+ declare const QUEUE_KEYS: readonly [string, string, string];
7
+ declare function queueKeyForPriority(priority?: QueuePriority): string;
8
+ export { defaultQueueKeys, QUEUE_HIGH_KEY, QUEUE_KEYS, QUEUE_LIST_KEY, QUEUE_LOW_KEY, queueKeyForPriority, };
@@ -0,0 +1,73 @@
1
+ import { RedisClient } from "bun";
2
+ import type FailedJobService from "./failedJobService";
3
+ import type { Job, Queue } from "./index";
4
+ import { type QueueJobEnvelope } from "./jobRunner";
5
+ declare const STREAM_GROUP = "strata-workers-v1";
6
+ declare function streamQueueKey(queueKey: string): string;
7
+ declare function streamDeadLetterKey(queueKey: string): string;
8
+ declare function streamRetryKey(queueKey: string): string;
9
+ interface StreamsEnqueueOptions {
10
+ /** Total lifetime including time waiting and retries, measured from Redis server time. */
11
+ timeoutMs?: number;
12
+ cancellable?: boolean;
13
+ }
14
+ declare function streamControlKey(key: string, jobId: string): string;
15
+ declare function cancelStreamJob(client: RedisClient, key: string, jobId: string): Promise<boolean>;
16
+ declare function readStreamCancellation(client: RedisClient, controlKey: string): Promise<boolean>;
17
+ declare function readStreamTime(client: RedisClient): Promise<number>;
18
+ declare function watchStreamDeadline(client: RedisClient, deadlineAtMs: number, remainingMs: number, expire: () => void, fail: (error: unknown) => void): () => void;
19
+ declare function scheduleStreamRetry(client: RedisClient, key: string, reservation: StreamReservation, envelope: QueueJobEnvelope, delayMs: number): Promise<void>;
20
+ declare function promoteStreamRetries(client: RedisClient, key: string): Promise<number>;
21
+ declare function readStreamQueueDepth(client: RedisClient, key: string): Promise<number>;
22
+ type StreamReservation = {
23
+ id: string;
24
+ owner: string;
25
+ payload: string;
26
+ };
27
+ declare function ensureStreamGroup(client: RedisClient, key: string): Promise<void>;
28
+ declare function readStreamEntry(value: unknown, owner: string): StreamReservation | null;
29
+ declare function updateStreamReservation(client: RedisClient, key: string, reservation: StreamReservation, action: "renew" | "ack" | "quarantine", controlKey?: string): Promise<boolean>;
30
+ declare function reserveStreamJob(client: RedisClient, key: string, owner: string, cursor?: string): Promise<{
31
+ reservation: StreamReservation | null;
32
+ cursor: string;
33
+ }>;
34
+ declare class RedisStreamsQueue implements Queue {
35
+ private readonly client;
36
+ constructor(redisUrl: string);
37
+ close(): void;
38
+ dispatch<TPayload extends object>(job: Job<TPayload>, payload: TPayload): Promise<void>;
39
+ enqueue<TPayload extends object>(job: Job<TPayload>, payload: TPayload, options?: StreamsEnqueueOptions): Promise<string>;
40
+ replay<TPayload extends object>(job: Job<TPayload>, payload: TPayload, jobId: string): Promise<void>;
41
+ private admit;
42
+ cancel(jobId: string): Promise<boolean>;
43
+ }
44
+ declare class RedisStreamsWorker {
45
+ private readonly failedJobs;
46
+ private readonly timeoutSeconds;
47
+ private readonly queueKeys;
48
+ private readonly client;
49
+ private readonly owner;
50
+ private readonly cursors;
51
+ private processing;
52
+ private initialized;
53
+ private running;
54
+ private stopping;
55
+ private active?;
56
+ constructor(redisUrl: string, failedJobs: FailedJobService, timeoutSeconds?: number, queueKeys?: readonly string[]);
57
+ requestStop(): void;
58
+ isRunning(): boolean;
59
+ close(): void;
60
+ private initialize;
61
+ private reserve;
62
+ processNext(): Promise<boolean>;
63
+ private processOne;
64
+ private handle;
65
+ run(): Promise<void>;
66
+ }
67
+ declare function migrateLegacyQueueToStreams(client: RedisClient, options: {
68
+ maintenance: true;
69
+ batchSize?: number;
70
+ queueKeys?: readonly string[];
71
+ }): Promise<number>;
72
+ export type { StreamReservation, StreamsEnqueueOptions };
73
+ export { cancelStreamJob, ensureStreamGroup, migrateLegacyQueueToStreams, promoteStreamRetries, RedisStreamsQueue, RedisStreamsWorker, readStreamCancellation, readStreamEntry, readStreamQueueDepth, readStreamTime, reserveStreamJob, STREAM_GROUP, scheduleStreamRetry, streamControlKey, streamDeadLetterKey, streamQueueKey, streamRetryKey, updateStreamReservation, watchStreamDeadline, };
@@ -1,6 +1,7 @@
1
1
  interface FailedJobRecord {
2
2
  id: number;
3
3
  job_name: string;
4
+ job_id?: string | null;
4
5
  payload: Record<string, unknown>;
5
6
  exception: string;
6
7
  failed_at: Date;
@@ -0,0 +1,11 @@
1
+ interface PublicTenancyOptions {
2
+ /** Application-owned lookup of approved normalized hostnames; unknown hosts return null. */
3
+ resolveTenantId(hostname: string): Promise<number | null>;
4
+ /** Exact immediate socket-peer IP addresses. The proxy must overwrite X-Forwarded-Host. */
5
+ trustedProxyAddresses?: readonly string[];
6
+ }
7
+ /** Normalize an HTTP authority for approved-domain storage and lookup, without its port. */
8
+ declare function normalizeTenantHostname(authority: string): string;
9
+ declare function createPublicTenantSelector(options: PublicTenancyOptions): (request: Request) => Promise<number>;
10
+ export type { PublicTenancyOptions };
11
+ export { createPublicTenantSelector, normalizeTenantHostname };
@@ -1,3 +1,4 @@
1
+ import { type PublicTenancyOptions } from "./publicTenantHost";
1
2
  import { type TenantContext } from "./tenantContext";
2
3
  declare const DEFAULT_TENANT: TenantContext;
3
4
  declare function resolveUserTenantId(userId: number): Promise<number>;
@@ -6,7 +7,10 @@ type TenantResolver = (id: number) => Promise<TenantContext | null>;
6
7
  interface TenantMiddlewareOptions {
7
8
  /** Load trusted application metadata after the framework selects the tenant ID. */
8
9
  resolveTenant?: TenantResolver;
10
+ publicTenancy?: PublicTenancyOptions;
9
11
  }
10
12
  declare function createTenantMiddleware(options?: TenantMiddlewareOptions): (request: Request, next: () => Promise<Response>) => Promise<Response>;
13
+ export type { PublicTenancyOptions } from "./publicTenantHost";
14
+ export { normalizeTenantHostname } from "./publicTenantHost";
11
15
  export type { TenantMiddlewareOptions, TenantResolver };
12
16
  export { auditChecksum, createTenantMiddleware, DEFAULT_TENANT, resolveUserTenantId };
@@ -379,6 +379,7 @@ class ConfigStore {
379
379
 
380
380
  // ../../src/core/contracts/serviceTokens.ts
381
381
  var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
382
+ var CORE_PUBLIC_TENANCY_TOKEN = createServiceToken("core.publicTenancy");
382
383
  var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
383
384
  var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
384
385
  var CORE_CONFIG_TOKEN = "core.config";
@@ -78,6 +78,7 @@ class ConfigStore {
78
78
 
79
79
  // ../../src/core/contracts/serviceTokens.ts
80
80
  var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
81
+ var CORE_PUBLIC_TENANCY_TOKEN = createServiceToken("core.publicTenancy");
81
82
  var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
82
83
  var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
83
84
  var CORE_CONFIG_TOKEN = "core.config";
@@ -78,6 +78,7 @@ class ConfigStore {
78
78
 
79
79
  // ../../src/core/contracts/serviceTokens.ts
80
80
  var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
81
+ var CORE_PUBLIC_TENANCY_TOKEN = createServiceToken("core.publicTenancy");
81
82
  var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
82
83
  var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
83
84
  var CORE_CONFIG_TOKEN = "core.config";
@@ -78,6 +78,7 @@ class ConfigStore {
78
78
 
79
79
  // ../../src/core/contracts/serviceTokens.ts
80
80
  var CORE_THROTTLE_QUOTA_POLICY_TOKEN = createServiceToken("core.throttleQuotaPolicy");
81
+ var CORE_PUBLIC_TENANCY_TOKEN = createServiceToken("core.publicTenancy");
81
82
  var CORE_TENANT_RESOLVER_TOKEN = createServiceToken("core.tenantResolver");
82
83
  var CORE_HTTP_CLEANUP_TOKEN = createServiceToken("core.httpCleanup");
83
84
  var CORE_CONFIG_TOKEN = "core.config";
@@ -132,6 +133,7 @@ export {
132
133
  CORE_EVENT_BUS_TOKEN,
133
134
  CORE_HTTP_CLEANUP_TOKEN,
134
135
  CORE_POLICY_GATE_TOKEN,
136
+ CORE_PUBLIC_TENANCY_TOKEN,
135
137
  CORE_QUEUE_TOKEN,
136
138
  CORE_TENANT_RESOLVER_TOKEN,
137
139
  CORE_THROTTLE_QUOTA_POLICY_TOKEN,
@@ -331,6 +331,31 @@ function getDefinedColumnEntries(table, values, options = {}) {
331
331
  return [[column, value]];
332
332
  });
333
333
  }
334
+ function buildSelectLock(table, options) {
335
+ const lock = options.lock;
336
+ if (!lock)
337
+ return "";
338
+ const driver = currentSqlDialect().driver;
339
+ if (driver === "sqlite")
340
+ throw new Error("Row locks are not supported on SQLite; use an explicit write transaction.");
341
+ if (lock.mode !== "update" && lock.mode !== "share")
342
+ throw new TypeError("Invalid row lock mode.");
343
+ if (lock.wait !== undefined && !["wait", "nowait", "skipLocked"].includes(lock.wait))
344
+ throw new TypeError("Invalid row lock wait policy.");
345
+ if (options.groupBy || options.having)
346
+ throw new Error("Row locks cannot be combined with grouped/aggregate queries.");
347
+ let target = "";
348
+ if (lock.of !== undefined) {
349
+ if (driver !== "pgsql")
350
+ throw new Error("Row lock targets are supported only on PostgreSQL.");
351
+ const tables = [table.name, ...(options.joins ?? []).map((join) => join.table)];
352
+ if (!lock.of.length || lock.of.some((name) => !tables.includes(name)))
353
+ throw new TypeError("Row lock targets must name tables in this query.");
354
+ target = ` OF ${lock.of.map(quoteIdentifier).join(", ")}`;
355
+ }
356
+ const wait = lock.wait === "nowait" ? " NOWAIT" : lock.wait === "skipLocked" ? " SKIP LOCKED" : "";
357
+ return ` FOR ${lock.mode === "update" ? "UPDATE" : "SHARE"}${target}${wait}`;
358
+ }
334
359
  function buildSelectQuery(table, options = {}, whereNodes = []) {
335
360
  const params = [];
336
361
  const columns = buildSelectList(table, options.select, params);
@@ -341,8 +366,9 @@ function buildSelectQuery(table, options = {}, whereNodes = []) {
341
366
  const orderBy = buildOrderByClause(table.name, options.orderBy ?? table.defaultOrderBy);
342
367
  const limit = buildLimitClause(options.limit);
343
368
  const offset = buildOffsetClause(options.offset);
369
+ const lock = buildSelectLock(table, options);
344
370
  return {
345
- text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}`,
371
+ text: `SELECT ${columns} FROM ${quoteIdentifier(table.name)}${joins}${clause}${groupBy}${havingClause}${orderBy}${limit}${offset}${lock}`,
346
372
  params
347
373
  };
348
374
  }
@@ -511,9 +537,52 @@ function buildDeleteByIdQuery(table, id) {
511
537
  params
512
538
  };
513
539
  }
540
+ function buildConditionalWriteQuery(table, operation, changes, options, returning) {
541
+ for (const key of [
542
+ "joins",
543
+ "groupBy",
544
+ "having",
545
+ "select",
546
+ "orderBy",
547
+ "limit",
548
+ "offset",
549
+ "lock"
550
+ ]) {
551
+ if (options[key] !== undefined)
552
+ throw new Error(`Conditional writes do not support ${key}.`);
553
+ }
554
+ const params = [];
555
+ const predicate = buildAdvancedWhereClause(table.name, options.where ?? {}, options.whereNodes ?? []);
556
+ if (!predicate.clause)
557
+ throw new Error("Conditional writes require an explicit predicate.");
558
+ if (returning !== undefined && currentSqlDialect().driver === "mysql")
559
+ throw new Error("Mutation RETURNING is not supported on MySQL.");
560
+ const columns = returning ?? [table.primaryKey];
561
+ if (!columns.length || columns.some((column) => !table.columns.includes(column)))
562
+ throw new Error("RETURNING columns must be nonempty declared table columns.");
563
+ const deletedAt = resolveSoftDeleteColumn(table);
564
+ const values = operation === "delete" && deletedAt ? { [deletedAt]: new Date } : changes;
565
+ let prefix;
566
+ if (operation === "update" || deletedAt) {
567
+ const entries = Object.entries(values).filter(([, value]) => value !== undefined);
568
+ if (!entries.length)
569
+ throw new Error("Conditional updates require at least one value.");
570
+ if (entries.some(([column]) => column === table.primaryKey || !table.columns.includes(column)))
571
+ throw new Error("Conditional updates require declared non-primary-key columns.");
572
+ const assignments = entries.map(([column, value]) => `${quoteIdentifier(column)} = ${pushParam(params, value)}`);
573
+ prefix = `UPDATE ${quoteIdentifier(table.name)} SET ${assignments.join(", ")}`;
574
+ } else
575
+ prefix = `DELETE FROM ${quoteIdentifier(table.name)}`;
576
+ const { clause } = buildQueryWhereClause(table, options, options.whereNodes ?? [], params);
577
+ return {
578
+ text: prefix + clause + returningSuffix(columns.map(quoteIdentifier).join(", ")),
579
+ params
580
+ };
581
+ }
514
582
  export {
515
583
  assertSafeProjectionExpression,
516
584
  buildAdvancedWhereClause,
585
+ buildConditionalWriteQuery,
517
586
  buildCountQuery,
518
587
  buildDeleteByIdQuery,
519
588
  buildGroupedCountQuery,