Skip to content

Commit 9bd7fbb

Browse files
committed
feat: upload document and file in one request
1 parent d49ce85 commit 9bd7fbb

File tree

11 files changed

+109
-49
lines changed

11 files changed

+109
-49
lines changed

addon/adapters/application.js

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { inject as service } from "@ember/service";
22
import OIDCJSONAPIAdapter from "ember-simple-auth-oidc/adapters/oidc-json-api-adapter";
33

4+
// when adding a new adapter, make sure to add it to the test app aswell
45
export default class ApplicationAdapter extends OIDCJSONAPIAdapter {
56
@service("alexandria-config") config;
67
@service session;

addon/adapters/document.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ApplicationAdapter from "./application";
2+
3+
export default class DocumentAdapter extends ApplicationAdapter {
4+
ajaxOptions(url, type, options) {
5+
const ajaxOptions = super.ajaxOptions(url, type, options);
6+
7+
if (type === "POST") {
8+
// Remove content type for updating and creating records so the content
9+
// type will be defined by the passed form data
10+
delete ajaxOptions.headers["content-type"];
11+
}
12+
13+
return ajaxOptions;
14+
}
15+
16+
createRecord(store, type, snapshot) {
17+
const url = this.buildURL(type.modelName, null, snapshot, "createRecord");
18+
19+
const data = store.serializerFor(type.modelName).serializeCreate(snapshot);
20+
21+
return this.ajax(url, "POST", { data });
22+
}
23+
}

addon/models/document.js

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export default class DocumentModel extends LocalizedModel {
1515
@attr modifiedByUser;
1616
@attr modifiedByGroup;
1717
@attr date;
18+
@attr content; // needed for upload
1819

1920
@belongsTo("category", { inverse: "documents", async: true }) category;
2021
@hasMany("tag", { inverse: "documents", async: true }) tags;

addon/serializers/document.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,22 @@
11
import { LocalizedSerializer } from "ember-localized-model";
22

3-
export default class DocumentSerializer extends LocalizedSerializer {}
3+
export default class DocumentSerializer extends LocalizedSerializer {
4+
serializeCreate(snapshot) {
5+
const data = snapshot.attributes();
6+
const content = data.content;
7+
delete data.content;
8+
data.category = snapshot.belongsTo("category").id;
9+
10+
const formData = new FormData();
11+
12+
formData.append(
13+
"data",
14+
new Blob([JSON.stringify(data)], {
15+
type: "application/vnd.api+json",
16+
}),
17+
);
18+
formData.append("content", content);
19+
20+
return formData;
21+
}
22+
}

addon/services/alexandria-documents.js

+13-27
Original file line numberDiff line numberDiff line change
@@ -62,23 +62,18 @@ export default class AlexandriaDocumentsService extends Service {
6262
for (const file of files) {
6363
if (
6464
category.allowedMimeTypes &&
65-
!category.allowedMimeTypes.includes(file.type)
65+
!category.allowedMimeTypes.includes(
66+
file.type ?? mime.getType(file.name.split(".").pop()),
67+
)
6668
) {
67-
// file.type is empty for Outlook msg files, see
68-
// https://stackoverflow.com/questions/55687631/which-mime-type-can-i-use-for-msg-file-using-file-object
69-
if (
70-
!category.allowedMimeTypes.includes("application/vnd.ms-outlook") ||
71-
!file.name.endsWith(".msg")
72-
) {
73-
return this.notification.danger(
74-
this.intl.t("alexandria.errors.invalid-file-type", {
75-
category: category.name,
76-
types: category.allowedMimeTypes
77-
.map((t) => mime.getExtension(t))
78-
.join(", "),
79-
}),
80-
);
81-
}
69+
return this.notification.danger(
70+
this.intl.t("alexandria.errors.invalid-file-type", {
71+
category: category.name,
72+
types: category.allowedMimeTypes
73+
.map((t) => mime.getExtension(t))
74+
.join(", "),
75+
}),
76+
);
8277
}
8378
}
8479

@@ -90,20 +85,11 @@ export default class AlexandriaDocumentsService extends Service {
9085
metainfo: this.config.defaultModelMeta.document,
9186
createdByGroup: this.config.activeGroup,
9287
modifiedByGroup: this.config.activeGroup,
88+
content: file,
9389
});
90+
// must be set outside for localized model
9491
documentModel.title = file.name;
9592
await documentModel.save();
96-
97-
const fileModel = this.store.createRecord("file", {
98-
name: file.name,
99-
variant: "original",
100-
document: documentModel,
101-
createdByGroup: this.config.activeGroup,
102-
modifiedByGroup: this.config.activeGroup,
103-
content: file,
104-
});
105-
await fileModel.save();
106-
10793
return documentModel;
10894
}),
10995
);

tests/acceptance/documents-test.js

