@txstate-mws/sveltekit-utils 1.1.6 → 1.2.0
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/api.d.ts +37 -0
- package/dist/api.js +98 -2
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +1 -1
package/dist/api.d.ts
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import type { InteractionEvent, ValidatedResponse } from '@txstate-mws/fastify-shared';
|
|
2
2
|
export type APIBaseQueryPayload = string | Record<string, undefined | string | number | (string | number)[]>;
|
|
3
3
|
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
|
|
4
|
+
export interface APIUploadInfo {
|
|
5
|
+
__type: 'APIUploadInfo';
|
|
6
|
+
multipartIndex: number;
|
|
7
|
+
name: string;
|
|
8
|
+
mime: string;
|
|
9
|
+
size: number;
|
|
10
|
+
}
|
|
11
|
+
export type APIBaseProgressFn = (info: {
|
|
12
|
+
loaded: number;
|
|
13
|
+
total: number;
|
|
14
|
+
ratio: number;
|
|
15
|
+
} | undefined) => void;
|
|
4
16
|
/**
|
|
5
17
|
* Provided for convenience in case you are not using APIBase but still want to record navigations
|
|
6
18
|
*
|
|
@@ -25,6 +37,7 @@ export declare class APIBase {
|
|
|
25
37
|
query?: APIBaseQueryPayload;
|
|
26
38
|
inlineValidation?: boolean;
|
|
27
39
|
}): Promise<ReturnType>;
|
|
40
|
+
uploadWithProgress(path: string, formData: FormData, progress: APIBaseProgressFn): Promise<any>;
|
|
28
41
|
get<ReturnType = any>(path: string, query?: APIBaseQueryPayload): Promise<ReturnType>;
|
|
29
42
|
/**
|
|
30
43
|
* Remember to use validatedPost when the user is interacting with a form. That way they
|
|
@@ -61,6 +74,30 @@ export declare class APIBase {
|
|
|
61
74
|
*/
|
|
62
75
|
delete<ReturnType = any>(path: string, query?: APIBaseQueryPayload, body?: any): Promise<ReturnType>;
|
|
63
76
|
graphql<ReturnType = any>(query: string, variables?: any, querySignature?: string): Promise<ReturnType>;
|
|
77
|
+
/**
|
|
78
|
+
* This is a special graphql request method that allows you to upload files. It will
|
|
79
|
+
* find all the File objects in the variables and replace them with an APIUploadInfo object.
|
|
80
|
+
*
|
|
81
|
+
* Then it will send a multipart/form-data request instead of a standard JSON body, and all
|
|
82
|
+
* the file data will be included in later parts.
|
|
83
|
+
*/
|
|
84
|
+
graphqlWithUploads<ReturnType = any>(query: string, variables: Record<string, any>, options?: {
|
|
85
|
+
/**
|
|
86
|
+
* Generally, set this to true if you are only validating a form. You don't want to
|
|
87
|
+
* be uploading files on every keystroke.
|
|
88
|
+
*
|
|
89
|
+
* In this case, we will send a regular post instead of multipart, and all the File objects
|
|
90
|
+
* in the variables will be replaced by an APIUploadInfo object as normal, including the
|
|
91
|
+
* multipartIndex, even though there are no multipart parts coming.
|
|
92
|
+
*/
|
|
93
|
+
omitUploads?: boolean;
|
|
94
|
+
querySignature?: string;
|
|
95
|
+
/**
|
|
96
|
+
* This function will be called with a number between 0 and 1 as the uploads progress. The
|
|
97
|
+
* completion percentage is for the entire submission, not individual files.
|
|
98
|
+
*/
|
|
99
|
+
progress?: APIBaseProgressFn;
|
|
100
|
+
}): Promise<ReturnType>;
|
|
64
101
|
protected analyticsQueue: InteractionEvent[];
|
|
65
102
|
recordInteraction(evt: Optional<InteractionEvent, 'screen'>): void;
|
|
66
103
|
/**
|
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { toasts } from '@txstate-mws/svelte-components';
|
|
2
2
|
import { error } from '@sveltejs/kit';
|
|
3
3
|
import { get } from 'svelte/store';
|
|
4
|
-
import { rescue, toArray } from 'txstate-utils';
|
|
4
|
+
import { pick, rescue, toArray } from 'txstate-utils';
|
|
5
5
|
import { page } from '$app/stores';
|
|
6
6
|
import { afterNavigate } from '$app/navigation';
|
|
7
7
|
import { unifiedAuth } from './unifiedauth.js';
|
|
@@ -26,6 +26,33 @@ export function recordNavigations(callback) {
|
|
|
26
26
|
}, 10);
|
|
27
27
|
});
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* A non-mutating function that will replace all File objects in the variables with an APIUploadInfo
|
|
31
|
+
* object, and put the original File object into the files array so it can be appended to the multipart
|
|
32
|
+
* upload.
|
|
33
|
+
*
|
|
34
|
+
* When I say non-mutating, I mean it will not modify the original variables object. It will mutate the
|
|
35
|
+
* files parameter, which should be passed an empty array.
|
|
36
|
+
*/
|
|
37
|
+
function replaceFiles(variables, files) {
|
|
38
|
+
let newVariables;
|
|
39
|
+
for (const key in variables) {
|
|
40
|
+
const val = variables[key];
|
|
41
|
+
if (val instanceof File) {
|
|
42
|
+
files.push(val);
|
|
43
|
+
newVariables ??= { ...variables };
|
|
44
|
+
newVariables[key] = { __type: 'APIUploadInfo', multipartIndex: files.length - 1, name: val.name, mime: val.type, size: val.size };
|
|
45
|
+
}
|
|
46
|
+
else if (val instanceof Object) {
|
|
47
|
+
const newVal = replaceFiles(val, files);
|
|
48
|
+
if (newVal !== val) {
|
|
49
|
+
newVariables ??= { ...variables };
|
|
50
|
+
newVariables[key] = newVal;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return newVariables ?? variables;
|
|
55
|
+
}
|
|
29
56
|
export class APIBase {
|
|
30
57
|
apiBase;
|
|
31
58
|
authRedirect;
|
|
@@ -101,6 +128,44 @@ export class APIBase {
|
|
|
101
128
|
throw e;
|
|
102
129
|
}
|
|
103
130
|
}
|
|
131
|
+
async uploadWithProgress(path, formData, progress) {
|
|
132
|
+
await this.readyPromise;
|
|
133
|
+
try {
|
|
134
|
+
return await new Promise((resolve, reject) => {
|
|
135
|
+
try {
|
|
136
|
+
progress({ loaded: 0, total: 0, ratio: 0 });
|
|
137
|
+
const request = new XMLHttpRequest();
|
|
138
|
+
request.open('POST', this.apiBase + path);
|
|
139
|
+
if (this.token)
|
|
140
|
+
request.setRequestHeader('Authorization', `Bearer ${this.token}`);
|
|
141
|
+
request.setRequestHeader('Accept', 'application/json');
|
|
142
|
+
request.upload.addEventListener('progress', e => progress({ ...pick(e, 'loaded', 'total'), ratio: e.lengthComputable ? e.loaded / e.total : 0.1 }));
|
|
143
|
+
// request finished
|
|
144
|
+
request.addEventListener('load', e => {
|
|
145
|
+
if (request.status >= 400)
|
|
146
|
+
reject(new Error(request.responseText));
|
|
147
|
+
else {
|
|
148
|
+
try {
|
|
149
|
+
resolve(JSON.parse(request.responseText));
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
reject(e);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
request.addEventListener('abort', e => reject(new Error('Upload aborted.')));
|
|
157
|
+
request.addEventListener('error', e => reject(new Error('An error occurred during transfer. Upload not completed.')));
|
|
158
|
+
request.send(formData);
|
|
159
|
+
}
|
|
160
|
+
catch (e) {
|
|
161
|
+
reject(e);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
progress(undefined);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
104
169
|
async get(path, query) {
|
|
105
170
|
return await this.request(path, 'GET', { query });
|
|
106
171
|
}
|
|
@@ -166,6 +231,36 @@ export class APIBase {
|
|
|
166
231
|
}
|
|
167
232
|
return gqlresponse.data;
|
|
168
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* This is a special graphql request method that allows you to upload files. It will
|
|
236
|
+
* find all the File objects in the variables and replace them with an APIUploadInfo object.
|
|
237
|
+
*
|
|
238
|
+
* Then it will send a multipart/form-data request instead of a standard JSON body, and all
|
|
239
|
+
* the file data will be included in later parts.
|
|
240
|
+
*/
|
|
241
|
+
async graphqlWithUploads(query, variables, options) {
|
|
242
|
+
const files = [];
|
|
243
|
+
variables = replaceFiles(variables, files);
|
|
244
|
+
// If we are only validating, we don't need to upload files
|
|
245
|
+
if (options?.omitUploads || !files.length)
|
|
246
|
+
return this.graphql(query, variables, options?.querySignature);
|
|
247
|
+
const form = new FormData();
|
|
248
|
+
form.set('body', JSON.stringify({
|
|
249
|
+
query,
|
|
250
|
+
variables,
|
|
251
|
+
extensions: {
|
|
252
|
+
querySignature: options?.querySignature
|
|
253
|
+
}
|
|
254
|
+
}));
|
|
255
|
+
for (let i = 0; i < files.length; i++)
|
|
256
|
+
form.set(`file${i}`, files[i]);
|
|
257
|
+
const gqlresponse = await this.uploadWithProgress('/graphql', form, options?.progress ?? (() => { }));
|
|
258
|
+
if (gqlresponse.errors?.length) {
|
|
259
|
+
toasts.add(gqlresponse.errors[0].message);
|
|
260
|
+
throw new Error(JSON.stringify(gqlresponse.errors));
|
|
261
|
+
}
|
|
262
|
+
return gqlresponse.data;
|
|
263
|
+
}
|
|
169
264
|
analyticsQueue = [];
|
|
170
265
|
recordInteraction(evt) {
|
|
171
266
|
evt.screen ??= get(page).route.id;
|
|
@@ -173,7 +268,8 @@ export class APIBase {
|
|
|
173
268
|
setTimeout(() => {
|
|
174
269
|
const events = [...this.analyticsQueue];
|
|
175
270
|
this.analyticsQueue.length = 0;
|
|
176
|
-
|
|
271
|
+
if (events.length)
|
|
272
|
+
this.post('/analytics', events).catch((e) => console.error(e));
|
|
177
273
|
}, 2000);
|
|
178
274
|
}
|
|
179
275
|
/**
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/api.ts","../src/index.ts","../src/unifiedauth.ts"],"version":"5.7.3"}
|