@getstrata/core 2.2.13 → 2.2.15
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.
- package/CHANGELOG.md +41 -14
- package/dist/core/database/baseRepository.d.ts +1 -0
- package/dist/core/database/defaultConnection.d.ts +2 -1
- package/dist/core/database/relationQuery.d.ts +4 -0
- package/dist/core/http/index.d.ts +2 -2
- package/dist/core/http/route.d.ts +64 -0
- package/dist/core/queue/redisQueue.d.ts +5 -1
- package/dist/core/queue/redisStreams.d.ts +8 -2
- package/dist/core/view/index.d.ts +1 -1
- package/dist/core/view/viewRenderer.d.ts +7 -1
- package/dist/entries/http/corsMiddleware.js +10 -1
- package/dist/entries/http/route.js +45 -1
- package/dist/entries/queue/createAppQueue.js +202 -92
- package/dist/entries/queue/publicQueue.js +202 -92
- package/dist/entries/queue/queueMetrics.js +202 -92
- package/dist/entries/queue/redisQueue.js +203 -92
- package/dist/framework/public-api.d.ts +4 -4
- package/dist/index.js +430 -144
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
# @getstrata/core changelog
|
|
2
2
|
|
|
3
|
+
## 2.2.15
|
|
4
|
+
|
|
5
|
+
- Batch legacy list-queue priority polling while retaining recovery and ownership contracts.
|
|
6
|
+
- Batch dialect-portable pivot writes with framework transaction/savepoint composition and consistent numeric/string ID toggle semantics. Noncanonical or textual IDs use sequential database comparisons to preserve coercion and collation behavior.
|
|
7
|
+
- New Redis scaffolds explicitly select Streams; existing unset transport settings continue to select lists.
|
|
8
|
+
- Generate the SQL outbox migration, listener registry and managed worker command. Durable publication and cache-invalidation activation remain explicit.
|
|
9
|
+
|
|
10
|
+
- Primary-key ascending `chunk` queries reuse the existing keyset walk, including default primary-key ordering; projections, joins, grouping, locks and other orders retain OFFSET behavior.
|
|
11
|
+
- Empty filtered pagination totals skip the row query. Nonempty page-number pagination preserves OFFSET semantics and its count-plus-row-query budget; explicit cursor/keyset APIs remain available for deep traversal.
|
|
12
|
+
- Independent hashed write casts run concurrently. Deterministic regressions prove concurrent start, complete awaiting and failure before model observers or SQL writes; hydration and existing-hash behavior are preserved.
|
|
13
|
+
- Streams workers poll retry promotion, pending recovery and new delivery across priorities in one bounded Redis script, preserving priority, ownership fencing and at-least-once delivery contracts.
|
|
14
|
+
- Clarify historical 2.1.0–2.2.13 release notes and database/queue documentation. Remove the consolidated 1.x-to-2.x migration guide; existing projects are upgraded individually.
|
|
15
|
+
|
|
16
|
+
## 2.2.14
|
|
17
|
+
|
|
18
|
+
Complete the scoped HTML rendering starter adoption: generated apps inject ViewRenderer through createViewProvider instead of copying renderPage; welcome pages opt into HTMX fragments with full-page fallbacks, auth retains existing contracts, and CORS preserves representation Vary headers. Includes generator, runtime auth and cache-header regressions.
|
|
19
|
+
|
|
20
|
+
Add optional typed resourceRoutes and strict mergeRouteMaps composition, preserving native Bun routing and app-owned middleware. Add explicit ViewRenderer.namespace prefixes with unchanged layout/Response/HTMX contracts; generated auth controllers use the namespace. Includes native HTTP, compile-time, rendering isolation and generator regressions.
|
|
21
|
+
|
|
3
22
|
## 2.2.13
|
|
4
23
|
|
|
5
|
-
-
|
|
24
|
+
- Add a typed per-app HTML renderer on top of the native response path.
|
|
25
|
+
- Add explicit HTMX page and fragment responses. A fragment can fall back to the full page. Layout and auth response contracts stay the same.
|
|
6
26
|
|
|
7
27
|
## 2.2.12
|
|
8
28
|
|
|
@@ -24,42 +44,47 @@ Add independently interruptible read-only SQLite and MySQL observations; extend
|
|
|
24
44
|
|
|
25
45
|
## 2.2.9
|
|
26
46
|
|
|
27
|
-
-
|
|
47
|
+
- Pool checkout can be cancelled. `reserve({ signal })` on the native driver aborts while it is still waiting for a connection. Drivers without that method are not given a fake one.
|
|
28
48
|
|
|
29
49
|
## 2.2.8
|
|
30
50
|
|
|
31
|
-
- Add bounded read-only Postgres outbox metrics
|
|
51
|
+
- Add bounded, read-only Postgres outbox metrics.
|
|
52
|
+
- Add bounded, opt-in Redis queue metrics. List and Streams depth stay separate.
|
|
32
53
|
|
|
33
54
|
## 2.2.7
|
|
34
55
|
|
|
35
|
-
-
|
|
56
|
+
- `ORDER BY` accepts explicit `nulls: "first" | "last"`. Postgres and SQLite emit `NULLS FIRST` / `NULLS LAST`. MySQL has no `NULLS FIRST` / `NULLS LAST` clause, so it orders nulls with an `IS NULL` discriminator and leaves the rest of the sort unchanged.
|
|
36
57
|
|
|
37
58
|
## 2.2.6
|
|
38
59
|
|
|
39
|
-
-
|
|
60
|
+
- The CLI, when it supervises the app or worker as a child, forwards shutdown signals to that child and waits for it. The container launcher no longer exits while the child is still running.
|
|
40
61
|
|
|
41
62
|
## 2.2.5
|
|
42
63
|
|
|
43
|
-
-
|
|
64
|
+
- Container readiness no longer means the API is admitting traffic. Database readiness no longer provisions roles or creates a missing database. A failed configured admin connection does not fall back to a generated password.
|
|
44
65
|
|
|
45
66
|
## 2.2.4
|
|
46
67
|
|
|
47
|
-
-
|
|
68
|
+
- Composite keyset pages on Postgres seek with a row comparison (`(a, b) > (cursor)`), so the planner can start at the index bound instead of expanding the cursor into a chain of OR predicates.
|
|
48
69
|
|
|
49
70
|
## 2.2.3
|
|
50
71
|
|
|
51
|
-
-
|
|
72
|
+
- Add scoped composite keyset pagination. The order must end with the primary key. Global scopes and caller filters stay in force across the cursor. Joins, grouping, and offset queries are rejected.
|
|
52
73
|
|
|
53
74
|
## 2.2.2
|
|
54
75
|
|
|
55
|
-
-
|
|
76
|
+
- Schema `bigId` emits an explicit 64-bit generated identity.
|
|
77
|
+
- Repository writes can be conditional and set-based.
|
|
78
|
+
- Repositories and models can take a typed row lock inside the current transaction.
|
|
79
|
+
- Generated Postgres query handles keep tagged-SQL typing.
|
|
56
80
|
|
|
57
81
|
## 2.2.1
|
|
58
82
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
- Opt-in Redis Streams (`QUEUE_REDIS_TRANSPORT=streams`). Lists stay the default. Streams adds consumer-group recovery, persisted retry schedules, total job deadlines, opt-in cancellation, and a stable job id across replay. Conversion from lists is a stopped maintenance step.
|
|
84
|
+
- Failed jobs stay recorded until retry admission succeeds. Manual replay keeps `failed_job.job_id` when the row has one.
|
|
85
|
+
- Public reads and tenant host selection are separate. Unknown hosts fail closed. Forwarded hosts are accepted only from configured trusted proxies.
|
|
86
|
+
- Generated apps can provision one initial administrator with `auth:provision-admin` and a one-time claim migration. Demo seeding stays explicit and is rejected in production and staging.
|
|
87
|
+
- Quota and in-memory throttle docs match the code: memory throttles stay process-local, and a Redis failure does not fall back to them.
|
|
63
88
|
|
|
64
89
|
## 2.2.0
|
|
65
90
|
|
|
@@ -96,7 +121,9 @@ Bounded memory throttling.
|
|
|
96
121
|
|
|
97
122
|
## 2.1.0
|
|
98
123
|
|
|
99
|
-
-
|
|
124
|
+
- Generated apps reuse the shared config store and base repositories instead of a copied stack.
|
|
125
|
+
- Password casts are awaited before the write reaches SQL or observers. Hydration does not hash.
|
|
126
|
+
- Model queries keep the concrete model type in their results. Service tokens, route handlers, and the starter's generated infrastructure were tightened in the same release.
|
|
100
127
|
|
|
101
128
|
## 2.0.9
|
|
102
129
|
|
|
@@ -69,6 +69,7 @@ declare class BaseRepository<TEntity extends object, PrimaryKey extends keyof TE
|
|
|
69
69
|
softDeleteById(id: TEntity[PrimaryKey]): Promise<boolean>;
|
|
70
70
|
forceDeleteById(id: TEntity[PrimaryKey]): Promise<boolean>;
|
|
71
71
|
restoreById(id: TEntity[PrimaryKey]): Promise<TEntity | null>;
|
|
72
|
+
private primaryKeySeekDirection;
|
|
72
73
|
withConnection(connection: DatabaseConnection): this;
|
|
73
74
|
getConnection(): DatabaseConnection;
|
|
74
75
|
getTable(): TableDefinition<TEntity, PrimaryKey>;
|
|
@@ -2,5 +2,6 @@ import type { SqlDatabaseConnection } from "./baseRepository";
|
|
|
2
2
|
declare function registerDefaultDatabasePool(connection: SqlDatabaseConnection): void;
|
|
3
3
|
declare function getDefaultDatabasePool(): SqlDatabaseConnection;
|
|
4
4
|
declare function getDefaultDatabaseQuery(): SqlDatabaseConnection;
|
|
5
|
+
declare function isDefaultDatabaseConnection(connection: unknown): boolean;
|
|
5
6
|
declare function resetDefaultDatabasePoolForTests(): void;
|
|
6
|
-
export { getDefaultDatabasePool, getDefaultDatabaseQuery, registerDefaultDatabasePool, resetDefaultDatabasePoolForTests, };
|
|
7
|
+
export { getDefaultDatabasePool, getDefaultDatabaseQuery, isDefaultDatabaseConnection, registerDefaultDatabasePool, resetDefaultDatabasePoolForTests, };
|
|
@@ -112,6 +112,7 @@ declare class BelongsToManyRelationQuery<TParent extends object, ParentKey exten
|
|
|
112
112
|
hydrateEager(row: Record<string, unknown>, alias: string): unknown;
|
|
113
113
|
toExistsClause(parentTable: string): ExistsClause;
|
|
114
114
|
private connection;
|
|
115
|
+
private atomically;
|
|
115
116
|
get(): Promise<RelatedRecord[]>;
|
|
116
117
|
first(): Promise<RelatedRecord | null>;
|
|
117
118
|
count(): Promise<number>;
|
|
@@ -123,6 +124,9 @@ declare class BelongsToManyRelationQuery<TParent extends object, ParentKey exten
|
|
|
123
124
|
attach(ids: unknown | readonly unknown[]): Promise<void>;
|
|
124
125
|
toggle(ids: unknown | readonly unknown[]): Promise<void>;
|
|
125
126
|
detach(ids?: unknown | readonly unknown[]): Promise<void>;
|
|
127
|
+
private toggleOne;
|
|
128
|
+
private insertPivotRows;
|
|
129
|
+
private deletePivotRows;
|
|
126
130
|
sync(ids: readonly unknown[]): Promise<void>;
|
|
127
131
|
create(attributes?: Record<string, unknown>): Promise<RelatedRecord>;
|
|
128
132
|
}
|
|
@@ -14,8 +14,8 @@ export { parseMultipartUpload, sanitizeUploadFileName, validateUploadFile, } fro
|
|
|
14
14
|
export { createRequireAuthMiddleware } from "./requireAuthMiddleware";
|
|
15
15
|
export { serializeDate, toPaginatedResourceCollection, toResourceCollection } from "./resources";
|
|
16
16
|
export { createdResponse, errorResponse, jsonResponse, noContentResponse, withErrorHandling, } from "./response";
|
|
17
|
-
export type { RouteRequest } from "./route";
|
|
18
|
-
export { getRouteParams } from "./route";
|
|
17
|
+
export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap, RouteRequest, } from "./route";
|
|
18
|
+
export { getRouteParams, mergeRouteMaps, resourceRoutes } from "./route";
|
|
19
19
|
export { withMiddleware } from "./routeMiddleware";
|
|
20
20
|
export { bindRouteModel } from "./routeModelBinding";
|
|
21
21
|
export { securedBindRouteModel, securedBindRouteModelByKey, } from "./securedRouteModelBinding";
|
|
@@ -1,3 +1,67 @@
|
|
|
1
|
+
import type { RouteHandler } from "./middleware";
|
|
2
|
+
type ResourceMemberHandler = RouteHandler<RouteRequest<{
|
|
3
|
+
id: string;
|
|
4
|
+
}>>;
|
|
5
|
+
interface ResourceActions {
|
|
6
|
+
index?: RouteHandler;
|
|
7
|
+
create?: RouteHandler;
|
|
8
|
+
store?: RouteHandler;
|
|
9
|
+
show?: ResourceMemberHandler;
|
|
10
|
+
edit?: ResourceMemberHandler;
|
|
11
|
+
update?: ResourceMemberHandler;
|
|
12
|
+
destroy?: ResourceMemberHandler;
|
|
13
|
+
}
|
|
14
|
+
type ResourceRouteMap<P extends string, A extends ResourceActions> = (A extends {
|
|
15
|
+
index: infer H;
|
|
16
|
+
} ? {
|
|
17
|
+
[K in P]: {
|
|
18
|
+
GET: H;
|
|
19
|
+
};
|
|
20
|
+
} : Record<never, never>) & (A extends {
|
|
21
|
+
store: infer H;
|
|
22
|
+
} ? {
|
|
23
|
+
[K in P]: {
|
|
24
|
+
POST: H;
|
|
25
|
+
};
|
|
26
|
+
} : Record<never, never>) & (A extends {
|
|
27
|
+
create: infer H;
|
|
28
|
+
} ? {
|
|
29
|
+
[K in `${P}/create`]: {
|
|
30
|
+
GET: H;
|
|
31
|
+
};
|
|
32
|
+
} : Record<never, never>) & (A extends {
|
|
33
|
+
show: infer H;
|
|
34
|
+
} ? {
|
|
35
|
+
[K in `${P}/:id`]: {
|
|
36
|
+
GET: H;
|
|
37
|
+
};
|
|
38
|
+
} : Record<never, never>) & (A extends {
|
|
39
|
+
edit: infer H;
|
|
40
|
+
} ? {
|
|
41
|
+
[K in `${P}/:id/edit`]: {
|
|
42
|
+
GET: H;
|
|
43
|
+
};
|
|
44
|
+
} : Record<never, never>) & (A extends {
|
|
45
|
+
update: infer H;
|
|
46
|
+
} ? {
|
|
47
|
+
[K in `${P}/:id`]: {
|
|
48
|
+
PUT: H;
|
|
49
|
+
PATCH: H;
|
|
50
|
+
};
|
|
51
|
+
} : Record<never, never>) & (A extends {
|
|
52
|
+
destroy: infer H;
|
|
53
|
+
} ? {
|
|
54
|
+
[K in `${P}/:id`]: {
|
|
55
|
+
DELETE: H;
|
|
56
|
+
};
|
|
57
|
+
} : Record<never, never>);
|
|
58
|
+
/** Expand only explicitly supplied actions into native Bun routes. */
|
|
59
|
+
declare function resourceRoutes<const P extends string, const A extends ResourceActions>(path: P, actions: A & Record<Exclude<keyof A, keyof ResourceActions>, never>): ResourceRouteMap<P, A>;
|
|
60
|
+
type Intersection<U> = (U extends unknown ? (value: U) => void : never) extends (value: infer I) => void ? I : never;
|
|
61
|
+
/** Strict composition: duplicate paths fail rather than silently replacing handlers. */
|
|
62
|
+
declare function mergeRouteMaps<const T extends readonly Record<string, unknown>[]>(...maps: T): Intersection<T[number]>;
|
|
63
|
+
export type { ResourceActions, ResourceMemberHandler, ResourceRouteMap };
|
|
64
|
+
export { mergeRouteMaps, resourceRoutes };
|
|
1
65
|
type RouteRequest<TParams extends Record<string, string>> = Request & {
|
|
2
66
|
params: TParams;
|
|
3
67
|
};
|
|
@@ -15,6 +15,10 @@ type QueueReservation = {
|
|
|
15
15
|
declare function queueInvalidKey(queueKey: string): string;
|
|
16
16
|
declare function reserveQueueJob(client: RedisClient, queueKey: string, owner: string): Promise<QueueReservation | null>;
|
|
17
17
|
declare function updateQueueReservation(client: RedisClient, queueKey: string, reservation: QueueReservation, action: "renew" | "ack" | "quarantine"): Promise<boolean>;
|
|
18
|
+
declare function pollListPriorities(client: RedisClient, queueKeys: readonly string[], owner: string, reservationId: string, reclaim: boolean): Promise<{
|
|
19
|
+
queueKey: string;
|
|
20
|
+
reservation: QueueReservation;
|
|
21
|
+
} | null>;
|
|
18
22
|
declare function reclaimExpiredQueueReservations(client: RedisClient, queueKeys?: readonly string[]): Promise<number>;
|
|
19
23
|
declare class RedisQueue implements Queue {
|
|
20
24
|
private readonly client;
|
|
@@ -42,4 +46,4 @@ declare function countPendingQueueJobs(redisUrl: string): Promise<number>;
|
|
|
42
46
|
export type { StreamReservation, StreamsEnqueueOptions } from "./redisStreams";
|
|
43
47
|
export { migrateLegacyQueueToStreams, RedisStreamsQueue, RedisStreamsWorker, STREAM_GROUP, streamDeadLetterKey, streamQueueKey, updateStreamReservation, } from "./redisStreams";
|
|
44
48
|
export type { QueueJobEnvelope, QueueReservation };
|
|
45
|
-
export { countPendingQueueJobs, parseQueueJobEnvelope, QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, QueueWorker, queueInvalidKey, queueKeyForPriority, queueProcessingKey, queueProcessingLeaseKey, RedisQueue, reclaimExpiredQueueReservations, reservationExpired, reserveQueueJob, updateQueueReservation, };
|
|
49
|
+
export { countPendingQueueJobs, parseQueueJobEnvelope, pollListPriorities, QUEUE_HIGH_KEY, QUEUE_LIST_KEY, QUEUE_LOW_KEY, QueueWorker, queueInvalidKey, queueKeyForPriority, queueProcessingKey, queueProcessingLeaseKey, RedisQueue, reclaimExpiredQueueReservations, reservationExpired, reserveQueueJob, updateQueueReservation, };
|
|
@@ -18,6 +18,13 @@ declare function readStreamTime(client: RedisClient): Promise<number>;
|
|
|
18
18
|
declare function watchStreamDeadline(client: RedisClient, deadlineAtMs: number, remainingMs: number, expire: () => void, fail: (error: unknown) => void): () => void;
|
|
19
19
|
declare function scheduleStreamRetry(client: RedisClient, key: string, reservation: StreamReservation, envelope: QueueJobEnvelope, delayMs: number): Promise<void>;
|
|
20
20
|
declare function promoteStreamRetries(client: RedisClient, key: string): Promise<number>;
|
|
21
|
+
declare function pollStreamPriorities(client: RedisClient, queueKeys: readonly string[], owner: string, cursors: readonly string[]): Promise<{
|
|
22
|
+
cursors: string[];
|
|
23
|
+
hit: {
|
|
24
|
+
index: number;
|
|
25
|
+
reservation: StreamReservation;
|
|
26
|
+
} | null;
|
|
27
|
+
}>;
|
|
21
28
|
declare function readStreamQueueDepth(client: RedisClient, key: string): Promise<number>;
|
|
22
29
|
type StreamReservation = {
|
|
23
30
|
id: string;
|
|
@@ -58,7 +65,6 @@ declare class RedisStreamsWorker {
|
|
|
58
65
|
isRunning(): boolean;
|
|
59
66
|
close(): void;
|
|
60
67
|
private initialize;
|
|
61
|
-
private reserve;
|
|
62
68
|
processNext(): Promise<boolean>;
|
|
63
69
|
private processOne;
|
|
64
70
|
private handle;
|
|
@@ -70,4 +76,4 @@ declare function migrateLegacyQueueToStreams(client: RedisClient, options: {
|
|
|
70
76
|
queueKeys?: readonly string[];
|
|
71
77
|
}): Promise<number>;
|
|
72
78
|
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, };
|
|
79
|
+
export { cancelStreamJob, ensureStreamGroup, migrateLegacyQueueToStreams, pollStreamPriorities, promoteStreamRetries, RedisStreamsQueue, RedisStreamsWorker, readStreamCancellation, readStreamEntry, readStreamQueueDepth, readStreamTime, reserveStreamJob, STREAM_GROUP, scheduleStreamRetry, streamControlKey, streamDeadLetterKey, streamQueueKey, streamRetryKey, updateStreamReservation, watchStreamDeadline, };
|
|
@@ -2,7 +2,7 @@ export type { LayoutDataResolver, RenderOptions } from "./etaViewEngine";
|
|
|
2
2
|
export { DEFAULT_VIEWS_DIRECTORY, EtaViewEngine } from "./etaViewEngine";
|
|
3
3
|
export { htmlResponse, isHtmxRequest, redirectResponse, rssResponse, textResponse, xmlResponse, } from "./htmlResponse";
|
|
4
4
|
export type { ViewEngine } from "./viewEngine";
|
|
5
|
-
export type { HtmxViewResponseOptions, ViewResponseOptions } from "./viewRenderer";
|
|
5
|
+
export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions, } from "./viewRenderer";
|
|
6
6
|
export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer } from "./viewRenderer";
|
|
7
7
|
export type { WebErrorViewInput, WebErrorViewOptions } from "./webErrorView";
|
|
8
8
|
export { configureWebErrorView, errorTemplateName, htmlErrorResponse, notFoundHtmlResponse, renderKernelErrorChrome, renderWebErrorHtml, } from "./webErrorView";
|
|
@@ -10,15 +10,21 @@ interface HtmxViewResponseOptions extends ViewResponseOptions {
|
|
|
10
10
|
/** Optional fragment template; otherwise render the page template without its layout. */
|
|
11
11
|
fragment?: string;
|
|
12
12
|
}
|
|
13
|
+
interface NamespacedViewRenderer {
|
|
14
|
+
render: ViewRenderer["render"];
|
|
15
|
+
renderHtmx: ViewRenderer["renderHtmx"];
|
|
16
|
+
}
|
|
13
17
|
/** App-owned renderer; request data stays in the engine's async resolver. */
|
|
14
18
|
declare class ViewRenderer {
|
|
15
19
|
private readonly engine;
|
|
16
20
|
private readonly defaultLayout?;
|
|
17
21
|
constructor(engine: ViewEngine, defaultLayout?: string | false | undefined);
|
|
22
|
+
/** Explicit template prefix; layout names and response contracts remain unchanged. */
|
|
23
|
+
namespace(prefix: string): NamespacedViewRenderer;
|
|
18
24
|
render(name: string, data?: Record<string, unknown>, options?: ViewResponseOptions): Promise<Response>;
|
|
19
25
|
/** Opt-in representation selection; auth/CSRF/tenancy remain the route's responsibility. */
|
|
20
26
|
renderHtmx(name: string, data?: Record<string, unknown>, options?: HtmxViewResponseOptions): Promise<Response>;
|
|
21
27
|
}
|
|
22
28
|
declare const CORE_VIEW_RENDERER_TOKEN: import("../contracts/container").ServiceToken<ViewRenderer>;
|
|
23
|
-
export type { HtmxViewResponseOptions, ViewResponseOptions };
|
|
29
|
+
export type { HtmxViewResponseOptions, NamespacedViewRenderer, ViewResponseOptions };
|
|
24
30
|
export { CORE_VIEW_RENDERER_TOKEN, ViewRenderer };
|
|
@@ -141,7 +141,16 @@ function createCorsMiddleware(options = {}) {
|
|
|
141
141
|
const response = await next();
|
|
142
142
|
const headers = new Headers(response.headers);
|
|
143
143
|
for (const [key, value] of buildCorsHeaders(request, captured)) {
|
|
144
|
-
|
|
144
|
+
if (key === "vary") {
|
|
145
|
+
const fields = (headers.get("Vary") ?? "").split(",").map((field) => field.trim()).filter(Boolean);
|
|
146
|
+
if (fields.includes("*"))
|
|
147
|
+
continue;
|
|
148
|
+
if (!fields.some((field) => field.toLowerCase() === "origin"))
|
|
149
|
+
fields.push(value);
|
|
150
|
+
headers.set("Vary", fields.join(", "));
|
|
151
|
+
} else {
|
|
152
|
+
headers.set(key, value);
|
|
153
|
+
}
|
|
145
154
|
}
|
|
146
155
|
return new Response(response.body, {
|
|
147
156
|
status: response.status,
|
|
@@ -1,8 +1,52 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// ../../src/core/http/route.ts
|
|
3
|
+
function resourceRoutes(path, actions) {
|
|
4
|
+
if (!/^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/.test(path) || path.split("/").some((part) => part === "." || part === "..")) {
|
|
5
|
+
throw new TypeError("Resource path must be a static absolute path without a trailing slash.");
|
|
6
|
+
}
|
|
7
|
+
const routes = {};
|
|
8
|
+
const definitions = {
|
|
9
|
+
index: [path, ["GET"]],
|
|
10
|
+
create: [`${path}/create`, ["GET"]],
|
|
11
|
+
store: [path, ["POST"]],
|
|
12
|
+
show: [`${path}/:id`, ["GET"]],
|
|
13
|
+
edit: [`${path}/:id/edit`, ["GET"]],
|
|
14
|
+
update: [`${path}/:id`, ["PUT", "PATCH"]],
|
|
15
|
+
destroy: [`${path}/:id`, ["DELETE"]]
|
|
16
|
+
};
|
|
17
|
+
for (const [action, handler] of Object.entries(actions)) {
|
|
18
|
+
if (!Object.hasOwn(definitions, action) || typeof handler !== "function") {
|
|
19
|
+
throw new TypeError(`Invalid resource action: ${action}`);
|
|
20
|
+
}
|
|
21
|
+
const [route, methods] = definitions[action];
|
|
22
|
+
const methodMap = routes[route] ?? {};
|
|
23
|
+
routes[route] = methodMap;
|
|
24
|
+
for (const method of methods)
|
|
25
|
+
methodMap[method] = handler;
|
|
26
|
+
}
|
|
27
|
+
return routes;
|
|
28
|
+
}
|
|
29
|
+
function mergeRouteMaps(...maps) {
|
|
30
|
+
const routes = {};
|
|
31
|
+
for (const map of maps) {
|
|
32
|
+
for (const [path, handler] of Object.entries(map)) {
|
|
33
|
+
if (Object.hasOwn(routes, path))
|
|
34
|
+
throw new Error(`Duplicate route path: ${path}`);
|
|
35
|
+
Object.defineProperty(routes, path, {
|
|
36
|
+
value: handler,
|
|
37
|
+
enumerable: true,
|
|
38
|
+
writable: true,
|
|
39
|
+
configurable: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return routes;
|
|
44
|
+
}
|
|
3
45
|
function getRouteParams(request) {
|
|
4
46
|
return request.params;
|
|
5
47
|
}
|
|
6
48
|
export {
|
|
7
|
-
getRouteParams
|
|
49
|
+
getRouteParams,
|
|
50
|
+
mergeRouteMaps,
|
|
51
|
+
resourceRoutes
|
|
8
52
|
};
|