+10-6
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,16 @@ module("Acceptance | documents", function (hooks) {
158158

159159
assert.dom("[data-test-document]").doesNotExist();
160160
this.assertRequest("POST", "/api/v1/documents", (request) => {
161-
const attributes = JSON.parse(request.requestBody).data.attributes;
162-
assert.strictEqual(
163-
attributes.title.en,
164-
"test-file.txt",
165-
"correct title is set",
166-
);
161+
request.requestBody
162+
.get("data")
163+
.text()
164+
.then((data) => {
165+
assert.strictEqual(
166+
JSON.parse(data).title.en,
167+
"test-file.txt",
168+
"correct title is set",
169+
);
170+
});
167171
});
168172
await triggerEvent("[data-test-upload] [data-test-input]", "change", {
169173
files: [new File(["Ember Rules!"], "test-file.txt")],

tests/dummy/app/adapters/document.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import ApplicationAdapter from "./application";
2+
3+
export default class DocumentAdapter extends ApplicationAdapter {
4+
ajaxOptions(url, type, options) {
5+
const ajaxOptions = super.ajaxOptions(url, type, options);
6+
7+
if (type === "POST") {
8+
// Remove content type for updating and creating records so the content
9+
// type will be defined by the passed form data
10+
delete ajaxOptions.headers["content-type"];
11+
}
12+
13+
return ajaxOptions;
14+
}
15+
16+
createRecord(store, type, snapshot) {
17+
const url = this.buildURL(type.modelName, null, snapshot, "createRecord");
18+
19+
const data = store.serializerFor(type.modelName).serializeCreate(snapshot);
20+
21+
return this.ajax(url, "POST", { data });
22+
}
23+
}

tests/dummy/mirage/config.js

+4-3
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export default function makeServer(config) {
1010
this.timing = 400;
1111

1212
this.resource("categories", { only: ["index", "show"] });
13-
this.resource("documents");
13+
this.resource("documents", { except: ["create"] });
14+
this.post("/documents", function (schema) {
15+
return schema.documents.create();
16+
});
1417
this.resource("tags", { except: ["delete"] });
1518
this.resource("marks", { only: ["index"] });
1619

@@ -23,8 +26,6 @@ export default function makeServer(config) {
2326
});
2427
});
2528

26-
this.put("/file-upload", () => new Response(201, {}, {}));
27-
2829
this.get("/files/multi", () => new Response(200, {}, {}));
2930
},
3031
});

tests/dummy/mirage/factories/file.js

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ export default Factory.extend({
77
createdAt: () => faker.date.past(),
88
name: () => faker.system.fileName(),
99
variant: "original",
10-
uploadUrl: "/api/v1/file-upload",
1110
downloadUrl: () => faker.internet.url(),
1211
checksum: () => `sha256:${faker.git.commitSha({ length: 64 })}`,
1312
});

tests/unit/services/alexandria-documents-test.js

+1-5
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,14 @@ module("Unit | Service | alexandria-documents", function (hooks) {
2828
);
2929

3030
// Each file generates three requests.
31-
assert.strictEqual(requests.length, files.length * 2);
31+
assert.strictEqual(requests.length, files.length);
3232

3333
// Files will be uploaded in parallel. So, we cannot know the order.
3434
const documentRequests = requests.filter((request) =>
3535
request.url.endsWith("documents"),
3636
);
37-
const fileRequests = requests.filter((request) =>
38-
request.url.endsWith("files"),
39-
);
4037

4138
assert.strictEqual(documentRequests.length, files.length);
42-
assert.strictEqual(fileRequests.length, files.length);
4339
});
4440

4541
test("it restricts mime type", async function (assert) {

tests/unit/services/alexandria-tags-test.js

+13-6
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,16 @@ module("Unit | Service | alexandria-tags", function (hooks) {
1717
const service = this.owner.lookup("service:alexandria-tags");
1818
const store = this.owner.lookup("service:store");
1919

20-
const document = await store.createRecord("document").save();
20+
const documentId = this.server.create("document").id;
21+
const document = await store.findRecord("document", documentId);
2122
const tag = await store.createRecord("tag", { name: "T1" }).save();
2223

2324
await service.add(document, tag);
2425

2526
assert.deepEqual(
2627
requests.map((request) => request.method),
2728
[
28-
"POST", // Create document
29+
"GET", // Get document
2930
"POST", // Create tag
3031
"PATCH", // Add tag to document
3132
],
@@ -39,15 +40,20 @@ module("Unit | Service | alexandria-tags", function (hooks) {
3940

4041
const service = this.owner.lookup("service:alexandria-tags");
4142
const store = this.owner.lookup("service:store");
42-
43-
const document = await store.createRecord("document").save();
43+
const categoryId = this.server.create("category").id;
44+
const document = await store
45+
.createRecord("document", {
46+
category: await store.findRecord("category", categoryId),
47+
})
48+
.save();
4449
const tag = "T1";
4550

4651
await service.add(document, tag);
4752

4853
assert.deepEqual(
4954
requests.map((request) => request.method),
5055
[
56+
"GET", // Get category
5157
"POST", // Create document
5258
"GET", // search for existing tag
5359
"POST", // Create tag
@@ -64,15 +70,16 @@ module("Unit | Service | alexandria-tags", function (hooks) {
6470
const service = this.owner.lookup("service:alexandria-tags");
6571
const store = this.owner.lookup("service:store");
6672

67-
const document = await store.createRecord("document").save();
73+
const documentId = this.server.create("document").id;
74+
const document = await store.findRecord("document", documentId);
6875
const tag = (await document.tags)[0];
6976

7077
await service.remove(document, tag);
7178

7279
assert.deepEqual(
7380
requests.map((request) => request.method),
7481
[
75-
"POST", // Create document
82+
"GET", // Get document
7683
"PATCH", // Remove tag from document
7784
],
7885
);

0 commit comments

Comments
 (0)