@gridframe/server 1.0.0 → 1.0.1
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/dist/index.d.ts +109 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +346 -0
- package/package.json +12 -4
- package/src/index.test.ts +0 -480
- package/src/index.ts +0 -710
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type CardDeeplinkConfig, type DashboardCardLayout, type DashboardFooterConfig, type DashboardLayoutItem, type DashboardSummary, type PanelCardDataResponse, type VisualizationType } from "@gridframe/core";
|
|
2
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
3
|
+
type CardLibraryTemplate = {
|
|
4
|
+
key: string;
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
visualization: VisualizationType;
|
|
8
|
+
defaultLayout: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
deeplinkLabel?: string;
|
|
13
|
+
};
|
|
14
|
+
type DashboardSeedCard = {
|
|
15
|
+
libraryItemKey: string;
|
|
16
|
+
layout?: DashboardCardLayout;
|
|
17
|
+
};
|
|
18
|
+
type DashboardSeed = {
|
|
19
|
+
title: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
footer?: DashboardFooterConfig;
|
|
22
|
+
cards: DashboardSeedCard[];
|
|
23
|
+
};
|
|
24
|
+
type PersistedDashboardCard = {
|
|
25
|
+
id: string;
|
|
26
|
+
dashboardId: string;
|
|
27
|
+
libraryItemKey?: string;
|
|
28
|
+
name: string;
|
|
29
|
+
visualization: VisualizationType;
|
|
30
|
+
deeplink?: Omit<CardDeeplinkConfig, "href">;
|
|
31
|
+
layout: DashboardCardLayout;
|
|
32
|
+
sortOrder: number;
|
|
33
|
+
};
|
|
34
|
+
type PersistedDashboard = {
|
|
35
|
+
id: string;
|
|
36
|
+
ownerUserId: string;
|
|
37
|
+
title: string;
|
|
38
|
+
description?: string;
|
|
39
|
+
footer?: DashboardFooterConfig;
|
|
40
|
+
isDefault: boolean;
|
|
41
|
+
revision: number;
|
|
42
|
+
cards: PersistedDashboardCard[];
|
|
43
|
+
};
|
|
44
|
+
type DashboardBootstrap = {
|
|
45
|
+
dashboards: DashboardSummary[];
|
|
46
|
+
dashboard: PersistedDashboard;
|
|
47
|
+
};
|
|
48
|
+
type DashboardCardCreate = Omit<PersistedDashboardCard, "id" | "dashboardId" | "sortOrder">;
|
|
49
|
+
interface DashboardRepository {
|
|
50
|
+
bootstrap(ownerUserId: string, dashboardId: string | undefined, seed: DashboardSeed, cardLibrary: readonly CardLibraryTemplate[]): MaybePromise<DashboardBootstrap>;
|
|
51
|
+
loadDashboard(ownerUserId: string, dashboardId: string): MaybePromise<PersistedDashboard>;
|
|
52
|
+
updateLayout(ownerUserId: string, dashboardId: string, revision: number, cards: DashboardLayoutItem[]): MaybePromise<PersistedDashboard>;
|
|
53
|
+
updateCardName(ownerUserId: string, dashboardId: string, cardId: string, revision: number, name: string): MaybePromise<PersistedDashboard>;
|
|
54
|
+
addCard(ownerUserId: string, dashboardId: string, revision: number, card: DashboardCardCreate): MaybePromise<PersistedDashboard>;
|
|
55
|
+
removeCard(ownerUserId: string, dashboardId: string, cardId: string, revision: number): MaybePromise<PersistedDashboard>;
|
|
56
|
+
findOwnedCard(ownerUserId: string, dashboardId: string, cardId: string): MaybePromise<PersistedDashboardCard | undefined>;
|
|
57
|
+
}
|
|
58
|
+
type DashboardHandlerOptions = {
|
|
59
|
+
repository: DashboardRepository;
|
|
60
|
+
cardLibrary: readonly CardLibraryTemplate[] | ((context: DashboardContext) => MaybePromise<readonly CardLibraryTemplate[]>);
|
|
61
|
+
defaultDashboard: (context: DashboardContext) => MaybePromise<DashboardSeed>;
|
|
62
|
+
resolveCardData: (input: CardDataResolverInput) => MaybePromise<PanelCardDataResponse>;
|
|
63
|
+
urls?: DashboardUrlOptions;
|
|
64
|
+
};
|
|
65
|
+
type DashboardUrlOptions = {
|
|
66
|
+
apiBasePath?: string;
|
|
67
|
+
dashboardBasePath?: string;
|
|
68
|
+
};
|
|
69
|
+
type DashboardContext = {
|
|
70
|
+
userId: string;
|
|
71
|
+
};
|
|
72
|
+
type DashboardIdentity = DashboardContext & {
|
|
73
|
+
dashboardId: string;
|
|
74
|
+
};
|
|
75
|
+
type CardIdentity = DashboardIdentity & {
|
|
76
|
+
cardId: string;
|
|
77
|
+
};
|
|
78
|
+
type CardDataResolverInput = CardIdentity & {
|
|
79
|
+
card: PersistedDashboardCard;
|
|
80
|
+
request: Request;
|
|
81
|
+
};
|
|
82
|
+
declare class DashboardNotFoundError extends Error {
|
|
83
|
+
constructor();
|
|
84
|
+
}
|
|
85
|
+
declare class DashboardRevisionConflictError extends Error {
|
|
86
|
+
constructor();
|
|
87
|
+
}
|
|
88
|
+
declare class DashboardInvalidLayoutError extends Error {
|
|
89
|
+
readonly errors: string[];
|
|
90
|
+
constructor(errors: string[]);
|
|
91
|
+
}
|
|
92
|
+
declare class DashboardCardAlreadyAddedError extends Error {
|
|
93
|
+
constructor();
|
|
94
|
+
}
|
|
95
|
+
declare class DashboardInvalidLibraryItemError extends Error {
|
|
96
|
+
constructor();
|
|
97
|
+
}
|
|
98
|
+
declare function createDashboardHandlers(options: DashboardHandlerOptions): {
|
|
99
|
+
bootstrap: (request: Request, context: DashboardContext) => Promise<Response>;
|
|
100
|
+
updateLayout: (request: Request, identity: DashboardIdentity) => Promise<Response>;
|
|
101
|
+
updateCard: (request: Request, identity: CardIdentity) => Promise<Response>;
|
|
102
|
+
listCardLibrary: (_request: Request, identity: DashboardIdentity) => Promise<Response>;
|
|
103
|
+
addCard: (request: Request, identity: DashboardIdentity) => Promise<Response>;
|
|
104
|
+
removeCard: (request: Request, identity: CardIdentity) => Promise<Response>;
|
|
105
|
+
fetchCardData: (request: Request, identity: CardIdentity) => Promise<Response>;
|
|
106
|
+
};
|
|
107
|
+
export { DashboardCardAlreadyAddedError, DashboardInvalidLayoutError, DashboardInvalidLibraryItemError, DashboardNotFoundError, DashboardRevisionConflictError, createDashboardHandlers, };
|
|
108
|
+
export type { CardDataResolverInput, CardLibraryTemplate, DashboardBootstrap, DashboardHandlerOptions, DashboardRepository, DashboardSeed, DashboardSeedCard, PersistedDashboard, PersistedDashboardCard, };
|
|
109
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DashboardRevisionConflictError = exports.DashboardNotFoundError = exports.DashboardInvalidLibraryItemError = exports.DashboardInvalidLayoutError = exports.DashboardCardAlreadyAddedError = void 0;
|
|
4
|
+
exports.createDashboardHandlers = createDashboardHandlers;
|
|
5
|
+
const core_1 = require("@gridframe/core");
|
|
6
|
+
const core_2 = require("@gridframe/core");
|
|
7
|
+
class DashboardNotFoundError extends Error {
|
|
8
|
+
constructor() {
|
|
9
|
+
super("Dashboard not found");
|
|
10
|
+
this.name = "DashboardNotFoundError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
exports.DashboardNotFoundError = DashboardNotFoundError;
|
|
14
|
+
class DashboardRevisionConflictError extends Error {
|
|
15
|
+
constructor() {
|
|
16
|
+
super("Dashboard revision conflict");
|
|
17
|
+
this.name = "DashboardRevisionConflictError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.DashboardRevisionConflictError = DashboardRevisionConflictError;
|
|
21
|
+
class DashboardInvalidLayoutError extends Error {
|
|
22
|
+
errors;
|
|
23
|
+
constructor(errors) {
|
|
24
|
+
super(errors.join("; "));
|
|
25
|
+
this.errors = errors;
|
|
26
|
+
this.name = "DashboardInvalidLayoutError";
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.DashboardInvalidLayoutError = DashboardInvalidLayoutError;
|
|
30
|
+
class DashboardCardAlreadyAddedError extends Error {
|
|
31
|
+
constructor() {
|
|
32
|
+
super("Card is already on this Dashboard");
|
|
33
|
+
this.name = "DashboardCardAlreadyAddedError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.DashboardCardAlreadyAddedError = DashboardCardAlreadyAddedError;
|
|
37
|
+
class DashboardInvalidLibraryItemError extends Error {
|
|
38
|
+
constructor() {
|
|
39
|
+
super("Unknown Card library item");
|
|
40
|
+
this.name = "DashboardInvalidLibraryItemError";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
exports.DashboardInvalidLibraryItemError = DashboardInvalidLibraryItemError;
|
|
44
|
+
function createDashboardHandlers(options) {
|
|
45
|
+
const urls = {
|
|
46
|
+
apiBasePath: options.urls?.apiBasePath ?? "/api/gridframe",
|
|
47
|
+
dashboardBasePath: options.urls?.dashboardBasePath ?? "/gridframe",
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
bootstrap: async (request, context) => {
|
|
51
|
+
const parsedRequest = core_1.DashboardBootstrapRequestSchema.safeParse(await readJson(request));
|
|
52
|
+
if (!parsedRequest.success || !isIdentitySegment(context.userId)) {
|
|
53
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid bootstrap request");
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
const [seed, cardLibrary] = await Promise.all([
|
|
57
|
+
options.defaultDashboard(context),
|
|
58
|
+
resolveCardLibrary(options.cardLibrary, context),
|
|
59
|
+
]);
|
|
60
|
+
const result = await options.repository.bootstrap(context.userId, parsedRequest.data.dashboardId, seed, cardLibrary);
|
|
61
|
+
return Response.json(serializeDashboardBootstrap(result, urls));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
if (error instanceof DashboardNotFoundError) {
|
|
65
|
+
return errorResponse(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
|
|
66
|
+
}
|
|
67
|
+
if (error instanceof DashboardInvalidLibraryItemError) {
|
|
68
|
+
return errorResponse(400, "INVALID_REQUEST", "Unknown Card library item");
|
|
69
|
+
}
|
|
70
|
+
return errorResponse(500, "DASHBOARD_LOAD_FAILED", "Dashboard could not be loaded");
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
updateLayout: async (request, identity) => {
|
|
74
|
+
const parsed = core_1.UpdateDashboardLayoutRequestSchema.safeParse(await readJson(request));
|
|
75
|
+
const revision = parsed.success
|
|
76
|
+
? parseRevision(parsed.data.revision)
|
|
77
|
+
: undefined;
|
|
78
|
+
if (!parsed.success || revision === undefined || !isDashboard(identity)) {
|
|
79
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Dashboard layout request");
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const dashboard = await options.repository.loadDashboard(identity.userId, identity.dashboardId);
|
|
83
|
+
const validation = (0, core_2.validateDashboardLayout)(parsed.data.cards, dashboard.cards.map((card) => card.id));
|
|
84
|
+
if (!validation.valid) {
|
|
85
|
+
throw new DashboardInvalidLayoutError(validation.errors);
|
|
86
|
+
}
|
|
87
|
+
const updated = await options.repository.updateLayout(identity.userId, identity.dashboardId, revision, parsed.data.cards);
|
|
88
|
+
return Response.json(serializeDashboardDocument(updated, urls));
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
return mutationError(error, "Invalid Dashboard layout");
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
updateCard: async (request, identity) => {
|
|
95
|
+
const parsed = core_1.UpdateDashboardCardRequestSchema.safeParse(await readJson(request));
|
|
96
|
+
const revision = parsed.success
|
|
97
|
+
? parseRevision(parsed.data.revision)
|
|
98
|
+
: undefined;
|
|
99
|
+
const name = parsed.success ? parsed.data.name.trim() : "";
|
|
100
|
+
if (!parsed.success ||
|
|
101
|
+
revision === undefined ||
|
|
102
|
+
!name ||
|
|
103
|
+
!isCard(identity)) {
|
|
104
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Card update request");
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const updated = await options.repository.updateCardName(identity.userId, identity.dashboardId, identity.cardId, revision, name);
|
|
108
|
+
return Response.json(serializeDashboardDocument(updated, urls));
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
return mutationError(error, "Invalid Card update request");
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
listCardLibrary: async (_request, identity) => {
|
|
115
|
+
if (!isDashboard(identity)) {
|
|
116
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Card library request");
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const [dashboard, cardLibrary] = await Promise.all([
|
|
120
|
+
options.repository.loadDashboard(identity.userId, identity.dashboardId),
|
|
121
|
+
resolveCardLibrary(options.cardLibrary, identity),
|
|
122
|
+
]);
|
|
123
|
+
return Response.json(buildCardLibraryResponse(dashboard, cardLibrary));
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
return libraryError(error);
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
addCard: async (request, identity) => {
|
|
130
|
+
const parsed = core_1.AddDashboardCardRequestSchema.safeParse(await readJson(request));
|
|
131
|
+
const revision = parsed.success
|
|
132
|
+
? parseRevision(parsed.data.revision)
|
|
133
|
+
: undefined;
|
|
134
|
+
if (!parsed.success || revision === undefined || !isDashboard(identity)) {
|
|
135
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Card add request");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
const [dashboard, cardLibrary] = await Promise.all([
|
|
139
|
+
options.repository.loadDashboard(identity.userId, identity.dashboardId),
|
|
140
|
+
resolveCardLibrary(options.cardLibrary, identity),
|
|
141
|
+
]);
|
|
142
|
+
const template = cardLibrary.find((item) => item.key === parsed.data.libraryItemKey);
|
|
143
|
+
if (!template) {
|
|
144
|
+
throw new DashboardInvalidLibraryItemError();
|
|
145
|
+
}
|
|
146
|
+
if (dashboard.cards.some((card) => card.libraryItemKey === parsed.data.libraryItemKey)) {
|
|
147
|
+
throw new DashboardCardAlreadyAddedError();
|
|
148
|
+
}
|
|
149
|
+
const updated = await options.repository.addCard(identity.userId, identity.dashboardId, revision, {
|
|
150
|
+
libraryItemKey: template.key,
|
|
151
|
+
name: template.name,
|
|
152
|
+
visualization: template.visualization,
|
|
153
|
+
deeplink: template.deeplinkLabel
|
|
154
|
+
? { label: template.deeplinkLabel }
|
|
155
|
+
: undefined,
|
|
156
|
+
layout: firstAvailableLayout(dashboard.cards, template.defaultLayout),
|
|
157
|
+
});
|
|
158
|
+
return Response.json({
|
|
159
|
+
dashboard: serializeDashboardDocument(updated, urls),
|
|
160
|
+
cardLibrary: buildCardLibraryResponse(updated, cardLibrary),
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
return libraryError(error);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
removeCard: async (request, identity) => {
|
|
168
|
+
const parsed = core_1.RemoveDashboardCardRequestSchema.safeParse(await readJson(request));
|
|
169
|
+
const revision = parsed.success
|
|
170
|
+
? parseRevision(parsed.data.revision)
|
|
171
|
+
: undefined;
|
|
172
|
+
if (!parsed.success || revision === undefined || !isCard(identity)) {
|
|
173
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Card remove request");
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const cardLibrary = await resolveCardLibrary(options.cardLibrary, identity);
|
|
177
|
+
const updated = await options.repository.removeCard(identity.userId, identity.dashboardId, identity.cardId, revision);
|
|
178
|
+
return Response.json({
|
|
179
|
+
dashboard: serializeDashboardDocument(updated, urls),
|
|
180
|
+
cardLibrary: buildCardLibraryResponse(updated, cardLibrary),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
return libraryError(error);
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
fetchCardData: async (request, identity) => {
|
|
188
|
+
if (!isCard(identity)) {
|
|
189
|
+
return errorResponse(400, "INVALID_REQUEST", "Invalid Card data request");
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
const card = await options.repository.findOwnedCard(identity.userId, identity.dashboardId, identity.cardId);
|
|
193
|
+
if (!card) {
|
|
194
|
+
return errorResponse(404, "DASHBOARD_CARD_NOT_FOUND", "Dashboard Card not found");
|
|
195
|
+
}
|
|
196
|
+
const result = await options.resolveCardData({
|
|
197
|
+
...identity,
|
|
198
|
+
card,
|
|
199
|
+
request,
|
|
200
|
+
});
|
|
201
|
+
const parsed = core_1.PanelCardDataResponseSchema.safeParse(result);
|
|
202
|
+
if (!parsed.success) {
|
|
203
|
+
return cardQueryFailed();
|
|
204
|
+
}
|
|
205
|
+
return Response.json(parsed.data);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return cardQueryFailed();
|
|
209
|
+
}
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function serializeDashboardBootstrap(result, urls) {
|
|
214
|
+
return {
|
|
215
|
+
dashboards: result.dashboards,
|
|
216
|
+
dashboard: serializeDashboardDocument(result.dashboard, urls),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
function serializeDashboardDocument(dashboard, urls) {
|
|
220
|
+
const ownerId = encodeURIComponent(dashboard.ownerUserId);
|
|
221
|
+
const dashboardId = encodeURIComponent(dashboard.id);
|
|
222
|
+
return core_1.DashboardDocumentSchema.parse({
|
|
223
|
+
id: dashboard.id,
|
|
224
|
+
revision: String(dashboard.revision),
|
|
225
|
+
config: {
|
|
226
|
+
title: dashboard.title,
|
|
227
|
+
description: dashboard.description,
|
|
228
|
+
footer: dashboard.footer,
|
|
229
|
+
cards: dashboard.cards.map((card) => {
|
|
230
|
+
const cardId = encodeURIComponent(card.id);
|
|
231
|
+
return {
|
|
232
|
+
id: card.id,
|
|
233
|
+
name: card.name,
|
|
234
|
+
visualization: card.visualization,
|
|
235
|
+
query: `${urls.apiBasePath}/users/${ownerId}` +
|
|
236
|
+
`/dashboards/${dashboardId}/cards/${cardId}/data`,
|
|
237
|
+
deeplink: card.deeplink
|
|
238
|
+
? {
|
|
239
|
+
href: `${urls.dashboardBasePath}/users/${ownerId}` +
|
|
240
|
+
`/dashboards/${dashboardId}/cards/${cardId}`,
|
|
241
|
+
label: card.deeplink.label,
|
|
242
|
+
}
|
|
243
|
+
: undefined,
|
|
244
|
+
layout: card.layout,
|
|
245
|
+
};
|
|
246
|
+
}),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
function buildCardLibraryResponse(dashboard, cardLibrary) {
|
|
251
|
+
const installed = new Map(dashboard.cards.map((card) => [card.libraryItemKey, card.id]));
|
|
252
|
+
return core_1.CardLibraryResponseSchema.parse({
|
|
253
|
+
items: cardLibrary.map((template) => ({
|
|
254
|
+
key: template.key,
|
|
255
|
+
name: template.name,
|
|
256
|
+
description: template.description,
|
|
257
|
+
visualization: template.visualization,
|
|
258
|
+
defaultLayout: template.defaultLayout,
|
|
259
|
+
addedCardId: installed.get(template.key),
|
|
260
|
+
})),
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function firstAvailableLayout(cards, size) {
|
|
264
|
+
if (size.width > core_1.DASHBOARD_GRID_COLUMNS) {
|
|
265
|
+
throw new DashboardInvalidLayoutError([
|
|
266
|
+
`Card width ${size.width} exceeds the ${core_1.DASHBOARD_GRID_COLUMNS}-column grid`,
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
for (let y = 0;; y += 1) {
|
|
270
|
+
for (let x = 0; x + size.width <= core_1.DASHBOARD_GRID_COLUMNS; x += 1) {
|
|
271
|
+
const candidate = { x, y, ...size };
|
|
272
|
+
if (!cards.some((card) => layoutsOverlap(candidate, card.layout))) {
|
|
273
|
+
return candidate;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
function layoutsOverlap(a, b) {
|
|
279
|
+
return (a.x < b.x + b.width &&
|
|
280
|
+
a.x + a.width > b.x &&
|
|
281
|
+
a.y < b.y + b.height &&
|
|
282
|
+
a.y + a.height > b.y);
|
|
283
|
+
}
|
|
284
|
+
async function resolveCardLibrary(cardLibrary, context) {
|
|
285
|
+
return typeof cardLibrary === "function"
|
|
286
|
+
? cardLibrary(context)
|
|
287
|
+
: cardLibrary;
|
|
288
|
+
}
|
|
289
|
+
function mutationError(error, invalidMessage) {
|
|
290
|
+
if (error instanceof DashboardRevisionConflictError) {
|
|
291
|
+
return errorResponse(409, "REVISION_CONFLICT", "Dashboard was changed by another request");
|
|
292
|
+
}
|
|
293
|
+
if (error instanceof DashboardNotFoundError) {
|
|
294
|
+
return errorResponse(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
|
|
295
|
+
}
|
|
296
|
+
if (error instanceof DashboardInvalidLayoutError) {
|
|
297
|
+
return errorResponse(400, "INVALID_REQUEST", invalidMessage);
|
|
298
|
+
}
|
|
299
|
+
return errorResponse(500, "DASHBOARD_LOAD_FAILED", "Dashboard could not be updated");
|
|
300
|
+
}
|
|
301
|
+
function libraryError(error) {
|
|
302
|
+
if (error instanceof DashboardRevisionConflictError) {
|
|
303
|
+
return errorResponse(409, "REVISION_CONFLICT", "Dashboard was changed by another request");
|
|
304
|
+
}
|
|
305
|
+
if (error instanceof DashboardCardAlreadyAddedError) {
|
|
306
|
+
return errorResponse(409, "CARD_ALREADY_ADDED", "Card is already on this Dashboard");
|
|
307
|
+
}
|
|
308
|
+
if (error instanceof DashboardInvalidLibraryItemError) {
|
|
309
|
+
return errorResponse(400, "INVALID_REQUEST", "Unknown Card library item");
|
|
310
|
+
}
|
|
311
|
+
if (error instanceof DashboardNotFoundError) {
|
|
312
|
+
return errorResponse(404, "DASHBOARD_NOT_FOUND", "Dashboard not found");
|
|
313
|
+
}
|
|
314
|
+
return errorResponse(500, "DASHBOARD_LOAD_FAILED", "Dashboard could not be updated");
|
|
315
|
+
}
|
|
316
|
+
function errorResponse(status, code, message) {
|
|
317
|
+
return Response.json(core_1.DashboardApiErrorSchema.parse({ error: { code, message } }), { status });
|
|
318
|
+
}
|
|
319
|
+
async function readJson(request) {
|
|
320
|
+
try {
|
|
321
|
+
return await request.json();
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function parseRevision(revision) {
|
|
328
|
+
if (!/^[1-9]\d*$/.test(revision)) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const value = Number(revision);
|
|
332
|
+
return Number.isSafeInteger(value) ? value : undefined;
|
|
333
|
+
}
|
|
334
|
+
function isDashboard(identity) {
|
|
335
|
+
return (isIdentitySegment(identity.userId) &&
|
|
336
|
+
isIdentitySegment(identity.dashboardId));
|
|
337
|
+
}
|
|
338
|
+
function isCard(identity) {
|
|
339
|
+
return isDashboard(identity) && isIdentitySegment(identity.cardId);
|
|
340
|
+
}
|
|
341
|
+
function isIdentitySegment(value) {
|
|
342
|
+
return value.trim().length > 0 && value.length <= 256;
|
|
343
|
+
}
|
|
344
|
+
function cardQueryFailed() {
|
|
345
|
+
return errorResponse(502, "CARD_QUERY_FAILED", "Card data could not be loaded");
|
|
346
|
+
}
|
package/package.json
CHANGED
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gridframe/server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"main": "./dist/index.js",
|
|
5
|
+
"types": "./dist/index.d.ts",
|
|
4
6
|
"exports": {
|
|
5
|
-
".":
|
|
7
|
+
".": {
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"import": "./dist/index.js",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
6
13
|
},
|
|
7
14
|
"files": [
|
|
8
|
-
"
|
|
15
|
+
"dist",
|
|
9
16
|
"README.md"
|
|
10
17
|
],
|
|
11
18
|
"publishConfig": {
|
|
@@ -19,9 +26,10 @@
|
|
|
19
26
|
"@gridframe/typescript-config": "0.0.0"
|
|
20
27
|
},
|
|
21
28
|
"dependencies": {
|
|
22
|
-
"@gridframe/core": "
|
|
29
|
+
"@gridframe/core": "1.0.1"
|
|
23
30
|
},
|
|
24
31
|
"scripts": {
|
|
32
|
+
"build": "rm -rf dist && tsc",
|
|
25
33
|
"lint": "eslint . --max-warnings 0",
|
|
26
34
|
"test": "vitest run src",
|
|
27
35
|
"check-types": "tsc --noEmit"
|