Skip to content

Boundary ember code patterns

Carlos Alcaide Corvo edited this page Nov 30, 2021 · 1 revision

Table of contents:

Naming conventions:

Add naming conventions for model name, serializer name, model file, serializer file, and look for more.

Feature flag:

Define and set:

Add the feature flag to the Environment var's array:

  • Admin UI: ui/admin/config/environment.js.
  • Desktop Client: ui/desktop/config/environment.js
// Declare the feature to enable
let ENV = {
    ...
    featureFlags: {
        'featureToEnable': false, // Turn it off by default
    }
    ...
}

// Enables feature flag in development environment.
if (environment === 'development') {
    ENV.featureFlags['featureToEnable'] = true;
}

// Enables feature flag in test environment.
if (environment === 'test') {
    ENV.featureFlags['featureToEnable'] = true;
}

// Enables feature flag in production environment.
if (environment === 'production') {
    ENV.featureFlags['featureToEnable'] = true;
}

Use

Use the feature flag in views:

{{#if (feature-flag 'featureToEnable')}}
    ... Your hbs code
{{/if}}

Rolling out a feature

Once we decide to roll out the feature, we turn it true within featureFlags object. Since we turn it true by default, we should delete especific environment declarations.

So the previous example will look like:

let ENV = {
	...
	featureFlags: {
		'featureToEnable': true,
	}
}

Return table of contents.

Capabilities:

WORK IN PROGRESS Return table of contents.

CRUD Operations:

All the API methods are here. We recommend copying (in raw) the content of controller.swagger.json within the swagger editor for better visualization.

Modeling: models and serializers

All the protobuf's files are here very useful for modeling.

Make sure you are: boundary-ui/addons/api before running any command.

Artifacts involved: model, serializer and respective uni tests.

Models

Be aware untitled-library it is a naming example.

  1. Generate model: ember generate model untitled-library. And do nothing with it for now.

  2. Create (manually) a generated model: boundary-ui/addons/api/addon/generated/models/untitled-library.js.

import BaseModel from '../../models/base';
import { attr } from '@ember-data/model';

/**
 * Describe what this model represents.
 * 
 */
export default class GeneratedUntitledLibraryModel extends BaseModel {
  // =attributes

  @attr('string', {
    description: 'Optional name for identification purposes',
  })
  name;

  @attr('string', {
    description: 'Optional user-set description for identification purposes',
  })
  description;

  @attr('date', {
    description: 'The time this resource was created.',
    readOnly: true,
  })
  created_time;

  @attr('date', {
    description: 'The time this resource was last updated.',
    readOnly: true,
  })
  updated_time;

  @attr('number', {
    description: 'Current version number of this resource.',
  })
  version;

Be aware the attributes marked on the protobuf file as Output only mean they are readOnly.

  1. On the previous generated untitled-library model in step 1, we change it to extend from the manually generated model created in step 2. This model has a model fragment for specific attribute, see it as a model to represent an attribute type.
import Model from '@ember-data/model';

export default class UntitledLibrary extends GeneratedModelUntitledModel {
	// =attributes
	@fragment('fragment-untitled-library-attributes', { defaultValue: {} })
  attributes;
}
  1. Generate the fragment model as we generated the model before: ember generate model fragment-untitled-library. Change where it extends from, should be Fragment and then define attributes. Example:
import Fragment from 'ember-data-model-fragments/fragment';
import { attr } from '@ember-data/model';

export default class FragmentUntitledLibraryAttributesModel extends Fragment {
  // =attributes

  @attr('string', {
    description:
      'The boolean expression filter to use to determine.',
  })
  filter;
}

Apart from the model's unit tests, there is not much we can use with models for the sanity check.

Return table of contents.

Serializers

  1. Generate serializers for the model and the fragment (if necessary):

    • $ ember generate serializer untitled-library.
    • $ ember generate serializer fragment-untitled-library-attributes.
  2. Update serializers/untitled-library.js to use ApplicationSerializer instead of default JSONAPISerializer

import ApplicationSerializer from './application';

export default class UntitledLibrarySerializer extends ApplicationSerializer {
	...
}
  1. Update serializers/fragment-untitled-library-attributes.js to use JSONSerializer instead of default JSONAPISerializer
import JSONSerializer from '@ember-data/serializer/json';

export default class FragmentUntitledLibraryAttributesSerializer extends JSONSerializer {
	...
}
  1. Update attributes serializer to meet API expectations:
export default class FragmentUntitledLibraryAttributesSerializer extends JSONSerializer {
	/**
   * If an attribute is annotated as readOnly in the model, don't serialize it.
   * Otherwise delegate to default attribute serializer.
   * @override
   * @method serializeAttribute
   * @param {Snapshot} snapshot
   * @param {Object} json
   * @param {String} key
   * @param {Object} attribute
   */
  serializeAttribute(snapshot, json, key, attribute) {
    const { type, options } = attribute;
    let value = super.serializeAttribute(...arguments);
    // Convert empty string to null.
    if (type === 'string' && json[key] === '') json[key] = null;
    // Do not serialize read-only attributes.
    if (options.readOnly) delete json[key];
    // Version is sent only if it has a non-nullish value
    if (key === 'version') {
      if (json[key] === null || json[key] === undefined) delete json[key];
    }
    return value;
  }
}
Common serializations:
  • We do not serialize readOnly values.
  • We do serialize empty strings to null.
  • We delete some fields if they have a specific value (for example, very common on field version).

Return table of contents.

Serializers tests

Think off all the serializations you are performing and test all the cases. Good practice is to look at api/tests/unit/serializers/ for examples and inspiration.

import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';

module('Unit | Serializer | untitled library', function (hooks) {
  setupTest(hooks);

  test('it serializes correctly on create', function (assert) {
    assert.expect(1);
    const store = this.owner.lookup('service:store');
    const serializer = store.serializerFor('untitled-library');
    const record = store.createRecord('untitled-library', {
      name: 'Example',
      description: 'Description',
      version: 1,
    });
    const snapshot = record._createSnapshot();
    snapshot.adapterOptions = {};
    const serializedRecord = serializer.serialize(snapshot);
    assert.deepEqual(serializedRecord, {
      name: 'Group',
      description: 'Description',
      version: 1,
      attributes: {
        description: null,
      },
    });
  });
  
	test('it does not serialize version when it has null or undefined value', function (assert) {
    assert.expect(1);
    const store = this.owner.lookup('service:store');
    const serializer = store.serializerFor('untitled-library');
    store.push({
      data: {
        id: '1',
        type: 'untitled-library',
        attributes: {
          name: 'Test library',
          description: 'Description for the test',
          version: undefined,
          attributes: {
            description: 'Test description as attribute',
          },
        },
      },
    });
    const record = store.peekRecord('untitled-library', '1');
    const snapshot = record._createSnapshot();
    const serializedRecord = serializer.serialize(snapshot);
    assert.deepEqual(serializedRecord, {
      name: 'untitled-library',
      description: 'Description for the test',
      attributes: {
        description: 'Test description as attribute',
      },
    });
  });
});

Return table of contents.

Mirage

Artifacts involved config file, generated factories, factories, serializers, scenarios

  1. For CRUD operations we will need to mock 5 API methods:

api/mirage/config.js

	// Returns the list of all Untitled libraries
	this.get(
		'/untitled-libraries',
		({ untitledLibraries }, { queryParams: { scope_id: scopeId } }) => {
		  return untitledLibraries.where({ scopeId });
		}
	);
	// Returns a single untitled library
	this.get('/untitled-libraries/:id');
	// Creates a single untitled library
	this.post('/untitled-libraries');
	// Deletes a single untitled library
	this.del('/untitled-libraries/:id');
	// Updates a single untitled library
	this.patch('/untitled-libraries/:id');
  1. We need to add a generated factory:

addon/mirage/generated/factories/untitled-library.js

import { Factory } from 'ember-cli-mirage';
import { random, date, datatype } from 'faker';

/**
 * GeneratedUntitledLibrary
 * A Untitled library is a resource that represents a collection of libraries
 */
export default Factory.extend({
  name: () => random.words(),
  description: () => random.words(),
  created_time: () => date.recent(),
  updated_time: () => date.recent(),
  version: () => datatype.number(),
});
  1. We need to add a Factory addon/mirage/factories/untitled-library.js
import factory from '../generated/factories/untitled-library';
import { random, system } from 'faker';
import permissions from '../helpers/permissions';

const types = ['vault'];

export default factory.extend({
  id: (i) => `untitled-library-id-${i}`,
	
	// Capabilities
	authorized_actions: () =>
    permissions.authorizedActionsFor('untitled-library') || [
      'no-op',
      'read',
      'update',
      'delete',
    ],
	
	// In case the factory has also attributes you can generate them within the attributes function
	attributes() {
		switch (this.type) {
			case 'type1':
				return {
					description: 'This will return a hardcoded string'
				};
				break;
			default:
				return {
					description: () => random.words();
				};   
		}
	},
});
  1. We need to add a Model addon/mirage/models/untitled-library.js
import { Model, belongsTo } from 'ember-cli-mirage';

export default Model.extend({
  scope: belongsTo(),
});
  1. We need to add a Serializer addon/mirage/serializers/untitled-library.js
import ApplicationSerializer from './application';

export default ApplicationSerializer.extend({
  modelName: 'untitled-library',
	
	_hashForModel(model) {
    const json = ApplicationSerializer.prototype._hashForModel.apply(
      this,
      arguments
    );
    return json;
  },
});
  1. Relations when mocking: For the sake of the example, let's assume to create untitled-library we need first a library-store. Let's assume library-store it is just a list of untitled-library.

Then, we will need to perform the creation of an untitled-library after library-store is created. To do so, let's open library-store factory: addon/mirage/factories/library-store.js

import factory from '../generated/factories/library-store';
import { random, system } from 'faker';
import permissions from '../helpers/permissions';

const types = ['vault'];

export default factory.extend({
  ... factory code
	
	withAssociations: trait({
		afterCreate(libraryStore, server) {
			const { scope } = credentialStore;
			server.createList('untitled-library', 2, {
				scope,
				credentialStore,
			});
		},
	}),
});
  1. Last step is to trigger the creation of library-store which will generate untitled-library with in. To do so we need to add the scenario to mirage: addon/mirage/scenarios/default.js
server.schema.scopes.where({ type: 'project' }).models.forEach((scope) => {
	server.createList('library-store', 2, { scope }, 'withAssociations');
});

Return table of contents.

List collection

WORK IN PROGRESS

Return table of contents.

Read CRUD operation

WORK IN PROGRESS

Return table of contents.

Delete CRUD operation

WORK IN PROGRESS

Return table of contents.

Create CRUD operation

WORK IN PROGRESS

Return table of contents.

Edit CRUD operation

WORK IN PROGRESS

Return table of contents.