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

Add organization logo to organization #1369

Merged
merged 1 commit into from
Nov 13, 2023
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
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
"@types/chai-as-promised": "^7.1.4",
"@types/lodash": "^4.14.195",
"@types/memoizee": "^0.4.7",
"@types/mime": "^3.0.3",
"@types/mocha": "^10.0.1",
"@types/ndjson": "^2.0.0",
"@types/sinon": "^10.0.6",
Expand Down Expand Up @@ -132,6 +133,7 @@
"handlebars": "^4.7.7",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
"mime": "^3.0.0",
"ndjson": "^2.0.0",
"p-throttle": "^4.1.1",
"pinejs-client-core": "^6.12.0",
Expand Down
17 changes: 12 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,22 +373,29 @@ export const getSdk = function ($opts?: SdkOptions) {
* @memberof balena
*
* @description
* The utils instance used internally. This should not be necessary
* in normal usage, but can be useful to handle some specific cases.
* The utils instance offers some convenient features for clients.
*
* @example
* balena.utils.mergePineOptions(
* { $expand: { device: { $select: ['id'] } } },
* { $expand: { device: { $select: ['name'] } } },
* );
*
* @example
* // Creating a new WebResourceFile in case 'File' API is not available.
* new balena.utils.BalenaWebResourceFile(
* [fs.readFileSync('./file.tgz')],
* 'file.tgz'
* );
*/
Object.defineProperty(sdk, 'utils', {
enumerable: true,
configurable: true,
get() {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { mergePineOptions } = require('./util') as typeof import('./util');
return { mergePineOptions };
const { mergePineOptions, BalenaWebResourceFile } =
// eslint-disable-next-line @typescript-eslint/no-var-requires
require('./util') as typeof import('./util');
return { mergePineOptions, BalenaWebResourceFile };
},
});

Expand Down
25 changes: 25 additions & 0 deletions src/models/organization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,31 @@ const getOrganizationModel = function (
* balena.models.organization.create({ name:'MyOrganization' }).then(function(organization) {
* console.log(organization);
* });
*
* @example
* balena.models.organization.create({
* name:'MyOrganization',
* logo_image: new balena.utils.BalenaWebResourceFile(
* [fs.readFileSync('./img.jpeg')],
* 'img.jpeg'
* );
* })
* .then(function(organization) {
* console.log(organization);
* });
*
* @example
* balena.models.organization.create({
* name:'MyOrganization',
* // Only in case File API is avaialable (most browsers and Node 20+)
* logo_image: new File(
* imageContent,
* 'img.jpeg'
* );
* })
* .then(function(organization) {
* console.log(organization);
* });
*/
const create = function (
organization: BalenaSdk.PineSubmitBody<BalenaSdk.Organization>,
Expand Down
2 changes: 2 additions & 0 deletions src/types/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type {
OptionalNavigationResource,
ReverseNavigationResource,
ConceptTypeNavigationResource,
WebResource,
} from '../../typings/pinejs-client-core';
import type { AnyObject } from '../../typings/utils';

Expand Down Expand Up @@ -84,6 +85,7 @@ export interface Organization {
handle: string;
has_past_due_invoice_since__date: string | null;
is_frozen: boolean;
logo_image: WebResource;

application: ReverseNavigationResource<Application>;
/** includes__organization_membership */
Expand Down
15 changes: 15 additions & 0 deletions src/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as errors from 'balena-errors';
import type * as Pine from '../../typings/pinejs-client-core';
import type { IfDefined } from '../../typings/utils';
import type { WebResourceFile } from 'balena-request';
import * as mime from 'mime';

export interface BalenaUtils {
mergePineOptions: typeof mergePineOptions;
BalenaWebResourceFile: typeof BalenaWebResourceFile;
}

export const notImplemented = () => {
Expand Down Expand Up @@ -349,3 +352,15 @@ export const limitedMap = <T, U>(
}
});
};

export class BalenaWebResourceFile extends Blob implements WebResourceFile {
public name: string;
constructor(blobParts: BlobPart[], name: string, options?: BlobPropertyBag) {
const opts = {
...options,
type: options?.type ?? mime.getType(name) ?? undefined,
};
super(blobParts, opts);
this.name = name;
}
}
24 changes: 23 additions & 1 deletion tests/integration/models/organization.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect } from 'chai';
import parallel from 'mocha.parallel';
import * as superagent from 'superagent';
import {
balena,
credentials,
Expand Down Expand Up @@ -98,6 +99,27 @@ describe('Organization model', function () {
.that.is.not.equal(ctx.newOrg1.handle);
ctx.newOrg2 = org;
});

it('should be able to create an organization with a logo', async function () {
const org = await balena.models.organization.create({
name: 'org-with-logo',
logo_image: new balena.utils.BalenaWebResourceFile(
[Buffer.from('this is a test\n')],
'orglogo.png',
),
});

const fetchedOrg = await balena.models.organization.get(org.id, {
$select: ['id', 'logo_image'],
});
expect(fetchedOrg)
.to.have.nested.property('logo_image.href')
.that.is.a('string');

const res = await superagent.get(fetchedOrg.logo_image.href);
expect(res.status).to.equal(200);
expect(res.headers['content-length']).to.equal('15');
});
});
});

Expand All @@ -108,7 +130,7 @@ describe('Organization model', function () {
$orderby: 'id asc',
});
expect(orgs).to.be.an('array');
expect(orgs).to.have.lengthOf(3);
expect(orgs).to.have.lengthOf(4);
const [org1, org2, org3] = orgs;
expect(org1).to.deep.match(ctx.userInitialOrg);
expect(org2).to.deep.match(ctx.newOrg1);
Expand Down
12 changes: 11 additions & 1 deletion typings/pinejs-client-core.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { WebResourceFile } from 'balena-request';
import type {
AnyObject,
PropsAssignableWithType,
Expand Down Expand Up @@ -143,6 +144,14 @@ export type PostResult<T> = SelectResultObject<
Exclude<StringKeyof<T>, PropsOfType<T, ReverseNavigationResource<object>>>
>;

export type WebResource = {
filename: string;
href: string;
content_type?: string;
content_disposition?: string;
size?: number;
};

// based on https://github.com/balena-io/pinejs-client-js/blob/master/core.d.ts

type RawFilter =
Expand Down Expand Up @@ -379,10 +388,11 @@ export type ODataOptionsStrict<T> = Omit<
export type ODataOptionsWithFilter<T> = ODataOptions<T> &
Required<Pick<ODataOptions<T>, '$filter'>>;

export type ReplaceWebResource<K> = K extends WebResource ? WebResourceFile : K;
export type SubmitBody<T> = {
[k in keyof T]?: T[k] extends AssociatedResource<object>
? number | null
: T[k];
: ReplaceWebResource<T[k]>;
Comment on lines +391 to +395
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless you need to use this elsewhere, I would just inline the ReplaceWebResource

};

type BaseResourceId =
Expand Down
Loading