@weapnl/js-junction 0.0.10 → 0.1.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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v0.1.0
6
+ - Added the Temporary Media Upload functionality.
7
+
8
+ ## v0.0.11
9
+ - Fixed a bug where cancelled requests would cause issues when cancelling subsequent requests.
10
+
5
11
  ## v0.0.10
6
12
  - Updated Readme.md
7
13
  - Added onFinished to the index.d.ts file (TS support).
package/README.md CHANGED
@@ -12,6 +12,7 @@ This package has support for Typescript (TS).
12
12
  - [Creating Models](#creating-models)
13
13
  - [Performing Requests](#performing-requests)
14
14
  - [Applying Filters and Scopes](#applying-filters-and-scopes)
15
+ - [Uploading Files with Spatie Medialibrary](#uploading-files-with-spatie-medialibrary)
15
16
 
16
17
  ## Installation
17
18
  ```bash
@@ -371,3 +372,53 @@ api.removeHeader('HEADER NAME HERE'); // Removes the header.
371
372
  **Sample response**
372
373
 
373
374
  After executing a request, the property `response` contains a `Response` object, which has properties `statusCode`, `data` and `validation`.
375
+
376
+ ### Uploading Files with [Spatie Medialibrary](https://spatie.be/docs/laravel-medialibrary/v11/introduction)
377
+
378
+ #### Step 1: Uploading Files to a Model
379
+ To upload files to a model, use the `upload` function available on the model instance. This function requires two arguments:
380
+
381
+ 1. **Uploaded Files**: An array of files, typically obtained from an input field of `type="file"`.
382
+ 2. **Collection Name**: The name of the media [collection](https://spatie.be/docs/laravel-medialibrary/v11/working-with-media-collections/simple-media-collections) to which the files should be attached. This corresponds to the collection defined in your Laravel model.
383
+
384
+ **Example Usage:**
385
+ ```js
386
+ // Retrieve the employee model instance (e.g., Employee with ID 3)
387
+ const employee = Employee.show(3);
388
+
389
+ // Upload the files to the 'IdentityFiles' collection
390
+ employee.upload(uploadedFiles, 'IdentityFiles');
391
+ ```
392
+
393
+ In this example, `uploadedFiles` is an array of files from an input field, and `'IdentityFiles'` is the name of the media collection on the `Employee` model where these files should be stored.
394
+
395
+ #### Step 2: Handling the Uploaded Files
396
+ Once the `upload` function is called, the files are sent to the API. The API temporarily stores these files in the media library and returns the media IDs associated with each file. These media IDs are automatically set on the model instance.
397
+
398
+ #### Step 3: Saving the Model with Attached Media
399
+ After uploading the files, you can call the `.save()` method on the model instance. This step finalizes the process by permanently attaching the uploaded files to the specified media collection on the model. The media IDs stored on the model are now linked to the correct collection in the database.
400
+
401
+ **Example of Saving the Model:**
402
+ ```js
403
+ // Save the employee model with the uploaded files attached
404
+ employee.save();
405
+ ```
406
+
407
+ When the `save()` method is invoked, the model is updated or created (depending on whether it was previously persisted), and the uploaded media files are attached to the correct collection as defined in the earlier steps.
408
+
409
+ #### Advanced Usage: Uploading Files in Nested Structures
410
+ If your model contains nested relationships, such as an `Employee` model with a `Contact` relationship, you can still use the `upload` function to attach files to the appropriate collection within the nested structure.
411
+
412
+ **Example with Nested Structure:**
413
+ ```js
414
+ // Retrieve the employee model instance
415
+ const employee = Employee.show(3);
416
+
417
+ // Upload a profile picture to the 'ProfilePicture' collection within the 'Contact' relationship
418
+ employee.contact.upload(uploadedFiles, 'ProfilePicture');
419
+
420
+ // Save the employee model, including the nested contact with the attached profile picture
421
+ employee.save();
422
+ ```
423
+
424
+ In this scenario, the uploaded files are linked to the `ProfilePicture` collection within the `Contact` relationship of the `Employee` model. When the `save()` method is called, the files are properly attached within the nested structure.
package/index.d.ts CHANGED
@@ -81,7 +81,7 @@ declare class Request extends Mixins {
81
81
  put(data?: object): Promise<this>;
82
82
  setKey(key: string): this;
83
83
  delete(): Promise<this>;
84
- storeFiles(files?: object, data?: object): Promise<this>;
84
+ storeFiles(files?: object, data?: object, url?: string|null): Promise<this>;
85
85
  readonly bodyParameters: object;
86
86
  onSuccess<T = any>(callback?: (result: T, data: any) => void): this;
87
87
  onError(callback?: (response: Response) => void): this;
@@ -112,6 +112,8 @@ export class Model extends Request {
112
112
 
113
113
  static relations(): JsonMap;
114
114
 
115
+ static mediaCollections(): JsonMap;
116
+
115
117
  static readonly endpoint: string;
116
118
 
117
119
  readonly _identifier: number | string;
@@ -128,6 +130,8 @@ export class Model extends Request {
128
130
 
129
131
  save(extraData?: JsonMap): Promise<Model>;
130
132
 
133
+ upload(files: object|object[], collection: string): Promise<this>;
134
+
131
135
  clone(): Model;
132
136
 
133
137
  _queryString(identifier?: number | string): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@weapnl/js-junction",
3
- "version": "0.0.10",
3
+ "version": "0.1.0",
4
4
  "description": "This project allows you to easily consume API's built with Junction.",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/api.js CHANGED
@@ -76,7 +76,9 @@ export default class Api {
76
76
  return;
77
77
  }
78
78
 
79
- delete this._requests[request.key];
79
+ if (request.response.statusCode !== 0) {
80
+ delete this._requests[request.key];
81
+ }
80
82
  }
81
83
 
82
84
  /**
@@ -2,6 +2,7 @@ import Accessors from './properties/accessors';
2
2
  import Attributes from './properties/attributes';
3
3
  import Counts from './properties/counts';
4
4
  import Relations from './properties/relations';
5
+ import MediaCollections from './properties/mediaCollections';
5
6
  import Request from '../request';
6
7
 
7
8
  export default class Model extends Request {
@@ -12,6 +13,7 @@ export default class Model extends Request {
12
13
  this._attributes = new Attributes(this);
13
14
  this._counts = new Counts(this);
14
15
  this._relations = new Relations(this);
16
+ this._mediaCollections = new MediaCollections(this);
15
17
 
16
18
  this.setApi(api);
17
19
  this.fill(defaults);
@@ -46,6 +48,7 @@ export default class Model extends Request {
46
48
  ...this._attributes.toJson(this),
47
49
  ...this._counts.toJson(this),
48
50
  ...this._relations.toJson(this),
51
+ ...this._mediaCollections.toJson(this),
49
52
  };
50
53
  }
51
54
 
@@ -96,6 +99,15 @@ export default class Model extends Request {
96
99
  return {};
97
100
  }
98
101
 
102
+ /**
103
+ * The media collections of the model
104
+ *
105
+ * @returns {Object.<any, Object>}
106
+ */
107
+ static mediaCollections () {
108
+ return {};
109
+ }
110
+
99
111
  /**
100
112
  * @returns {string} Endpoint of the model for the API.
101
113
  */
@@ -182,7 +194,7 @@ export default class Model extends Request {
182
194
 
183
195
  this._response = await this._connection.post(
184
196
  this._queryString(),
185
- { ...this._attributes.toJson(this), ...extraData },
197
+ { ...this._attributes.toJson(this), ...this._mediaCollections.toJson(this), ...extraData },
186
198
  );
187
199
 
188
200
  this._connection.removeRequest(this);
@@ -210,7 +222,7 @@ export default class Model extends Request {
210
222
 
211
223
  this._response = await this._connection.put(
212
224
  this._queryString(this._identifier),
213
- { ...this._attributes.toJson(this), ...extraData },
225
+ { ...this._attributes.toJson(this), ...this._mediaCollections.toJson(this), ...extraData },
214
226
  );
215
227
 
216
228
  this._connection.removeRequest(this);
@@ -245,6 +257,32 @@ export default class Model extends Request {
245
257
  return !! this._response.data;
246
258
  }
247
259
 
260
+ /**
261
+ * Upload an temporary media file to the API.
262
+ *
263
+ * @param {array|File} [files] The uploaded file or files.
264
+ * @param {string} [collection] The name of the file collection.
265
+ *
266
+ * @returns {array} The received media ids.
267
+ */
268
+ async upload (files, collection) {
269
+ this._media ??= {};
270
+ const filesArray = (Array.isArray(files) ? files : [files]).filter((value) => value !== null);
271
+
272
+ if (filesArray.length === 0) {
273
+ this._media[collection] = {};
274
+ return;
275
+ }
276
+
277
+ const request = await this.storeFiles({
278
+ files: _.flatMapDeep(filesArray),
279
+ }, {}, '/media/upload');
280
+
281
+ this._media[collection] = request._response.data;
282
+
283
+ return request._response.data;
284
+ }
285
+
248
286
  /**
249
287
  * Save the current model. Based on the value of the identifier `store` or `update` will be called.
250
288
  *
@@ -0,0 +1,28 @@
1
+ /**
2
+ * @implements {Property}
3
+ */
4
+ export default class MediaCollections {
5
+ /**
6
+ * @param {Model} model Instance of the model.
7
+ */
8
+ constructor (model) {
9
+ }
10
+
11
+ /**
12
+ * @param {Model} model
13
+ *
14
+ * @return {Object} The attributes casted to a json object.
15
+ */
16
+ toJson (model) {
17
+ const json = {};
18
+
19
+ _.each(model.constructor.mediaCollections(), (options, key) => {
20
+ const mediaPrefix = '_media.';
21
+ let value = _.get(model, options.jsonKey ?? mediaPrefix + key, _.get(model, mediaPrefix + _.camelCase(key)));
22
+
23
+ _.set(json, mediaPrefix + key, value ?? []);
24
+ });
25
+
26
+ return json;
27
+ }
28
+ }
package/src/request.js CHANGED
@@ -165,11 +165,12 @@ export default class Request {
165
165
  /**
166
166
  * @param {Object} files
167
167
  * @param {Object} data
168
+ * @param {string|null} url
168
169
  *
169
170
  * @returns {this} The current instance.
170
171
  */
171
- async storeFiles (files = {}, data = {}) {
172
- const url = this.url ?? this.constructor.endpoint;
172
+ async storeFiles (files = {}, data = {}, url = null) {
173
+ let queryUrl = url ?? this.url ?? this.constructor.endpoint;
173
174
 
174
175
  this._connection.cancelRunning(this);
175
176
 
@@ -181,13 +182,23 @@ export default class Request {
181
182
 
182
183
  const formData = this._createFormData(_.merge({}, files, data));
183
184
 
185
+ if (! _.isEmpty(this.bodyParameters)) {
186
+ queryUrl = `${queryUrl}?${this.bodyParameters}`;
187
+ }
188
+
184
189
  this._response = await this._connection.post(
185
- `${url}?${this.bodyParameters}`,
190
+ queryUrl,
186
191
  formData,
187
192
  );
188
193
 
189
194
  this._connection.removeRequest(this);
190
195
 
196
+ this.setConfig({
197
+ headers: {
198
+ 'Content-Type': 'application/json',
199
+ },
200
+ });
201
+
191
202
  await this.triggerResponseEvents(this._response);
192
203
 
193
204
  return this;