Skip to content
Merged
10 changes: 5 additions & 5 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
> v0.2.0 ~ "First-class trailers, inspections and full test coverage"
> v0.2.1 ~ "Inspection display dates"

---
## Highlights

- **First-class Trailer data models** — trailer models, serializers and data contracts for the console, with current trailers embedded on the vehicle serializer.
- **Inspection data models** — models and serializers backing the maintenance platform upgrade.
- **`is-waypoint-record` util** — moved out of `ember-core` and into this package.
- **Test coverage campaign** — a 100% coverage gate wired into CI, with Codecov reporting.
- **Inspection display dates are formatted** — `inspection-form` and `inspection-submission` format their display-date getters as `yyyy-MM-dd HH:mm` and answer `null` for a date they cannot read, the way every other model in this package does. The console's inspection indexes were showing a raw datetime instance string.
- **`frequency` is gone from `inspection-form`** — nothing scheduled an inspection from it, and it is being dropped from the FleetOps API resource, report schema and console in fleetbase/fleetops#319.

The underscored attributes (`created_at`, `published_at`, …) are untouched, so anything needing a real `Date` is unaffected.

---
## Need help?
Expand Down
9 changes: 9 additions & 0 deletions addon/models/fuel-provider-connection.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export default class FuelProviderConnectionModel extends Model {
@attr('string') name;
@attr('string', { defaultValue: 'production' }) environment;
@attr('string', { defaultValue: 'configured' }) status;
@attr('raw') credentials;
@attr('raw') sync_settings;
@attr('raw') last_sync_state;
@attr('string') last_error;
Expand All @@ -25,6 +26,14 @@ export default class FuelProviderConnectionModel extends Model {
return this.name || this.provider;
}

@computed('last_sync_state.summary.imported') get lastImported() {
return String(this.last_sync_state?.summary?.imported ?? 0);
}

@computed('last_sync_state.summary.unmatched') get lastUnmatched() {
return String(this.last_sync_state?.summary?.unmatched ?? 0);
}

@computed('last_synced_at') get lastSyncedAt() {
return this.formatDate(this.last_synced_at);
}
Expand Down
23 changes: 23 additions & 0 deletions addon/models/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Model, { attr } from '@ember-data/model';

export default class FuelProviderSyncRunModel extends Model {
@attr('string') public_id;
@attr('string') fuel_provider_connection_uuid;
@attr('string') provider;
@attr('string') status;
@attr('string') error;
@attr('number') imported;
@attr('number') matched;
@attr('number') unmatched;
@attr('number') fuel_reports_created;
@attr('number') liters;
@attr('number') amount;
@attr('date') from;
@attr('date') to;
@attr('date') started_at;
@attr('date') finished_at;
@attr('date') created_at;
@attr('date') updated_at;
@attr('raw') summary;
@attr('raw') meta;
}
32 changes: 25 additions & 7 deletions addon/models/inspection-form.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { format as formatDate, isValid as isValidDate } from 'date-fns';

export default class InspectionFormModel extends Model {
@attr('string') uuid;
Expand All @@ -10,7 +12,6 @@ export default class InspectionFormModel extends Model {
@attr('string') description;
@attr('string') type;
@attr('string') status;
@attr('string') frequency;
@belongsTo('maintenance-subject', { polymorphic: true, async: false }) subject;
@attr('raw') items;
@attr('raw') settings;
Expand All @@ -25,15 +26,32 @@ export default class InspectionFormModel extends Model {
return this.name || this.public_id;
}

get createdAt() {
return this.created_at;
/**
* The display dates a table or a details panel reads. They returned the
* raw `Date`, which rendered as a full datetime instance string in a
* column; every other model in this package formats them here.
*/
@computed('created_at') get createdAt() {
if (!isValidDate(this.created_at)) {
return null;
}

return formatDate(this.created_at, 'yyyy-MM-dd HH:mm');
}

get updatedAt() {
return this.updated_at;
@computed('updated_at') get updatedAt() {
if (!isValidDate(this.updated_at)) {
return null;
}

return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm');
}

get publishedAt() {
return this.published_at;
@computed('published_at') get publishedAt() {
if (!isValidDate(this.published_at)) {
return null;
}

return formatDate(this.published_at, 'yyyy-MM-dd HH:mm');
}
}
39 changes: 31 additions & 8 deletions addon/models/inspection-submission.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import Model, { attr, belongsTo } from '@ember-data/model';
import { computed } from '@ember/object';
import { format as formatDate, isValid as isValidDate } from 'date-fns';

export default class InspectionSubmissionModel extends Model {
@attr('string') uuid;
Expand Down Expand Up @@ -43,19 +45,40 @@ export default class InspectionSubmissionModel extends Model {
return this.public_id || this.form_name || 'Inspection';
}

get createdAt() {
return this.created_at;
/**
* The display dates a table or a details panel reads — formatted here, as
* in every other model in this package, rather than handed out as a raw
* `Date` that renders as a full datetime instance string.
*/
@computed('created_at') get createdAt() {
if (!isValidDate(this.created_at)) {
return null;
}

return formatDate(this.created_at, 'yyyy-MM-dd HH:mm');
}

get updatedAt() {
return this.updated_at;
@computed('updated_at') get updatedAt() {
if (!isValidDate(this.updated_at)) {
return null;
}

return formatDate(this.updated_at, 'yyyy-MM-dd HH:mm');
}

get submittedAt() {
return this.submitted_at;
@computed('submitted_at') get submittedAt() {
if (!isValidDate(this.submitted_at)) {
return null;
}

return formatDate(this.submitted_at, 'yyyy-MM-dd HH:mm');
}

get resolvedAt() {
return this.resolved_at;
@computed('resolved_at') get resolvedAt() {
if (!isValidDate(this.resolved_at)) {
return null;
}

return formatDate(this.resolved_at, 'yyyy-MM-dd HH:mm');
}
}
14 changes: 13 additions & 1 deletion addon/serializers/fuel-provider-connection.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';

export default class FuelProviderConnectionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {}
export default class FuelProviderConnectionSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
serialize() {
const json = super.serialize(...arguments);

// Credentials are write-only. A fetched record has no credentials to send
// until the operator supplies a replacement.
if (json.credentials === null || json.credentials === undefined) {
delete json.credentials;
}

return json;
}
}
3 changes: 3 additions & 0 deletions addon/serializers/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';

export default class FuelProviderSyncRunSerializer extends ApplicationSerializer {}
6 changes: 5 additions & 1 deletion addon/serializers/maintenance-schedule.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { isBlank } from '@ember/utils';
import clearUnsetPolymorphicRelationships from '../utils/clear-unset-polymorphic-relationships';

export default class MaintenanceScheduleSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
Expand All @@ -26,6 +27,7 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer
const json = super.serialize(snapshot, options);
const readOnly = ['subject_name', 'default_assignee_name'];
readOnly.forEach((attr) => delete json[attr]);
clearUnsetPolymorphicRelationships(snapshot, json, ['subject', 'default_assignee']);
return json;
}

Expand All @@ -44,7 +46,9 @@ export default class MaintenanceScheduleSerializer extends ApplicationSerializer
let key = relationship.key;
let belongsTo = snapshot.belongsTo(key);

const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type'));
// Read the type through attributes(): these models declare no `<key>_type`
// attribute, and snapshot.attr() asserts on an undeclared one.
const isPolymorphicTypeBlank = isBlank(snapshot.attributes()[key + '_type']);
if (isPolymorphicTypeBlank) {
key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key;
if (!belongsTo) {
Expand Down
6 changes: 5 additions & 1 deletion addon/serializers/maintenance.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { isBlank } from '@ember/utils';
import clearUnsetPolymorphicRelationships from '../utils/clear-unset-polymorphic-relationships';

export default class MaintenanceSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
Expand Down Expand Up @@ -28,6 +29,7 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend(
const json = super.serialize(snapshot, options);
const readOnly = ['maintainable_name', 'performed_by_name', 'work_order_subject', 'duration_hours', 'is_overdue', 'days_until_due', 'cost_breakdown'];
readOnly.forEach((attr) => delete json[attr]);
clearUnsetPolymorphicRelationships(snapshot, json, ['maintainable', 'performed_by']);
return json;
}

Expand All @@ -46,7 +48,9 @@ export default class MaintenanceSerializer extends ApplicationSerializer.extend(
let key = relationship.key;
let belongsTo = snapshot.belongsTo(key);

const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type'));
// Read the type through attributes(): these models declare no `<key>_type`
// attribute, and snapshot.attr() asserts on an undeclared one.
const isPolymorphicTypeBlank = isBlank(snapshot.attributes()[key + '_type']);
if (isPolymorphicTypeBlank) {
key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key;
if (!belongsTo) {
Expand Down
6 changes: 5 additions & 1 deletion addon/serializers/work-order.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ApplicationSerializer from '@fleetbase/ember-core/serializers/application';
import { EmbeddedRecordsMixin } from '@ember-data/serializer/rest';
import { isBlank } from '@ember/utils';
import clearUnsetPolymorphicRelationships from '../utils/clear-unset-polymorphic-relationships';

export default class WorkOrderSerializer extends ApplicationSerializer.extend(EmbeddedRecordsMixin) {
/**
Expand All @@ -27,6 +28,7 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em
const json = super.serialize(snapshot, options);
const readOnly = ['target_name', 'assignee_name', 'is_overdue', 'days_until_due', 'completion_percentage'];
readOnly.forEach((attr) => delete json[attr]);
clearUnsetPolymorphicRelationships(snapshot, json, ['target', 'assignee']);
return json;
}

Expand All @@ -45,7 +47,9 @@ export default class WorkOrderSerializer extends ApplicationSerializer.extend(Em
let key = relationship.key;
let belongsTo = snapshot.belongsTo(key);

const isPolymorphicTypeBlank = isBlank(snapshot.attr(key + '_type'));
// Read the type through attributes(): these models declare no `<key>_type`
// attribute, and snapshot.attr() asserts on an undeclared one.
const isPolymorphicTypeBlank = isBlank(snapshot.attributes()[key + '_type']);
if (isPolymorphicTypeBlank) {
key = this.keyForAttribute ? this.keyForAttribute(key, 'serialize') : key;
if (!belongsTo) {
Expand Down
23 changes: 23 additions & 0 deletions addon/utils/clear-unset-polymorphic-relationships.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/**
* Send each unset polymorphic relationship as cleared.
*
* Ember Data only calls `serializePolymorphicType` for a related record that is
* present, so a removed relationship otherwise reaches the server as `null`
* embedded data while its `<key>_uuid` and `<key>_type` columns keep pointing
* at the old record.
*
* @param {Snapshot} snapshot
* @param {Object} json the serialized payload, changed in place
* @param {Array<String>} keys the polymorphic relationship names
* @return {Object} json
*/
export default function clearUnsetPolymorphicRelationships(snapshot, json, keys) {
for (const key of keys) {
if (!snapshot.belongsTo(key)) {
json[`${key}_uuid`] = null;
json[`${key}_type`] = null;
}
}

return json;
}
1 change: 1 addition & 0 deletions app/models/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/fleetops-data/models/fuel-provider-sync-run';
1 change: 1 addition & 0 deletions app/serializers/fuel-provider-sync-run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/fleetops-data/serializers/fuel-provider-sync-run';
1 change: 1 addition & 0 deletions app/utils/clear-unset-polymorphic-relationships.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from '@fleetbase/fleetops-data/utils/clear-unset-polymorphic-relationships';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@fleetbase/fleetops-data",
"version": "0.2.0",
"version": "0.2.1",
"description": "Fleetbase Fleet-Ops based models, serializers, transforms, adapters and GeoJson utility functions.",
"keywords": [
"fleetbase-data",
Expand Down
3 changes: 2 additions & 1 deletion tests/helpers/polymorphic-contract.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,14 @@
* Build the smallest snapshot `serializePolymorphicType` actually reads.
*
* @param {Object} [options]
* @param {Object} [options.attrs] values `snapshot.attr(key)` should return
* @param {Object} [options.attrs] values `snapshot.attr(key)` and `snapshot.attributes()` should return
* @param {Object|null} [options.belongsTo] value `snapshot.belongsTo(key)` should return
* @return {Object}
*/
export function snapshotStub({ attrs = {}, belongsTo = null } = {}) {
return {
attr: (key) => attrs[key],
attributes: () => attrs,
belongsTo: () => belongsTo,
};
}
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/models/fuel-provider-connection-test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { module, test } from 'qunit';
import { set } from '@ember/object';
import { setupTest } from 'dummy/tests/helpers';
import { FIXED_DATE, FIXED_DATE_LONG, THREE_DAYS_DISTANCE, assertDateGetters, assertDefaults, assertRelationships } from 'dummy/tests/helpers/model-contract';

Expand Down Expand Up @@ -35,6 +36,37 @@ module('Unit | Model | fuel provider connection', function (hooks) {
});

module('display and formatting', function () {
test('sync counts default to zero when a connection has no complete summary', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection');

assert.strictEqual(connection.lastImported, '0', 'a new connection has no imports');
assert.strictEqual(connection.lastUnmatched, '0', 'a new connection has no unmatched purchases');

for (const state of [null, {}, { summary: null }, { summary: {} }, { summary: { imported: null, unmatched: null } }]) {
connection.set('last_sync_state', state);
assert.strictEqual(connection.lastImported, '0', 'an absent import count has a readable fallback');
assert.strictEqual(connection.lastUnmatched, '0', 'an absent unmatched count has a readable fallback');
}
});

test('sync counts display the summary and update when a later sync changes it', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection', {
last_sync_state: { summary: { imported: 12, unmatched: 3 } },
});

assert.strictEqual(connection.lastImported, '12');
assert.strictEqual(connection.lastUnmatched, '3');

connection.set('last_sync_state', { summary: { imported: 5, unmatched: 0 } });
assert.strictEqual(connection.lastImported, '5', 'replacing the sync result invalidates the cached count');
assert.strictEqual(connection.lastUnmatched, '0', 'a successful zero count is preserved');

set(connection, 'last_sync_state.summary.imported', 8);
set(connection, 'last_sync_state.summary.unmatched', 2);
assert.strictEqual(connection.lastImported, '8', 'nested import updates invalidate the cached count');
assert.strictEqual(connection.lastUnmatched, '2', 'nested unmatched updates invalidate the cached count');
});

test('displayName prefers the connection name over the provider', function (assert) {
const connection = this.store.createRecord('fuel-provider-connection', { name: 'Shell APAC', provider: 'shell' });
assert.strictEqual(connection.displayName, 'Shell APAC');
Expand Down
Loading
Loading