Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api): upload files directly via alexandria API #672

Merged
merged 1 commit into from
Jan 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions addon/adapters/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ApplicationAdapter from "./application";

export default class FileAdapter extends ApplicationAdapter {
ajaxOptions(url, type, options) {
const ajaxOptions = super.ajaxOptions(url, type, options);

if (type === "PUT") {
// Use PATCH instead of PUT for updating records
ajaxOptions.type = "PATCH";
ajaxOptions.method = "PATCH";
}

if (type === "PUT" || type === "POST") {
// Remove content type for updating and creating records so the content
// type will be defined by the passed form data
delete ajaxOptions.headers["content-type"];
}

return ajaxOptions;
}
}
3 changes: 1 addition & 2 deletions addon/models/file.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,9 @@ import Model, { attr, belongsTo, hasMany } from "@ember-data/model";
export default class FileModel extends Model {
@attr variant;
@attr name;
@attr uploadUrl;
@attr downloadUrl;
@attr objectName;
@attr metainfo;
@attr content;
@attr checksum;

@attr createdAt;
Expand Down
31 changes: 31 additions & 0 deletions addon/serializers/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import JSONSerializer from "@ember-data/serializer/json-api";

/*
* If pagination is enabled in the backend, the response format will be changed.
* The response data will be wrapped in a `results` object.
* This would need some configurable normalizer functionality to work.
*/
export default class FileSerializer extends JSONSerializer {
// If we don't do this, Ember will interpret the `meta` property in the single
// response as meta object and omit it from the attributes.
extractMeta() {}

// Disable root key serialization since we want to send plain form data
serializeIntoHash = null;

serialize(snapshot) {
const { name, variant, content } = snapshot.attributes();

const formData = new FormData();

formData.append("name", name);
formData.append("variant", variant);
formData.append("document", snapshot.belongsTo("document")?.id);

if (content instanceof File) {
formData.append("content", content);
}

return formData;
}
}
26 changes: 2 additions & 24 deletions addon/services/alexandria-documents.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { action } from "@ember/object";
import Service, { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import fetch from "fetch";

export default class AlexandriaDocumentsService extends Service {
@service store;
Expand Down Expand Up @@ -68,19 +67,10 @@ export default class AlexandriaDocumentsService extends Service {
document: documentModel,
createdByGroup: this.config.activeGroup,
modifiedByGroup: this.config.activeGroup,
content: file,
});
await fileModel.save();

const response = await fetch(fileModel.uploadUrl, {
method: "PUT",
body: file,
headers: { "content-type": "application/octet-stream" },
});

if (!response.ok) {
throw new Error(response.statusText, response.status);
}

return documentModel;
}),
);
Expand All @@ -99,21 +89,9 @@ export default class AlexandriaDocumentsService extends Service {
document,
createdByGroup: this.config.activeGroup,
modifiedByGroup: this.config.activeGroup,
content: file,
});

await fileModel.save();

const response = await fetch(fileModel.uploadUrl, {
method: "PUT",
body: file,
headers: { "content-type": "application/octet-stream" },
});

if (!response.ok) {
throw new Error(response.statusText, response.status);
}

await document.reload();
}

/**
Expand Down
7 changes: 4 additions & 3 deletions tests/acceptance/documents-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ module("Acceptance | documents", function (hooks) {
assert.dom("[data-test-file]").doesNotExist();

this.assertRequest("POST", "/api/v1/files", (request) => {
const { attributes } = JSON.parse(request.requestBody).data;
assert.strictEqual(attributes.name, "test-file.txt");
assert.strictEqual(attributes.variant, "original");
const name = request.requestBody.get("name");
const variant = request.requestBody.get("variant");
assert.strictEqual(name, "test-file.txt");
assert.strictEqual(variant, "original");
});
await triggerEvent("[data-test-replace]", "change", {
files: [new File(["Ember Rules!"], "test-file.txt")],
Expand Down
21 changes: 21 additions & 0 deletions tests/dummy/app/adapters/file.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import ApplicationAdapter from "./application";

export default class FileAdapter extends ApplicationAdapter {
ajaxOptions(url, type, options) {
const ajaxOptions = super.ajaxOptions(url, type, options);

if (type === "PUT") {
// Use PATCH instead of PUT for updating records
ajaxOptions.type = "PATCH";
ajaxOptions.method = "PATCH";
}

if (type === "PUT" || type === "POST") {
// Remove content type for updating and creating records so the content
// type will be defined by the passed form data
delete ajaxOptions.headers["content-type"];
}

return ajaxOptions;
}
}
6 changes: 3 additions & 3 deletions tests/dummy/mirage/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@ export default function makeServer(config) {
this.resource("tags", { except: ["delete"] });
this.resource("marks", { only: ["index"] });

this.post("/files", function (schema) {
const attrs = this.normalizedRequestAttrs();
this.post("/files", function (schema, request) {
const attrs = Object.fromEntries(request.requestBody.entries());
return schema.files.create({
...attrs,
uploadUrl: "/api/v1/file-upload",
document: schema.documents.find(attrs.document),
});
});

Expand Down
12 changes: 12 additions & 0 deletions tests/unit/adapters/file-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { setupTest } from "dummy/tests/helpers";
import { module, test } from "qunit";

module("Unit | Adapter | file", function (hooks) {
setupTest(hooks);

// Replace this with your real tests.
test("it exists", function (assert) {
const adapter = this.owner.lookup("adapter:file");
assert.ok(adapter);
});
});
11 changes: 9 additions & 2 deletions tests/unit/serializers/file-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,17 @@ module("Unit | Serializer | file", function (hooks) {

test("it serializes records", function (assert) {
const store = this.owner.lookup("service:store");
const record = store.createRecord("file", {});
const file = {
name: "foo",
variant: "original",
};
const record = store.createRecord("file", file);

const serializedRecord = record.serialize();

assert.ok(serializedRecord);
assert.deepEqual(Object.fromEntries(serializedRecord.entries()), {
...file,
document: "undefined",
});
});
});
9 changes: 2 additions & 7 deletions tests/unit/services/alexandria-documents-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ module("Unit | Service | alexandria-documents", function (hooks) {
);

// Each file generates three requests.
assert.strictEqual(requests.length, files.length * 3);
assert.strictEqual(requests.length, files.length * 2);

// Files will be uploaded in parallel. So, we cannot know the order.
const documentRequests = requests.filter((request) =>
Expand All @@ -41,13 +41,9 @@ module("Unit | Service | alexandria-documents", function (hooks) {
const fileRequests = requests.filter((request) =>
request.url.endsWith("files"),
);
const uploadRequests = requests.filter((request) =>
request.url.endsWith("file-upload"),
);

assert.strictEqual(documentRequests.length, files.length);
assert.strictEqual(fileRequests.length, files.length);
assert.strictEqual(uploadRequests.length, files.length);
});

test("it replaces documents", async function (assert) {
Expand All @@ -66,8 +62,7 @@ module("Unit | Service | alexandria-documents", function (hooks) {
(request) => !request.url.includes("documents"),
);

assert.strictEqual(requests.length, 2);
assert.strictEqual(requests.length, 1);
assert.ok(requests[0].url.endsWith("files"));
assert.ok(requests[1].url.endsWith("file-upload"));
});
});
Loading