@htlkg/astro 0.0.1 → 0.0.3
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/README.md +24 -8
- package/dist/chunk-2GML443T.js +273 -0
- package/dist/chunk-2GML443T.js.map +1 -0
- package/dist/{chunk-Z2ZAL7KX.js → chunk-UBF5F2RG.js} +1 -1
- package/dist/{chunk-Z2ZAL7KX.js.map → chunk-UBF5F2RG.js.map} +1 -1
- package/dist/chunk-XOY5BM3N.js +151 -0
- package/dist/chunk-XOY5BM3N.js.map +1 -0
- package/dist/htlkg/config.js +1 -1
- package/dist/htlkg/index.js +1 -1
- package/dist/index.js +126 -14
- package/dist/index.js.map +1 -1
- package/dist/middleware/index.js +27 -28
- package/dist/middleware/index.js.map +1 -1
- package/dist/utils/index.js +31 -12
- package/dist/vue-app-setup.js +47 -0
- package/dist/vue-app-setup.js.map +1 -0
- package/package.json +60 -26
- package/src/auth/auth.md +77 -0
- package/src/components/Island.astro +56 -0
- package/src/components/components.md +79 -0
- package/src/factories/createListPage.ts +290 -0
- package/src/factories/index.ts +16 -0
- package/src/htlkg/config.ts +10 -0
- package/src/htlkg/htlkg.md +63 -0
- package/src/htlkg/index.ts +49 -157
- package/src/index.ts +3 -0
- package/src/layouts/AdminLayout.astro +103 -92
- package/src/layouts/layouts.md +87 -0
- package/src/middleware/auth.ts +42 -0
- package/src/middleware/middleware.md +82 -0
- package/src/middleware/route-guards.ts +4 -28
- package/src/patterns/patterns.md +104 -0
- package/src/utils/filters.ts +320 -0
- package/src/utils/index.ts +8 -2
- package/src/utils/params.ts +260 -0
- package/src/utils/utils.md +86 -0
- package/src/vue-app-setup.ts +21 -28
- package/dist/chunk-WLOFOVCL.js +0 -210
- package/dist/chunk-WLOFOVCL.js.map +0 -1
- package/dist/chunk-ZQ4XMJH7.js +0 -1
- package/dist/chunk-ZQ4XMJH7.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,16 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
Astro integration, layouts, page patterns, middleware, and utilities for Hotelinking applications.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Modules
|
|
6
|
+
|
|
7
|
+
### [htlkg Integration](src/htlkg/htlkg.md)
|
|
8
|
+
Zero-config Astro integration with automatic middleware, route guards, and virtual modules.
|
|
9
|
+
|
|
10
|
+
### [Layouts](src/layouts/layouts.md)
|
|
11
|
+
Page layouts: AdminLayout, BrandLayout, AuthLayout, PublicLayout.
|
|
12
|
+
|
|
13
|
+
### [Middleware](src/middleware/middleware.md)
|
|
14
|
+
Authentication middleware and route guards: `requireAuth`, `requireAdminAccess`, `requireBrandAccess`.
|
|
15
|
+
|
|
16
|
+
### [Components](src/components/components.md)
|
|
17
|
+
Astro components: Sidebar, Topbar, PageHeader, Island.
|
|
6
18
|
|
|
7
|
-
|
|
19
|
+
### [Patterns](src/patterns/patterns.md)
|
|
20
|
+
Reusable page patterns for admin and brand pages.
|
|
21
|
+
|
|
22
|
+
### [Utils](src/utils/utils.md)
|
|
23
|
+
SSR, hydration, and static rendering helpers.
|
|
24
|
+
|
|
25
|
+
### [Auth](src/auth/auth.md)
|
|
26
|
+
Authentication pages and components: LoginPage, LoginForm.
|
|
27
|
+
|
|
28
|
+
## Overview
|
|
8
29
|
|
|
9
|
-
|
|
10
|
-
- **Layouts** - AdminLayout, BrandLayout, AuthLayout, PublicLayout
|
|
11
|
-
- **Page Patterns** - Reusable page templates for common scenarios
|
|
12
|
-
- **Middleware** - Authentication and route guards
|
|
13
|
-
- **Components** - Astro components (Sidebar, Topbar, PageHeader)
|
|
14
|
-
- **Utilities** - SSR, hydration, and static rendering helpers
|
|
30
|
+
`@htlkg/astro` consolidates all Astro-specific functionality into a single package.
|
|
15
31
|
|
|
16
32
|
## Installation
|
|
17
33
|
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
// src/utils/params.ts
|
|
2
|
+
function parseListParams(url, config = {}) {
|
|
3
|
+
const searchParams = url.searchParams;
|
|
4
|
+
const {
|
|
5
|
+
defaultPageSize = 25,
|
|
6
|
+
defaultSort = { key: "id", order: "asc" },
|
|
7
|
+
filterableFields = [],
|
|
8
|
+
maxPageSize = 100
|
|
9
|
+
} = config;
|
|
10
|
+
let page = parseInt(searchParams.get("page") ?? "1", 10);
|
|
11
|
+
if (Number.isNaN(page) || page < 1) page = 1;
|
|
12
|
+
let pageSize = parseInt(searchParams.get("pageSize") ?? String(defaultPageSize), 10);
|
|
13
|
+
if (Number.isNaN(pageSize) || pageSize < 1) pageSize = defaultPageSize;
|
|
14
|
+
if (pageSize > maxPageSize) pageSize = maxPageSize;
|
|
15
|
+
const sortKey = searchParams.get("sortKey") ?? defaultSort.key;
|
|
16
|
+
const sortOrderParam = searchParams.get("sortOrder");
|
|
17
|
+
const sortOrder = sortOrderParam === "asc" || sortOrderParam === "desc" ? sortOrderParam : defaultSort.order;
|
|
18
|
+
const search = searchParams.get("search") ?? void 0;
|
|
19
|
+
const filters = {};
|
|
20
|
+
for (const field of filterableFields) {
|
|
21
|
+
const value = searchParams.get(field.key);
|
|
22
|
+
if (value === null || value === "") continue;
|
|
23
|
+
switch (field.type) {
|
|
24
|
+
case "number":
|
|
25
|
+
const numValue = parseFloat(value);
|
|
26
|
+
if (!Number.isNaN(numValue)) {
|
|
27
|
+
filters[field.key] = numValue;
|
|
28
|
+
}
|
|
29
|
+
break;
|
|
30
|
+
case "boolean":
|
|
31
|
+
filters[field.key] = value === "true" || value === "1";
|
|
32
|
+
break;
|
|
33
|
+
case "date":
|
|
34
|
+
const dateValue = new Date(value);
|
|
35
|
+
if (!Number.isNaN(dateValue.getTime())) {
|
|
36
|
+
filters[field.key] = dateValue;
|
|
37
|
+
}
|
|
38
|
+
break;
|
|
39
|
+
case "select":
|
|
40
|
+
if (!field.options || field.options.includes(value)) {
|
|
41
|
+
filters[field.key] = value;
|
|
42
|
+
}
|
|
43
|
+
break;
|
|
44
|
+
case "text":
|
|
45
|
+
default:
|
|
46
|
+
filters[field.key] = value;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
page,
|
|
52
|
+
pageSize,
|
|
53
|
+
sortKey,
|
|
54
|
+
sortOrder,
|
|
55
|
+
search,
|
|
56
|
+
filters
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function buildListQueryString(params, defaults) {
|
|
60
|
+
const searchParams = new URLSearchParams();
|
|
61
|
+
if (params.page !== void 0 && params.page !== 1) {
|
|
62
|
+
searchParams.set("page", String(params.page));
|
|
63
|
+
}
|
|
64
|
+
if (params.pageSize !== void 0 && params.pageSize !== (defaults?.pageSize ?? 25)) {
|
|
65
|
+
searchParams.set("pageSize", String(params.pageSize));
|
|
66
|
+
}
|
|
67
|
+
if (params.sortKey !== void 0 && params.sortKey !== (defaults?.sortKey ?? "id")) {
|
|
68
|
+
searchParams.set("sortKey", params.sortKey);
|
|
69
|
+
}
|
|
70
|
+
if (params.sortOrder !== void 0 && params.sortOrder !== (defaults?.sortOrder ?? "asc")) {
|
|
71
|
+
searchParams.set("sortOrder", params.sortOrder);
|
|
72
|
+
}
|
|
73
|
+
if (params.search) {
|
|
74
|
+
searchParams.set("search", params.search);
|
|
75
|
+
}
|
|
76
|
+
if (params.filters) {
|
|
77
|
+
for (const [key, value] of Object.entries(params.filters)) {
|
|
78
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
79
|
+
if (value instanceof Date) {
|
|
80
|
+
searchParams.set(key, value.toISOString());
|
|
81
|
+
} else if (typeof value === "boolean") {
|
|
82
|
+
searchParams.set(key, value ? "true" : "false");
|
|
83
|
+
} else {
|
|
84
|
+
searchParams.set(key, String(value));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return searchParams.toString();
|
|
89
|
+
}
|
|
90
|
+
function buildListUrl(basePath, params, defaults) {
|
|
91
|
+
const queryString = buildListQueryString(params, defaults);
|
|
92
|
+
return queryString ? `${basePath}?${queryString}` : basePath;
|
|
93
|
+
}
|
|
94
|
+
function mergeListParams(current, updates) {
|
|
95
|
+
return {
|
|
96
|
+
...current,
|
|
97
|
+
...updates,
|
|
98
|
+
filters: {
|
|
99
|
+
...current.filters,
|
|
100
|
+
...updates.filters
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function hasActiveFilters(params) {
|
|
105
|
+
if (params.search) return true;
|
|
106
|
+
return Object.keys(params.filters).length > 0;
|
|
107
|
+
}
|
|
108
|
+
function getFilterCount(params) {
|
|
109
|
+
let count = params.search ? 1 : 0;
|
|
110
|
+
count += Object.keys(params.filters).length;
|
|
111
|
+
return count;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/utils/filters.ts
|
|
115
|
+
function buildGraphQLFilter(filters, filterableFields, options = {}) {
|
|
116
|
+
const conditions = [];
|
|
117
|
+
for (const field of filterableFields) {
|
|
118
|
+
if (!field.graphql) continue;
|
|
119
|
+
const value = filters[field.key];
|
|
120
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
121
|
+
const condition = buildFieldCondition(field, value);
|
|
122
|
+
if (condition) {
|
|
123
|
+
conditions.push(condition);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (options.search && options.searchFields && options.searchFields.length > 0) {
|
|
127
|
+
const searchConditions = options.searchFields.map((fieldKey) => ({
|
|
128
|
+
[fieldKey]: { contains: options.search }
|
|
129
|
+
}));
|
|
130
|
+
if (searchConditions.length === 1) {
|
|
131
|
+
conditions.push(searchConditions[0]);
|
|
132
|
+
} else if (searchConditions.length > 1) {
|
|
133
|
+
conditions.push({ or: searchConditions });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (conditions.length === 0) {
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
if (conditions.length === 1) {
|
|
140
|
+
return conditions[0];
|
|
141
|
+
}
|
|
142
|
+
return { and: conditions };
|
|
143
|
+
}
|
|
144
|
+
function buildFieldCondition(field, value) {
|
|
145
|
+
const operator = field.graphqlOperator;
|
|
146
|
+
if (operator) {
|
|
147
|
+
return { [field.key]: { [operator]: value } };
|
|
148
|
+
}
|
|
149
|
+
switch (field.type) {
|
|
150
|
+
case "text":
|
|
151
|
+
return { [field.key]: { contains: String(value) } };
|
|
152
|
+
case "number":
|
|
153
|
+
return { [field.key]: { eq: Number(value) } };
|
|
154
|
+
case "boolean":
|
|
155
|
+
return { [field.key]: { eq: Boolean(value) } };
|
|
156
|
+
case "date":
|
|
157
|
+
if (value instanceof Date) {
|
|
158
|
+
return { [field.key]: { eq: value.toISOString() } };
|
|
159
|
+
}
|
|
160
|
+
return { [field.key]: { eq: value } };
|
|
161
|
+
case "select":
|
|
162
|
+
return { [field.key]: { eq: value } };
|
|
163
|
+
default:
|
|
164
|
+
return { [field.key]: { eq: value } };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function applyClientFilters(items, filters, filterableFields, options = {}) {
|
|
168
|
+
let result = items;
|
|
169
|
+
for (const field of filterableFields) {
|
|
170
|
+
if (field.graphql) continue;
|
|
171
|
+
const value = filters[field.key];
|
|
172
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
173
|
+
result = result.filter((item) => matchesFieldFilter(item, field, value));
|
|
174
|
+
}
|
|
175
|
+
if (options.search && options.searchFields) {
|
|
176
|
+
const searchLower = options.search.toLowerCase();
|
|
177
|
+
result = result.filter(
|
|
178
|
+
(item) => options.searchFields.some((fieldKey) => {
|
|
179
|
+
const fieldValue = item[fieldKey];
|
|
180
|
+
if (fieldValue === null || fieldValue === void 0) return false;
|
|
181
|
+
return String(fieldValue).toLowerCase().includes(searchLower);
|
|
182
|
+
})
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function matchesFieldFilter(item, field, filterValue) {
|
|
188
|
+
const itemValue = item[field.key];
|
|
189
|
+
switch (field.type) {
|
|
190
|
+
case "text":
|
|
191
|
+
if (itemValue === null || itemValue === void 0) return false;
|
|
192
|
+
return String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());
|
|
193
|
+
case "number":
|
|
194
|
+
return Number(itemValue) === Number(filterValue);
|
|
195
|
+
case "boolean":
|
|
196
|
+
return Boolean(itemValue) === Boolean(filterValue);
|
|
197
|
+
case "date":
|
|
198
|
+
if (!itemValue) return false;
|
|
199
|
+
const itemDate = itemValue instanceof Date ? itemValue : new Date(itemValue);
|
|
200
|
+
const filterDate = filterValue instanceof Date ? filterValue : new Date(filterValue);
|
|
201
|
+
return itemDate.toDateString() === filterDate.toDateString();
|
|
202
|
+
case "select":
|
|
203
|
+
return itemValue === filterValue;
|
|
204
|
+
default:
|
|
205
|
+
return itemValue === filterValue;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function sortItems(items, sortKey, sortOrder) {
|
|
209
|
+
if (!sortKey) return items;
|
|
210
|
+
const multiplier = sortOrder === "asc" ? 1 : -1;
|
|
211
|
+
return [...items].sort((a, b) => {
|
|
212
|
+
const aVal = a[sortKey];
|
|
213
|
+
const bVal = b[sortKey];
|
|
214
|
+
if (aVal === null || aVal === void 0) return 1;
|
|
215
|
+
if (bVal === null || bVal === void 0) return -1;
|
|
216
|
+
if (aVal === bVal) return 0;
|
|
217
|
+
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
218
|
+
return aVal.localeCompare(bVal) * multiplier;
|
|
219
|
+
}
|
|
220
|
+
if (typeof aVal === "number" && typeof bVal === "number") {
|
|
221
|
+
return (aVal - bVal) * multiplier;
|
|
222
|
+
}
|
|
223
|
+
if (aVal instanceof Date && bVal instanceof Date) {
|
|
224
|
+
return (aVal.getTime() - bVal.getTime()) * multiplier;
|
|
225
|
+
}
|
|
226
|
+
return String(aVal).localeCompare(String(bVal)) * multiplier;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
function paginateItems(items, page, pageSize) {
|
|
230
|
+
const totalItems = items.length;
|
|
231
|
+
const totalPages = Math.ceil(totalItems / pageSize);
|
|
232
|
+
const currentPage = Math.min(Math.max(1, page), totalPages || 1);
|
|
233
|
+
const startIndex = (currentPage - 1) * pageSize;
|
|
234
|
+
const endIndex = startIndex + pageSize;
|
|
235
|
+
const paginatedItems = items.slice(startIndex, endIndex);
|
|
236
|
+
return {
|
|
237
|
+
paginatedItems,
|
|
238
|
+
totalItems,
|
|
239
|
+
totalPages,
|
|
240
|
+
currentPage,
|
|
241
|
+
pageSize
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function processListData(items, params, filterableFields, options = {}) {
|
|
245
|
+
let result = applyClientFilters(items, params.filters, filterableFields, {
|
|
246
|
+
search: params.search,
|
|
247
|
+
searchFields: options.searchFields
|
|
248
|
+
});
|
|
249
|
+
result = sortItems(result, params.sortKey, params.sortOrder);
|
|
250
|
+
const paginated = paginateItems(result, params.page, params.pageSize);
|
|
251
|
+
return {
|
|
252
|
+
items: paginated.paginatedItems,
|
|
253
|
+
totalItems: paginated.totalItems,
|
|
254
|
+
totalPages: paginated.totalPages,
|
|
255
|
+
currentPage: paginated.currentPage,
|
|
256
|
+
pageSize: paginated.pageSize
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export {
|
|
261
|
+
parseListParams,
|
|
262
|
+
buildListQueryString,
|
|
263
|
+
buildListUrl,
|
|
264
|
+
mergeListParams,
|
|
265
|
+
hasActiveFilters,
|
|
266
|
+
getFilterCount,
|
|
267
|
+
buildGraphQLFilter,
|
|
268
|
+
applyClientFilters,
|
|
269
|
+
sortItems,
|
|
270
|
+
paginateItems,
|
|
271
|
+
processListData
|
|
272
|
+
};
|
|
273
|
+
//# sourceMappingURL=chunk-2GML443T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/params.ts","../src/utils/filters.ts"],"sourcesContent":["/**\n * URL Parameter Parsing Utilities\n *\n * Utilities for parsing and building URL query parameters for list pages,\n * supporting pagination, sorting, filtering, and search.\n */\n\n/**\n * Filter configuration for a field\n */\nexport interface FilterFieldConfig {\n\t/** Field key */\n\tkey: string;\n\t/** Field type for parsing */\n\ttype: \"text\" | \"number\" | \"boolean\" | \"date\" | \"select\";\n\t/** Whether this filter should be applied via GraphQL (vs client-side) */\n\tgraphql?: boolean;\n\t/** GraphQL operator to use (defaults to 'contains' for text, 'eq' for others) */\n\tgraphqlOperator?: \"eq\" | \"ne\" | \"le\" | \"lt\" | \"ge\" | \"gt\" | \"contains\" | \"notContains\" | \"beginsWith\";\n\t/** Valid options for select type */\n\toptions?: string[];\n}\n\n/**\n * Configuration for parsing list parameters\n */\nexport interface ListParamConfig {\n\t/** Default page size */\n\tdefaultPageSize?: number;\n\t/** Default sort configuration */\n\tdefaultSort?: { key: string; order: \"asc\" | \"desc\" };\n\t/** Searchable field keys */\n\tsearchableFields?: string[];\n\t/** Filterable field configurations */\n\tfilterableFields?: FilterFieldConfig[];\n\t/** Maximum page size allowed */\n\tmaxPageSize?: number;\n}\n\n/**\n * Parsed list parameters\n */\nexport interface ListParams {\n\t/** Current page (1-indexed) */\n\tpage: number;\n\t/** Items per page */\n\tpageSize: number;\n\t/** Sort field key */\n\tsortKey: string;\n\t/** Sort direction */\n\tsortOrder: \"asc\" | \"desc\";\n\t/** Search query */\n\tsearch?: string;\n\t/** Active filters */\n\tfilters: Record<string, any>;\n}\n\n/**\n * Parse URL query parameters for a list page\n *\n * @example\n * ```typescript\n * const params = parseListParams(Astro.url, {\n * defaultPageSize: 25,\n * defaultSort: { key: 'name', order: 'asc' },\n * filterableFields: [\n * { key: 'status', type: 'select', options: ['active', 'inactive'] },\n * { key: 'name', type: 'text' },\n * ],\n * });\n *\n * // URL: ?page=2&pageSize=50&sortKey=email&status=active\n * // Returns: { page: 2, pageSize: 50, sortKey: 'email', sortOrder: 'asc', filters: { status: 'active' } }\n * ```\n */\nexport function parseListParams(url: URL, config: ListParamConfig = {}): ListParams {\n\tconst searchParams = url.searchParams;\n\tconst {\n\t\tdefaultPageSize = 25,\n\t\tdefaultSort = { key: \"id\", order: \"asc\" },\n\t\tfilterableFields = [],\n\t\tmaxPageSize = 100,\n\t} = config;\n\n\t// Parse pagination\n\tlet page = parseInt(searchParams.get(\"page\") ?? \"1\", 10);\n\tif (Number.isNaN(page) || page < 1) page = 1;\n\n\tlet pageSize = parseInt(searchParams.get(\"pageSize\") ?? String(defaultPageSize), 10);\n\tif (Number.isNaN(pageSize) || pageSize < 1) pageSize = defaultPageSize;\n\tif (pageSize > maxPageSize) pageSize = maxPageSize;\n\n\t// Parse sorting\n\tconst sortKey = searchParams.get(\"sortKey\") ?? defaultSort.key;\n\tconst sortOrderParam = searchParams.get(\"sortOrder\");\n\tconst sortOrder: \"asc\" | \"desc\" =\n\t\tsortOrderParam === \"asc\" || sortOrderParam === \"desc\" ? sortOrderParam : defaultSort.order;\n\n\t// Parse search\n\tconst search = searchParams.get(\"search\") ?? undefined;\n\n\t// Parse filters\n\tconst filters: Record<string, any> = {};\n\n\tfor (const field of filterableFields) {\n\t\tconst value = searchParams.get(field.key);\n\t\tif (value === null || value === \"\") continue;\n\n\t\t// Parse based on field type\n\t\tswitch (field.type) {\n\t\t\tcase \"number\":\n\t\t\t\tconst numValue = parseFloat(value);\n\t\t\t\tif (!Number.isNaN(numValue)) {\n\t\t\t\t\tfilters[field.key] = numValue;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"boolean\":\n\t\t\t\tfilters[field.key] = value === \"true\" || value === \"1\";\n\t\t\t\tbreak;\n\n\t\t\tcase \"date\":\n\t\t\t\tconst dateValue = new Date(value);\n\t\t\t\tif (!Number.isNaN(dateValue.getTime())) {\n\t\t\t\t\tfilters[field.key] = dateValue;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"select\":\n\t\t\t\t// Validate against allowed options\n\t\t\t\tif (!field.options || field.options.includes(value)) {\n\t\t\t\t\tfilters[field.key] = value;\n\t\t\t\t}\n\t\t\t\tbreak;\n\n\t\t\tcase \"text\":\n\t\t\tdefault:\n\t\t\t\tfilters[field.key] = value;\n\t\t\t\tbreak;\n\t\t}\n\t}\n\n\treturn {\n\t\tpage,\n\t\tpageSize,\n\t\tsortKey,\n\t\tsortOrder,\n\t\tsearch,\n\t\tfilters,\n\t};\n}\n\n/**\n * Build URL query string from list parameters\n *\n * @example\n * ```typescript\n * const queryString = buildListQueryString({\n * page: 2,\n * pageSize: 50,\n * sortKey: 'name',\n * sortOrder: 'asc',\n * filters: { status: 'active' },\n * });\n * // Returns: \"page=2&pageSize=50&sortKey=name&sortOrder=asc&status=active\"\n * ```\n */\nexport function buildListQueryString(params: Partial<ListParams>, defaults?: Partial<ListParams>): string {\n\tconst searchParams = new URLSearchParams();\n\n\t// Add pagination (only if different from defaults)\n\tif (params.page !== undefined && params.page !== 1) {\n\t\tsearchParams.set(\"page\", String(params.page));\n\t}\n\n\tif (params.pageSize !== undefined && params.pageSize !== (defaults?.pageSize ?? 25)) {\n\t\tsearchParams.set(\"pageSize\", String(params.pageSize));\n\t}\n\n\t// Add sorting (only if different from defaults)\n\tif (params.sortKey !== undefined && params.sortKey !== (defaults?.sortKey ?? \"id\")) {\n\t\tsearchParams.set(\"sortKey\", params.sortKey);\n\t}\n\n\tif (params.sortOrder !== undefined && params.sortOrder !== (defaults?.sortOrder ?? \"asc\")) {\n\t\tsearchParams.set(\"sortOrder\", params.sortOrder);\n\t}\n\n\t// Add search\n\tif (params.search) {\n\t\tsearchParams.set(\"search\", params.search);\n\t}\n\n\t// Add filters\n\tif (params.filters) {\n\t\tfor (const [key, value] of Object.entries(params.filters)) {\n\t\t\tif (value === undefined || value === null || value === \"\") continue;\n\n\t\t\tif (value instanceof Date) {\n\t\t\t\tsearchParams.set(key, value.toISOString());\n\t\t\t} else if (typeof value === \"boolean\") {\n\t\t\t\tsearchParams.set(key, value ? \"true\" : \"false\");\n\t\t\t} else {\n\t\t\t\tsearchParams.set(key, String(value));\n\t\t\t}\n\t\t}\n\t}\n\n\treturn searchParams.toString();\n}\n\n/**\n * Build URL with list parameters\n *\n * @example\n * ```typescript\n * const url = buildListUrl('/admin/users', { page: 2, filters: { status: 'active' } });\n * // Returns: \"/admin/users?page=2&status=active\"\n * ```\n */\nexport function buildListUrl(basePath: string, params: Partial<ListParams>, defaults?: Partial<ListParams>): string {\n\tconst queryString = buildListQueryString(params, defaults);\n\treturn queryString ? `${basePath}?${queryString}` : basePath;\n}\n\n/**\n * Merge current params with new params (for updating single values)\n *\n * @example\n * ```typescript\n * const newParams = mergeListParams(currentParams, { page: 2 });\n * ```\n */\nexport function mergeListParams(current: ListParams, updates: Partial<ListParams>): ListParams {\n\treturn {\n\t\t...current,\n\t\t...updates,\n\t\tfilters: {\n\t\t\t...current.filters,\n\t\t\t...updates.filters,\n\t\t},\n\t};\n}\n\n/**\n * Check if any filters are active\n */\nexport function hasActiveFilters(params: ListParams): boolean {\n\tif (params.search) return true;\n\treturn Object.keys(params.filters).length > 0;\n}\n\n/**\n * Get filter count\n */\nexport function getFilterCount(params: ListParams): number {\n\tlet count = params.search ? 1 : 0;\n\tcount += Object.keys(params.filters).length;\n\treturn count;\n}\n","/**\n * Filter Building Utilities\n *\n * Utilities for building GraphQL filters from parsed URL parameters,\n * and for applying client-side filters to data.\n */\n\nimport type { FilterFieldConfig, ListParams } from \"./params\";\n\n/**\n * GraphQL filter operators\n */\nexport type GraphQLOperator = \"eq\" | \"ne\" | \"le\" | \"lt\" | \"ge\" | \"gt\" | \"contains\" | \"notContains\" | \"beginsWith\" | \"between\";\n\n/**\n * GraphQL filter condition\n */\nexport interface GraphQLFilterCondition {\n\t[key: string]: {\n\t\t[operator in GraphQLOperator]?: any;\n\t};\n}\n\n/**\n * GraphQL combined filter\n */\nexport interface GraphQLFilter {\n\tand?: GraphQLFilterCondition[];\n\tor?: GraphQLFilterCondition[];\n\tnot?: GraphQLFilter;\n\t[key: string]: any;\n}\n\n/**\n * Build GraphQL filter from parsed URL parameters\n *\n * Only includes fields marked with `graphql: true` in the field config.\n *\n * @example\n * ```typescript\n * const filter = buildGraphQLFilter(\n * { status: 'active', name: 'test' },\n * [\n * { key: 'status', type: 'select', graphql: true },\n * { key: 'name', type: 'text', graphql: true },\n * ]\n * );\n * // Returns: { and: [{ status: { eq: 'active' } }, { name: { contains: 'test' } }] }\n * ```\n */\nexport function buildGraphQLFilter(\n\tfilters: Record<string, any>,\n\tfilterableFields: FilterFieldConfig[],\n\toptions: { searchFields?: string[]; search?: string } = {}\n): GraphQLFilter | undefined {\n\tconst conditions: GraphQLFilterCondition[] = [];\n\n\t// Build field filters\n\tfor (const field of filterableFields) {\n\t\tif (!field.graphql) continue;\n\n\t\tconst value = filters[field.key];\n\t\tif (value === undefined || value === null || value === \"\") continue;\n\n\t\tconst condition = buildFieldCondition(field, value);\n\t\tif (condition) {\n\t\t\tconditions.push(condition);\n\t\t}\n\t}\n\n\t// Build search filter (applies to multiple fields with OR)\n\tif (options.search && options.searchFields && options.searchFields.length > 0) {\n\t\tconst searchConditions = options.searchFields.map((fieldKey) => ({\n\t\t\t[fieldKey]: { contains: options.search },\n\t\t}));\n\n\t\tif (searchConditions.length === 1) {\n\t\t\tconditions.push(searchConditions[0]);\n\t\t} else if (searchConditions.length > 1) {\n\t\t\t// Multiple search fields use OR\n\t\t\tconditions.push({ or: searchConditions } as unknown as GraphQLFilterCondition);\n\t\t}\n\t}\n\n\t// Return combined filter\n\tif (conditions.length === 0) {\n\t\treturn undefined;\n\t}\n\n\tif (conditions.length === 1) {\n\t\treturn conditions[0] as unknown as GraphQLFilter;\n\t}\n\n\treturn { and: conditions };\n}\n\n/**\n * Build a single field condition\n */\nfunction buildFieldCondition(field: FilterFieldConfig, value: any): GraphQLFilterCondition | undefined {\n\t// Use custom operator if provided\n\tconst operator = field.graphqlOperator;\n\n\tif (operator) {\n\t\t// Use the custom operator specified in config\n\t\treturn { [field.key]: { [operator]: value } };\n\t}\n\n\t// Default behavior based on field type\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\t\t// Text fields use contains by default (case-sensitive in DynamoDB)\n\t\t\treturn { [field.key]: { contains: String(value) } };\n\n\t\tcase \"number\":\n\t\t\t// Number fields use exact match\n\t\t\treturn { [field.key]: { eq: Number(value) } };\n\n\t\tcase \"boolean\":\n\t\t\t// Boolean fields use exact match\n\t\t\treturn { [field.key]: { eq: Boolean(value) } };\n\n\t\tcase \"date\":\n\t\t\t// Date fields - could be exact or range\n\t\t\tif (value instanceof Date) {\n\t\t\t\treturn { [field.key]: { eq: value.toISOString() } };\n\t\t\t}\n\t\t\treturn { [field.key]: { eq: value } };\n\n\t\tcase \"select\":\n\t\t\t// Select fields use exact match\n\t\t\treturn { [field.key]: { eq: value } };\n\n\t\tdefault:\n\t\t\treturn { [field.key]: { eq: value } };\n\t}\n}\n\n/**\n * Apply client-side filters to data array\n *\n * Only applies filters for fields NOT marked with `graphql: true`.\n *\n * @example\n * ```typescript\n * const filtered = applyClientFilters(\n * items,\n * { brandCount: 5 },\n * [{ key: 'brandCount', type: 'number', graphql: false }]\n * );\n * ```\n */\nexport function applyClientFilters<T extends Record<string, any>>(\n\titems: T[],\n\tfilters: Record<string, any>,\n\tfilterableFields: FilterFieldConfig[],\n\toptions: { search?: string; searchFields?: string[] } = {}\n): T[] {\n\tlet result = items;\n\n\t// Apply field filters (only non-GraphQL fields)\n\tfor (const field of filterableFields) {\n\t\tif (field.graphql) continue; // Skip GraphQL fields\n\n\t\tconst value = filters[field.key];\n\t\tif (value === undefined || value === null || value === \"\") continue;\n\n\t\tresult = result.filter((item) => matchesFieldFilter(item, field, value));\n\t}\n\n\t// Apply search filter (only if not applied via GraphQL)\n\tif (options.search && options.searchFields) {\n\t\tconst searchLower = options.search.toLowerCase();\n\t\tresult = result.filter((item) =>\n\t\t\toptions.searchFields!.some((fieldKey) => {\n\t\t\t\tconst fieldValue = item[fieldKey];\n\t\t\t\tif (fieldValue === null || fieldValue === undefined) return false;\n\t\t\t\treturn String(fieldValue).toLowerCase().includes(searchLower);\n\t\t\t})\n\t\t);\n\t}\n\n\treturn result;\n}\n\n/**\n * Check if an item matches a field filter\n */\nfunction matchesFieldFilter<T extends Record<string, any>>(item: T, field: FilterFieldConfig, filterValue: any): boolean {\n\tconst itemValue = item[field.key];\n\n\tswitch (field.type) {\n\t\tcase \"text\":\n\t\t\tif (itemValue === null || itemValue === undefined) return false;\n\t\t\treturn String(itemValue).toLowerCase().includes(String(filterValue).toLowerCase());\n\n\t\tcase \"number\":\n\t\t\treturn Number(itemValue) === Number(filterValue);\n\n\t\tcase \"boolean\":\n\t\t\treturn Boolean(itemValue) === Boolean(filterValue);\n\n\t\tcase \"date\":\n\t\t\tif (!itemValue) return false;\n\t\t\tconst itemDate = itemValue instanceof Date ? itemValue : new Date(itemValue);\n\t\t\tconst filterDate = filterValue instanceof Date ? filterValue : new Date(filterValue);\n\t\t\t// Compare dates (ignoring time)\n\t\t\treturn itemDate.toDateString() === filterDate.toDateString();\n\n\t\tcase \"select\":\n\t\t\treturn itemValue === filterValue;\n\n\t\tdefault:\n\t\t\treturn itemValue === filterValue;\n\t}\n}\n\n/**\n * Sort items by a key\n */\nexport function sortItems<T extends Record<string, any>>(items: T[], sortKey: string, sortOrder: \"asc\" | \"desc\"): T[] {\n\tif (!sortKey) return items;\n\n\tconst multiplier = sortOrder === \"asc\" ? 1 : -1;\n\n\treturn [...items].sort((a, b) => {\n\t\tconst aVal = a[sortKey];\n\t\tconst bVal = b[sortKey];\n\n\t\t// Handle null/undefined\n\t\tif (aVal === null || aVal === undefined) return 1;\n\t\tif (bVal === null || bVal === undefined) return -1;\n\t\tif (aVal === bVal) return 0;\n\n\t\t// Handle different types\n\t\tif (typeof aVal === \"string\" && typeof bVal === \"string\") {\n\t\t\treturn aVal.localeCompare(bVal) * multiplier;\n\t\t}\n\n\t\tif (typeof aVal === \"number\" && typeof bVal === \"number\") {\n\t\t\treturn (aVal - bVal) * multiplier;\n\t\t}\n\n\t\tif (aVal instanceof Date && bVal instanceof Date) {\n\t\t\treturn (aVal.getTime() - bVal.getTime()) * multiplier;\n\t\t}\n\n\t\t// Fallback to string comparison\n\t\treturn String(aVal).localeCompare(String(bVal)) * multiplier;\n\t});\n}\n\n/**\n * Paginate items\n */\nexport function paginateItems<T>(\n\titems: T[],\n\tpage: number,\n\tpageSize: number\n): {\n\tpaginatedItems: T[];\n\ttotalItems: number;\n\ttotalPages: number;\n\tcurrentPage: number;\n\tpageSize: number;\n} {\n\tconst totalItems = items.length;\n\tconst totalPages = Math.ceil(totalItems / pageSize);\n\tconst currentPage = Math.min(Math.max(1, page), totalPages || 1);\n\n\tconst startIndex = (currentPage - 1) * pageSize;\n\tconst endIndex = startIndex + pageSize;\n\tconst paginatedItems = items.slice(startIndex, endIndex);\n\n\treturn {\n\t\tpaginatedItems,\n\t\ttotalItems,\n\t\ttotalPages,\n\t\tcurrentPage,\n\t\tpageSize,\n\t};\n}\n\n/**\n * Process list data with filters, sorting, and pagination\n *\n * Convenience function that combines all processing steps.\n */\nexport function processListData<T extends Record<string, any>>(\n\titems: T[],\n\tparams: ListParams,\n\tfilterableFields: FilterFieldConfig[],\n\toptions: { searchFields?: string[] } = {}\n): {\n\titems: T[];\n\ttotalItems: number;\n\ttotalPages: number;\n\tcurrentPage: number;\n\tpageSize: number;\n} {\n\t// Apply client-side filters\n\tlet result = applyClientFilters(items, params.filters, filterableFields, {\n\t\tsearch: params.search,\n\t\tsearchFields: options.searchFields,\n\t});\n\n\t// Sort\n\tresult = sortItems(result, params.sortKey, params.sortOrder);\n\n\t// Paginate\n\tconst paginated = paginateItems(result, params.page, params.pageSize);\n\n\treturn {\n\t\titems: paginated.paginatedItems,\n\t\ttotalItems: paginated.totalItems,\n\t\ttotalPages: paginated.totalPages,\n\t\tcurrentPage: paginated.currentPage,\n\t\tpageSize: paginated.pageSize,\n\t};\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/htlkg/config.ts"],"sourcesContent":["/**\n * Configuration types for the htlkg Astro integration\n */\n\nimport type { AuthUser } from '@htlkg/core/types';\n\n// Re-export AuthUser for convenience\nexport type { AuthUser };\n\n/**\n * Route pattern that can be either a RegExp or a string.\n * - String patterns match exact paths or paths that start with the pattern\n * - RegExp patterns allow for more complex matching logic\n *\n * @example\n * // String pattern - matches /login and /login/*\n * '/login'\n *\n * @example\n * // RegExp pattern - matches /api/v1/* and /api/v2/*\n * /^\\/api\\/v[12]\\//\n */\nexport type RoutePattern = RegExp | string;\n\n/**\n * Configuration for brand-specific routes that require brand ID extraction.\n * The pattern should include a capture group for the brand ID.\n *\n * @example\n * {\n * pattern: /^\\/brands\\/(\\d+)/,\n * brandIdParam: 1 // First capture group\n * }\n */\nexport interface BrandRouteConfig {\n\t/**\n\t * Regular expression pattern with a capture group for the brand ID\n\t */\n\tpattern: RegExp;\n\n\t/**\n\t * Index of the capture group containing the brand ID (1-based)\n\t */\n\tbrandIdParam: number;\n}\n\n/**\n * Configuration for the default login page injected by the integration.\n */\nexport interface LoginPageConfig {\n\t/**\n\t * URL path where the login page should be accessible.\n\t * @default '/login'\n\t */\n\tpath?: string;\n\n\t/**\n\t * Logo URL to display on the login page.\n\t * @default undefined\n\t */\n\tlogo?: string;\n\n\t/**\n\t * Title text to display on the login page.\n\t * @default 'Sign In'\n\t */\n\ttitle?: string;\n\n\t/**\n\t * URL to redirect to after successful login.\n\t * @default '/admin'\n\t */\n\tredirectUrl?: string;\n}\n\n/**\n * Configuration for route guards that protect routes based on authentication\n * status and user permissions.\n *\n * @example\n * {\n * publicRoutes: ['/login', '/register', /^\\/public\\//],\n * authenticatedRoutes: ['/dashboard', /^\\/profile/],\n * adminRoutes: ['/admin', /^\\/admin\\//],\n * brandRoutes: [\n * { pattern: /^\\/brands\\/(\\d+)/, brandIdParam: 1 }\n * ],\n * loginUrl: '/login'\n * }\n */\nexport interface RouteGuardConfig {\n\t/**\n\t * Routes that are accessible without authentication.\n\t * These routes bypass all authentication checks.\n\t *\n\t * @default []\n\t */\n\tpublicRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require any authenticated user.\n\t * Users must be logged in but no specific permissions are required.\n\t *\n\t * @default []\n\t */\n\tauthenticatedRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require admin privileges.\n\t * Only users with isAdmin=true can access these routes.\n\t *\n\t * @default []\n\t */\n\tadminRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require brand-specific access.\n\t * Users must have access to the specific brand ID extracted from the route,\n\t * or be an admin.\n\t *\n\t * @default []\n\t */\n\tbrandRoutes?: BrandRouteConfig[];\n\n\t/**\n\t * URL to redirect to when authentication is required.\n\t * Query parameters will be added for return URL and error messages.\n\t *\n\t * @default '/login'\n\t */\n\tloginUrl?: string;\n}\n\n/**\n * Options for configuring the htlkg Astro integration.\n *\n * @example\n * htlkg({\n * auth: {\n * publicRoutes: ['/login', '/'],\n * adminRoutes: [/^\\/admin/],\n * loginUrl: '/login'\n * },\n * loginPage: {\n * logo: 'https://example.com/logo.png',\n * title: 'Admin Portal',\n * redirectUrl: '/admin/dashboard'\n * },\n * validateEnv: true,\n * tailwind: { configFile: './tailwind.config.mjs' }\n * })\n */\nexport interface HtlkgIntegrationOptions {\n\t/**\n\t * Route guard configuration for authentication and authorization.\n\t * Defines which routes are public, require authentication, or require\n\t * specific permissions.\n\t *\n\t * @default {}\n\t */\n\tauth?: RouteGuardConfig;\n\n\t/**\n\t * Configuration for the default login page.\n\t * Set to false to disable automatic login page injection.\n\t *\n\t * @default { path: '/login', title: 'Sign In', redirectUrl: '/admin' }\n\t */\n\tloginPage?: LoginPageConfig | false;\n\n\n\n\t/**\n\t * Validate that required environment variables are present.\n\t * When enabled, the integration will check for required Amplify\n\t * environment variables and log warnings if any are missing.\n\t *\n\t * @default true\n\t */\n\tvalidateEnv?: boolean;\n\n\t/**\n\t * List of required environment variable names to validate.\n\t * Only used when validateEnv is true.\n\t *\n\t * @default ['PUBLIC_COGNITO_USER_POOL_ID', 'PUBLIC_COGNITO_USER_POOL_CLIENT_ID']\n\t */\n\trequiredEnvVars?: string[];\n\n\t/**\n\t * AWS Amplify configuration.\n\t * Pass the amplify_outputs.json content directly for automatic configuration.\n\t * This is the recommended approach as it ensures consistency with Amplify CLI.\n\t *\n\t * @example\n\t * import amplifyOutputs from './amplify_outputs.json';\n\t * \n\t * htlkg({\n\t * amplify: amplifyOutputs\n\t * })\n\t */\n\tamplify?: Record<string, unknown>;\n\n\t/**\n\t * Tailwind CSS integration configuration.\n\t * Set to false to disable automatic Tailwind integration.\n\t * Pass an object to configure Tailwind options (e.g., configFile).\n\t *\n\t * @default undefined (Tailwind enabled with default config)\n\t */\n\ttailwind?: Record<string, unknown> | false;\n}\n\n/**\n * Type guard to check if a value is an authenticated user.\n * Useful for narrowing types in TypeScript when checking user authentication.\n *\n * @param user - Value to check\n * @returns True if the value is a valid AuthUser object\n *\n * @example\n * if (isAuthenticatedUser(locals.user)) {\n * // TypeScript knows locals.user is AuthUser here\n * console.log(locals.user.email);\n * }\n */\nexport function isAuthenticatedUser(user: unknown): user is AuthUser {\n\treturn (\n\t\tuser !== null &&\n\t\ttypeof user === 'object' &&\n\t\t'username' in user &&\n\t\t'email' in user &&\n\t\t'brandIds' in user &&\n\t\t'accountIds' in user &&\n\t\t'isAdmin' in user &&\n\t\ttypeof (user as AuthUser).username === 'string' &&\n\t\ttypeof (user as AuthUser).email === 'string' &&\n\t\tArray.isArray((user as AuthUser).brandIds) &&\n\t\tArray.isArray((user as AuthUser).accountIds) &&\n\t\ttypeof (user as AuthUser).isAdmin === 'boolean'\n\t);\n}\n"],"mappings":";
|
|
1
|
+
{"version":3,"sources":["../src/htlkg/config.ts"],"sourcesContent":["/**\n * Configuration types for the htlkg Astro integration\n */\n\nimport type { AuthUser } from '@htlkg/core/types';\n\n// Re-export AuthUser for convenience\nexport type { AuthUser };\n\n/**\n * Route pattern that can be either a RegExp or a string.\n * - String patterns match exact paths or paths that start with the pattern\n * - RegExp patterns allow for more complex matching logic\n *\n * @example\n * // String pattern - matches /login and /login/*\n * '/login'\n *\n * @example\n * // RegExp pattern - matches /api/v1/* and /api/v2/*\n * /^\\/api\\/v[12]\\//\n */\nexport type RoutePattern = RegExp | string;\n\n/**\n * Configuration for brand-specific routes that require brand ID extraction.\n * The pattern should include a capture group for the brand ID.\n *\n * @example\n * {\n * pattern: /^\\/brands\\/(\\d+)/,\n * brandIdParam: 1 // First capture group\n * }\n */\nexport interface BrandRouteConfig {\n\t/**\n\t * Regular expression pattern with a capture group for the brand ID\n\t */\n\tpattern: RegExp;\n\n\t/**\n\t * Index of the capture group containing the brand ID (1-based)\n\t */\n\tbrandIdParam: number;\n}\n\n/**\n * Configuration for the default login page injected by the integration.\n */\nexport interface LoginPageConfig {\n\t/**\n\t * URL path where the login page should be accessible.\n\t * @default '/login'\n\t */\n\tpath?: string;\n\n\t/**\n\t * Logo URL to display on the login page.\n\t * @default undefined\n\t */\n\tlogo?: string;\n\n\t/**\n\t * Title text to display on the login page.\n\t * @default 'Sign In'\n\t */\n\ttitle?: string;\n\n\t/**\n\t * URL to redirect to after successful login.\n\t * @default '/admin'\n\t */\n\tredirectUrl?: string;\n}\n\n/**\n * Configuration for route guards that protect routes based on authentication\n * status and user permissions.\n *\n * @example\n * {\n * publicRoutes: ['/login', '/register', /^\\/public\\//],\n * authenticatedRoutes: ['/dashboard', /^\\/profile/],\n * adminRoutes: ['/admin', /^\\/admin\\//],\n * brandRoutes: [\n * { pattern: /^\\/brands\\/(\\d+)/, brandIdParam: 1 }\n * ],\n * loginUrl: '/login'\n * }\n */\nexport interface RouteGuardConfig {\n\t/**\n\t * Routes that are accessible without authentication.\n\t * These routes bypass all authentication checks.\n\t *\n\t * @default []\n\t */\n\tpublicRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require any authenticated user.\n\t * Users must be logged in but no specific permissions are required.\n\t *\n\t * @default []\n\t */\n\tauthenticatedRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require admin privileges.\n\t * Only users with isAdmin=true can access these routes.\n\t *\n\t * @default []\n\t */\n\tadminRoutes?: RoutePattern[];\n\n\t/**\n\t * Routes that require brand-specific access.\n\t * Users must have access to the specific brand ID extracted from the route,\n\t * or be an admin.\n\t *\n\t * @default []\n\t */\n\tbrandRoutes?: BrandRouteConfig[];\n\n\t/**\n\t * URL to redirect to when authentication is required.\n\t * Query parameters will be added for return URL and error messages.\n\t *\n\t * @default '/login'\n\t */\n\tloginUrl?: string;\n}\n\n/**\n * Options for configuring the htlkg Astro integration.\n *\n * @example\n * htlkg({\n * auth: {\n * publicRoutes: ['/login', '/'],\n * adminRoutes: [/^\\/admin/],\n * loginUrl: '/login'\n * },\n * loginPage: {\n * logo: 'https://example.com/logo.png',\n * title: 'Admin Portal',\n * redirectUrl: '/admin/dashboard'\n * },\n * validateEnv: true,\n * tailwind: { configFile: './tailwind.config.mjs' }\n * })\n */\nexport interface HtlkgIntegrationOptions {\n\t/**\n\t * Route guard configuration for authentication and authorization.\n\t * Defines which routes are public, require authentication, or require\n\t * specific permissions.\n\t *\n\t * @default {}\n\t */\n\tauth?: RouteGuardConfig;\n\n\t/**\n\t * Configuration for the default login page.\n\t * Set to false to disable automatic login page injection.\n\t *\n\t * @default { path: '/login', title: 'Sign In', redirectUrl: '/admin' }\n\t */\n\tloginPage?: LoginPageConfig | false;\n\n\n\n\t/**\n\t * Validate that required environment variables are present.\n\t * When enabled, the integration will check for required Amplify\n\t * environment variables and log warnings if any are missing.\n\t *\n\t * @default true\n\t */\n\tvalidateEnv?: boolean;\n\n\t/**\n\t * List of required environment variable names to validate.\n\t * Only used when validateEnv is true.\n\t *\n\t * @default ['PUBLIC_COGNITO_USER_POOL_ID', 'PUBLIC_COGNITO_USER_POOL_CLIENT_ID']\n\t */\n\trequiredEnvVars?: string[];\n\n\t/**\n\t * AWS Amplify configuration.\n\t * Pass the amplify_outputs.json content directly for automatic configuration.\n\t * This is the recommended approach as it ensures consistency with Amplify CLI.\n\t *\n\t * @example\n\t * import amplifyOutputs from './amplify_outputs.json';\n\t * \n\t * htlkg({\n\t * amplify: amplifyOutputs\n\t * })\n\t */\n\tamplify?: Record<string, unknown>;\n\n\t/**\n\t * Tailwind CSS integration configuration.\n\t * Set to false to disable automatic Tailwind integration.\n\t * Pass an object to configure Tailwind options (e.g., configFile).\n\t *\n\t * @default undefined (Tailwind enabled with default config)\n\t */\n\ttailwind?: Record<string, unknown> | false;\n\n\t/**\n\t * Vue app setup mode.\n\t * - 'full': Use the Amplify-configured Vue app setup (requires SSR)\n\t * - 'basic': Use basic Vue integration without Amplify app setup (works with static)\n\t * - 'auto': Automatically detect based on output mode (default)\n\t *\n\t * @default 'auto'\n\t */\n\tvueAppSetup?: 'full' | 'basic' | 'auto';\n}\n\n/**\n * Type guard to check if a value is an authenticated user.\n * Useful for narrowing types in TypeScript when checking user authentication.\n *\n * @param user - Value to check\n * @returns True if the value is a valid AuthUser object\n *\n * @example\n * if (isAuthenticatedUser(locals.user)) {\n * // TypeScript knows locals.user is AuthUser here\n * console.log(locals.user.email);\n * }\n */\nexport function isAuthenticatedUser(user: unknown): user is AuthUser {\n\treturn (\n\t\tuser !== null &&\n\t\ttypeof user === 'object' &&\n\t\t'username' in user &&\n\t\t'email' in user &&\n\t\t'brandIds' in user &&\n\t\t'accountIds' in user &&\n\t\t'isAdmin' in user &&\n\t\ttypeof (user as AuthUser).username === 'string' &&\n\t\ttypeof (user as AuthUser).email === 'string' &&\n\t\tArray.isArray((user as AuthUser).brandIds) &&\n\t\tArray.isArray((user as AuthUser).accountIds) &&\n\t\ttypeof (user as AuthUser).isAdmin === 'boolean'\n\t);\n}\n"],"mappings":";AA4OO,SAAS,oBAAoB,MAAiC;AACpE,SACC,SAAS,QACT,OAAO,SAAS,YAChB,cAAc,QACd,WAAW,QACX,cAAc,QACd,gBAAgB,QAChB,aAAa,QACb,OAAQ,KAAkB,aAAa,YACvC,OAAQ,KAAkB,UAAU,YACpC,MAAM,QAAS,KAAkB,QAAQ,KACzC,MAAM,QAAS,KAAkB,UAAU,KAC3C,OAAQ,KAAkB,YAAY;AAExC;","names":[]}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// src/htlkg/index.ts
|
|
2
|
+
import tailwind from "@astrojs/tailwind";
|
|
3
|
+
import vue from "@astrojs/vue";
|
|
4
|
+
|
|
5
|
+
// src/htlkg/virtual-modules.ts
|
|
6
|
+
function serializePattern(pattern) {
|
|
7
|
+
if (pattern instanceof RegExp) {
|
|
8
|
+
return `new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(pattern.flags)})`;
|
|
9
|
+
}
|
|
10
|
+
return JSON.stringify(pattern);
|
|
11
|
+
}
|
|
12
|
+
function serializePatterns(patterns) {
|
|
13
|
+
if (!patterns || patterns.length === 0) return "[]";
|
|
14
|
+
return `[${patterns.map(serializePattern).join(", ")}]`;
|
|
15
|
+
}
|
|
16
|
+
function createVirtualModulePlugin(authConfig, loginPageConfig, amplifyConfig) {
|
|
17
|
+
return {
|
|
18
|
+
name: "htlkg-config",
|
|
19
|
+
resolveId(id) {
|
|
20
|
+
if (id === "virtual:htlkg-config") {
|
|
21
|
+
return "\0virtual:htlkg-config";
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
load(id) {
|
|
25
|
+
if (id === "\0virtual:htlkg-config") {
|
|
26
|
+
const serializedAuthConfig = `{
|
|
27
|
+
publicRoutes: ${serializePatterns(authConfig.publicRoutes || [])},
|
|
28
|
+
authenticatedRoutes: ${serializePatterns(authConfig.authenticatedRoutes || [])},
|
|
29
|
+
adminRoutes: ${serializePatterns(authConfig.adminRoutes || [])},
|
|
30
|
+
brandRoutes: ${JSON.stringify(authConfig.brandRoutes || [])},
|
|
31
|
+
loginUrl: ${JSON.stringify(authConfig.loginUrl || "/login")}
|
|
32
|
+
}`;
|
|
33
|
+
const serializedAmplifyConfig = amplifyConfig ? JSON.stringify(amplifyConfig) : "null";
|
|
34
|
+
const serializedLoginPageConfig = loginPageConfig !== false && loginPageConfig !== null ? JSON.stringify(loginPageConfig) : "null";
|
|
35
|
+
return `export const routeGuardConfig = ${serializedAuthConfig};
|
|
36
|
+
export const loginPageConfig = ${serializedLoginPageConfig};
|
|
37
|
+
export const amplifyConfig = ${serializedAmplifyConfig};`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
var virtualModuleTypes = `
|
|
43
|
+
declare module 'virtual:htlkg-config' {
|
|
44
|
+
import type { RouteGuardConfig, LoginPageConfig } from '@htlkg/astro/htlkg/config';
|
|
45
|
+
|
|
46
|
+
export const routeGuardConfig: RouteGuardConfig;
|
|
47
|
+
export const loginPageConfig: LoginPageConfig | null;
|
|
48
|
+
export const amplifyConfig: Record<string, unknown> | null;
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
// src/htlkg/index.ts
|
|
53
|
+
import vueDevTools from "vite-plugin-vue-devtools";
|
|
54
|
+
var DEFAULT_ENV_VARS = [
|
|
55
|
+
"PUBLIC_COGNITO_USER_POOL_ID",
|
|
56
|
+
"PUBLIC_COGNITO_USER_POOL_CLIENT_ID"
|
|
57
|
+
];
|
|
58
|
+
function htlkg(options = {}) {
|
|
59
|
+
const {
|
|
60
|
+
auth = {},
|
|
61
|
+
loginPage = { path: "/login", title: "Sign In", redirectUrl: "/admin" },
|
|
62
|
+
validateEnv = true,
|
|
63
|
+
requiredEnvVars = DEFAULT_ENV_VARS,
|
|
64
|
+
tailwind: tailwindOptions,
|
|
65
|
+
amplify,
|
|
66
|
+
vueAppSetup = "auto"
|
|
67
|
+
} = options;
|
|
68
|
+
const integrations = [];
|
|
69
|
+
if (tailwindOptions !== false) {
|
|
70
|
+
const tailwindConfig = typeof tailwindOptions === "object" ? tailwindOptions : void 0;
|
|
71
|
+
integrations.push(
|
|
72
|
+
tailwind(tailwindConfig)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
const useFullVueSetup = vueAppSetup === "full";
|
|
76
|
+
if (useFullVueSetup) {
|
|
77
|
+
integrations.push(vue({ appEntrypoint: "@htlkg/astro/vue-app-setup" }));
|
|
78
|
+
} else {
|
|
79
|
+
integrations.push(vue());
|
|
80
|
+
}
|
|
81
|
+
integrations.push({
|
|
82
|
+
name: "@htlkg/astro",
|
|
83
|
+
hooks: {
|
|
84
|
+
"astro:config:setup": ({ updateConfig, addMiddleware, injectRoute, logger }) => {
|
|
85
|
+
try {
|
|
86
|
+
logger.info(useFullVueSetup ? "Vue configured with Amplify app setup" : "Vue configured (basic mode)");
|
|
87
|
+
if (amplify) {
|
|
88
|
+
logger.info("Amplify configuration provided");
|
|
89
|
+
}
|
|
90
|
+
if (validateEnv && !amplify && useFullVueSetup) {
|
|
91
|
+
const missing = requiredEnvVars.filter((v) => !process.env[v]);
|
|
92
|
+
if (missing.length > 0) {
|
|
93
|
+
logger.warn(`Missing env vars: ${missing.join(", ")}`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const virtualModulePlugin = createVirtualModulePlugin(auth, loginPage, amplify || null);
|
|
97
|
+
const vitePlugins = [virtualModulePlugin];
|
|
98
|
+
if (import.meta.env?.DEV !== false) {
|
|
99
|
+
vitePlugins.push(vueDevTools());
|
|
100
|
+
logger.info("Vue DevTools enabled");
|
|
101
|
+
}
|
|
102
|
+
updateConfig({ vite: { plugins: vitePlugins } });
|
|
103
|
+
if (useFullVueSetup) {
|
|
104
|
+
addMiddleware({ entrypoint: "@htlkg/astro/middleware", order: "pre" });
|
|
105
|
+
logger.info("Authentication middleware injected");
|
|
106
|
+
}
|
|
107
|
+
if (loginPage !== false && useFullVueSetup) {
|
|
108
|
+
const loginPath = loginPage.path || "/login";
|
|
109
|
+
injectRoute({
|
|
110
|
+
pattern: loginPath,
|
|
111
|
+
entrypoint: "@htlkg/astro/auth/LoginPage.astro",
|
|
112
|
+
prerender: false
|
|
113
|
+
});
|
|
114
|
+
logger.info(`Login page injected at ${loginPath}`);
|
|
115
|
+
}
|
|
116
|
+
logger.info("htlkg integration configured");
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const msg = error instanceof Error ? error.message : "Unknown error";
|
|
119
|
+
logger.error(`htlkg configuration failed: ${msg}`);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
"astro:config:done": ({ injectTypes }) => {
|
|
124
|
+
injectTypes({
|
|
125
|
+
filename: "htlkg.d.ts",
|
|
126
|
+
content: `
|
|
127
|
+
import type { AuthUser } from '@htlkg/core/types';
|
|
128
|
+
|
|
129
|
+
declare global {
|
|
130
|
+
namespace App {
|
|
131
|
+
interface Locals {
|
|
132
|
+
user: AuthUser | null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
${virtualModuleTypes}
|
|
138
|
+
|
|
139
|
+
export {};
|
|
140
|
+
`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
return integrations;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export {
|
|
149
|
+
htlkg
|
|
150
|
+
};
|
|
151
|
+
//# sourceMappingURL=chunk-XOY5BM3N.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/htlkg/index.ts","../src/htlkg/virtual-modules.ts"],"sourcesContent":["/**\n * htlkg Astro Integration\n * \n * Supports static, hybrid, and full SSR output modes.\n */\n\nimport tailwind from '@astrojs/tailwind';\nimport vue from '@astrojs/vue';\nimport type { AstroIntegration } from 'astro';\nimport type { HtlkgIntegrationOptions } from './config.js';\nimport { createVirtualModulePlugin, virtualModuleTypes } from './virtual-modules.js';\nimport vueDevTools from 'vite-plugin-vue-devtools';\n\nconst DEFAULT_ENV_VARS = [\n\t'PUBLIC_COGNITO_USER_POOL_ID',\n\t'PUBLIC_COGNITO_USER_POOL_CLIENT_ID'\n];\n\nexport function htlkg(\n\toptions: HtlkgIntegrationOptions = {},\n): AstroIntegration | AstroIntegration[] {\n\tconst {\n\t\tauth = {},\n\t\tloginPage = { path: '/login', title: 'Sign In', redirectUrl: '/admin' },\n\t\tvalidateEnv = true,\n\t\trequiredEnvVars = DEFAULT_ENV_VARS,\n\t\ttailwind: tailwindOptions,\n\t\tamplify,\n\t\tvueAppSetup = 'auto',\n\t} = options;\n\n\tconst integrations: AstroIntegration[] = [];\n\n\t// Add Tailwind integration (enabled by default)\n\tif (tailwindOptions !== false) {\n\t\tconst tailwindConfig =\n\t\t\ttypeof tailwindOptions === 'object' ? tailwindOptions : undefined;\n\t\tintegrations.push(\n\t\t\ttailwind(tailwindConfig as Parameters<typeof tailwind>[0]),\n\t\t);\n\t}\n\n\t// Determine Vue setup mode:\n\t// - 'full': Use Amplify app entrypoint (requires SSR)\n\t// - 'basic': Basic Vue without app entrypoint (works with static)\n\t// - 'auto': Default to 'basic' for compatibility with static builds\n\tconst useFullVueSetup = vueAppSetup === 'full';\n\n\t// Add Vue integration\n\tif (useFullVueSetup) {\n\t\tintegrations.push(vue({ appEntrypoint: '@htlkg/astro/vue-app-setup' }));\n\t} else {\n\t\tintegrations.push(vue());\n\t}\n\n\t// Add the main htlkg integration\n\tintegrations.push({\n\t\tname: '@htlkg/astro',\n\t\thooks: {\n\t\t\t'astro:config:setup': ({ updateConfig, addMiddleware, injectRoute, logger }) => {\n\t\t\t\ttry {\n\t\t\t\t\tlogger.info(useFullVueSetup \n\t\t\t\t\t\t? 'Vue configured with Amplify app setup' \n\t\t\t\t\t\t: 'Vue configured (basic mode)');\n\n\t\t\t\t\tif (amplify) {\n\t\t\t\t\t\tlogger.info('Amplify configuration provided');\n\t\t\t\t\t}\n\n\t\t\t\t\t// Validate env vars (only for full setup)\n\t\t\t\t\tif (validateEnv && !amplify && useFullVueSetup) {\n\t\t\t\t\t\tconst missing = requiredEnvVars.filter(v => !process.env[v]);\n\t\t\t\t\t\tif (missing.length > 0) {\n\t\t\t\t\t\t\tlogger.warn(`Missing env vars: ${missing.join(', ')}`);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Create Vite virtual module plugin\n\t\t\t\t\tconst virtualModulePlugin = createVirtualModulePlugin(auth, loginPage, amplify || null);\n\t\t\t\t\tconst vitePlugins: any[] = [virtualModulePlugin];\n\t\t\t\t\t\n\t\t\t\t\tif (import.meta.env?.DEV !== false) {\n\t\t\t\t\t\tvitePlugins.push(vueDevTools());\n\t\t\t\t\t\tlogger.info('Vue DevTools enabled');\n\t\t\t\t\t}\n\n\t\t\t\t\tupdateConfig({ vite: { plugins: vitePlugins } });\n\n\t\t\t\t\t// Inject middleware (only for full setup)\n\t\t\t\t\tif (useFullVueSetup) {\n\t\t\t\t\t\taddMiddleware({ entrypoint: '@htlkg/astro/middleware', order: 'pre' });\n\t\t\t\t\t\tlogger.info('Authentication middleware injected');\n\t\t\t\t\t}\n\n\t\t\t\t\t// Inject login page (only for full setup)\n\t\t\t\t\tif (loginPage !== false && useFullVueSetup) {\n\t\t\t\t\t\tconst loginPath = loginPage.path || '/login';\n\t\t\t\t\t\tinjectRoute({\n\t\t\t\t\t\t\tpattern: loginPath,\n\t\t\t\t\t\t\tentrypoint: '@htlkg/astro/auth/LoginPage.astro',\n\t\t\t\t\t\t\tprerender: false,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tlogger.info(`Login page injected at ${loginPath}`);\n\t\t\t\t\t}\n\n\t\t\t\t\tlogger.info('htlkg integration configured');\n\t\t\t\t} catch (error) {\n\t\t\t\t\tconst msg = error instanceof Error ? error.message : 'Unknown error';\n\t\t\t\t\tlogger.error(`htlkg configuration failed: ${msg}`);\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t},\n\t\t\t'astro:config:done': ({ injectTypes }) => {\n\t\t\t\tinjectTypes({\n\t\t\t\t\tfilename: 'htlkg.d.ts',\n\t\t\t\t\tcontent: `\nimport type { AuthUser } from '@htlkg/core/types';\n\ndeclare global {\n namespace App {\n interface Locals {\n user: AuthUser | null;\n }\n }\n}\n\n${virtualModuleTypes}\n\nexport {};\n`,\n\t\t\t\t});\n\t\t\t},\n\t\t},\n\t});\n\n\treturn integrations;\n}\n","/**\n * Virtual module setup for htlkg integration\n * Creates Vite virtual modules to pass configuration to middleware and pages\n */\n\nimport type { RouteGuardConfig, LoginPageConfig, RoutePattern } from './config.js';\n\n/**\n * Serialize a route pattern (RegExp or string) to JavaScript code\n */\nfunction serializePattern(pattern: RegExp | string): string {\n\tif (pattern instanceof RegExp) {\n\t\treturn `new RegExp(${JSON.stringify(pattern.source)}, ${JSON.stringify(pattern.flags)})`;\n\t}\n\treturn JSON.stringify(pattern);\n}\n\n/**\n * Serialize an array of route patterns to JavaScript code\n */\nfunction serializePatterns(patterns: RoutePattern[]): string {\n\tif (!patterns || patterns.length === 0) return '[]';\n\treturn `[${patterns.map(serializePattern).join(', ')}]`;\n}\n\n/**\n * Create the virtual module plugin for Vite\n * This plugin provides configuration to middleware and pages at runtime\n */\nexport function createVirtualModulePlugin(\n\tauthConfig: RouteGuardConfig,\n\tloginPageConfig: LoginPageConfig | false | null,\n\tamplifyConfig: Record<string, unknown> | null\n) {\n\treturn {\n\t\tname: 'htlkg-config',\n\t\tresolveId(id: string) {\n\t\t\tif (id === 'virtual:htlkg-config') {\n\t\t\t\treturn '\\0virtual:htlkg-config';\n\t\t\t}\n\t\t},\n\t\tload(id: string) {\n\t\t\tif (id === '\\0virtual:htlkg-config') {\n\t\t\t\t// Serialize auth configuration with RegExp support\n\t\t\t\tconst serializedAuthConfig = `{\n\t\t\t\t\tpublicRoutes: ${serializePatterns(authConfig.publicRoutes || [])},\n\t\t\t\t\tauthenticatedRoutes: ${serializePatterns(authConfig.authenticatedRoutes || [])},\n\t\t\t\t\tadminRoutes: ${serializePatterns(authConfig.adminRoutes || [])},\n\t\t\t\t\tbrandRoutes: ${JSON.stringify(authConfig.brandRoutes || [])},\n\t\t\t\t\tloginUrl: ${JSON.stringify(authConfig.loginUrl || '/login')}\n\t\t\t\t}`;\n\n\t\t\t\tconst serializedAmplifyConfig = amplifyConfig\n\t\t\t\t\t? JSON.stringify(amplifyConfig)\n\t\t\t\t\t: 'null';\n\n\t\t\t\tconst serializedLoginPageConfig = loginPageConfig !== false && loginPageConfig !== null\n\t\t\t\t\t? JSON.stringify(loginPageConfig)\n\t\t\t\t\t: 'null';\n\n\t\t\t\treturn `export const routeGuardConfig = ${serializedAuthConfig};\nexport const loginPageConfig = ${serializedLoginPageConfig};\nexport const amplifyConfig = ${serializedAmplifyConfig};`;\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Type definitions for the virtual module\n * This should be injected into the project's type definitions\n */\nexport const virtualModuleTypes = `\ndeclare module 'virtual:htlkg-config' {\n import type { RouteGuardConfig, LoginPageConfig } from '@htlkg/astro/htlkg/config';\n \n export const routeGuardConfig: RouteGuardConfig;\n export const loginPageConfig: LoginPageConfig | null;\n export const amplifyConfig: Record<string, unknown> | null;\n}\n`;\n"],"mappings":"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","names":[]}
|
package/dist/htlkg/config.js
CHANGED