Skip to content

Commit

Permalink
feat: latest audits
Browse files Browse the repository at this point in the history
  • Loading branch information
solaris007 committed Dec 21, 2024
1 parent 1da765c commit 5111972
Show file tree
Hide file tree
Showing 29 changed files with 861 additions and 100 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,31 @@ import BaseCollection from '../base/base.collection.js';
* @extends BaseCollection
*/
class AuditCollection extends BaseCollection {
// add custom methods here
// create a copy of the audit as a LatestAudit entity
async _onCreate(item) {
const collection = this.entityRegistry.getCollection('LatestAuditCollection');
await collection.create(item.toJSON());
}

// of the created audits, find the latest per site and auditType
// and create a LatestAudit copy for each
async _onCreateMany(items) {
const collection = this.entityRegistry.getCollection('LatestAuditCollection');
const latestAudits = items.createdItems.reduce((acc, audit) => {
const siteId = audit.getSiteId();
const auditType = audit.getAuditType();
const auditedAt = audit.getAuditedAt();
const key = `${siteId}-${auditType}`;

if (!acc[key] || acc[key].getAuditedAt() < auditedAt) {
acc[key] = audit;
}

return acc;
}, {});

await collection.createMany(Object.values(latestAudits).map((audit) => audit.toJSON()));
}
}

export default AuditCollection;
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Indexes Doc: https://electrodb.dev/en/modeling/indexes/

const schema = new SchemaBuilder(Audit, AuditCollection)
.addReference('belongs_to', 'Site', ['auditType', 'auditedAt'])
.addReference('has_one', 'LatestAudit', ['auditType'], { required: false })
.addReference('has_many', 'Opportunities')
.allowUpdates(false)
.allowRemove(false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/

import type {
BaseCollection, BaseModel, Opportunity, Site,
BaseCollection, BaseModel, LatestAudit, Opportunity, Site,
} from '../index';

export interface Audit extends BaseModel {
Expand All @@ -21,17 +21,10 @@ export interface Audit extends BaseModel {
getFullAuditRef(): string;
getIsError(): boolean;
getIsLive(): boolean;
getLatestAudit(): Promise<LatestAudit>;
getOpportunities(): Promise<Opportunity[]>;
getSite(): Promise<Site>;
getSiteId(): string;
setAuditResult(auditResult: object): Audit;
setAuditType(auditType: string): Audit;
setAuditedAt(auditedAt: number): Audit;
setFullAuditRef(fullAuditRef: string): Audit;
setIsError(isError: boolean): Audit;
setIsLive(isLive: boolean): Audit;
setSiteId(siteId: string): Audit;
toggleLive(): Audit;
}

export interface AuditCollection extends BaseCollection<Audit> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function findIndexNameByKeys(schema, keys) {

const index = schema.findIndexBySortKeys(keyNames);
if (index) {
return index.index;
return index.index || INDEX_TYPES.PRIMARY;
}

const allIndex = schema.findIndexByType(INDEX_TYPES.ALL);
Expand Down Expand Up @@ -150,6 +150,47 @@ class BaseCollection {
this._accessorCache = {};
}

async #onCreate(item) {
try {
await this._onCreate(item);
} catch (error) {
this.log.error('On-create handler failed', error);
}
}

async #onCreateMany({ createdItems, errorItems }) {
try {
await this._onCreateMany({ createdItems, errorItems });
} catch (error) {
this.log.error('On-create-many handler failed', error);
}
}

/**
* Handler for the create method. This method is
* called after the create method has successfully created an entity.
* @param {BaseModel} item - The created entity.
* @return {Promise<void>}
* @protected
*/
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
async _onCreate(item) {
// no-op
}

/**
* Handler for the createMany method. This method is
* called after the createMany method has successfully created entities.
* @param {Array<BaseModel>} createdItems - The created entities.
* @param {{ item: Object, error: ValidationError }[]} errorItems - Items that failed validation.
* @return {Promise<void>}
* @private
*/
// eslint-disable-next-line class-methods-use-this,@typescript-eslint/no-unused-vars
async _onCreateMany({ createdItems, errorItems }) {
// no-op
}

/**
* General method to query entities by index keys. This method is used by other
* query methods to perform the actual query operation. It will use the index keys
Expand Down Expand Up @@ -303,6 +344,7 @@ class BaseCollection {
const instance = this.#createInstance(record.data);

this.#invalidateCache();
await this.#onCreate(instance);

return instance;
} catch (error) {
Expand Down Expand Up @@ -383,6 +425,8 @@ class BaseCollection {

this.log.info(`Created ${createdItems.length} items for [${this.entityName}]`);

this.#onCreateMany({ createdItems, errorItems });

return { createdItems, errorItems };
} catch (error) {
this.log.error(`Failed to create many [${this.entityName}]`, error);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import ExperimentCollection from '../experiment/experiment.collection.js';
import ImportJobCollection from '../import-job/import-job.collection.js';
import ImportUrlCollection from '../import-url/import-url.collection.js';
import KeyEventCollection from '../key-event/key-event.collection.js';
import LatestAuditCollection from '../latest-audit/latest-audit.collection.js';
import OpportunityCollection from '../opportunity/opportunity.collection.js';
import OrganizationCollection from '../organization/organization.collection.js';
import SiteCandidateCollection from '../site-candidate/site-candidate.collection.js';
Expand All @@ -33,6 +34,7 @@ import ExperimentSchema from '../experiment/experiment.schema.js';
import ImportJobSchema from '../import-job/import-job.schema.js';
import ImportUrlSchema from '../import-url/import-url.schema.js';
import KeyEventSchema from '../key-event/key-event.schema.js';
import LatestAuditSchema from '../latest-audit/latest-audit.schema.js';
import OpportunitySchema from '../opportunity/opportunity.schema.js';
import OrganizationSchema from '../organization/organization.schema.js';
import SiteSchema from '../site/site.schema.js';
Expand Down Expand Up @@ -127,6 +129,7 @@ EntityRegistry.registerEntity(ExperimentSchema, ExperimentCollection);
EntityRegistry.registerEntity(ImportJobSchema, ImportJobCollection);
EntityRegistry.registerEntity(ImportUrlSchema, ImportUrlCollection);
EntityRegistry.registerEntity(KeyEventSchema, KeyEventCollection);
EntityRegistry.registerEntity(LatestAuditSchema, LatestAuditCollection);
EntityRegistry.registerEntity(OpportunitySchema, OpportunityCollection);
EntityRegistry.registerEntity(OrganizationSchema, OrganizationCollection);
EntityRegistry.registerEntity(SiteSchema, SiteCollection);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,20 @@

import type { ValidationError } from '../../errors';

export interface MultiStatusCreateResult<T> {
createdItems: T[],
errorItems: { item: object, error: ValidationError }[],
}

export interface BaseModel {
getCreatedAt(): string;
getId(): string;
getUpdatedAt(): string;
remove(): Promise<this>;
save(): Promise<this>;
toJSON(): object;
}

export interface MultiStatusCreateResult<T> {
createdItems: T[],
errorItems: { item: object, error: ValidationError }[],
_onCreate(item: BaseModel): void;
_onCreateMany(items: MultiStatusCreateResult<BaseModel>): void;
}

export interface QueryOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,48 @@ import {
referenceToBaseMethodName,
} from '../../util/util.js';

const createSortKeyAccessorConfigs = (
entity,
baseConfig,
baseMethodName,
target,
targetCollection,
foreignKeyName,
foreignKeyValue,
log,
) => {
const configs = [];

const belongsToRef = targetCollection.schema.getReferenceByTypeAndTarget(
// eslint-disable-next-line no-use-before-define
Reference.TYPES.BELONGS_TO,
entity.schema.getModelName(),
);

if (!belongsToRef) {
log.warn(`Reciprocal reference not found for ${entity.schema.getModelName()} to ${target}`);
return configs;
}

const sortKeys = belongsToRef.getSortKeys();
if (!isNonEmptyArray(sortKeys)) {
log.debug(`No sort keys defined for ${entity.schema.getModelName()} to ${target}`);
return configs;
}

for (let i = 1; i <= sortKeys.length; i += 1) {
const subset = sortKeys.slice(0, i);
configs.push({
name: keyNamesToMethodName(subset, `${baseMethodName}By`),
requiredKeys: subset,
foreignKey: { name: foreignKeyName, value: foreignKeyValue },
...baseConfig,
});
}

return configs;
};

class Reference {
static TYPES = {
BELONGS_TO: 'belongs_to',
Expand Down Expand Up @@ -100,6 +142,20 @@ class Reference {
requiredKeys: [],
foreignKey: { name: foreignKeyName, value: foreignKeyValue },
});

accessorConfigs.push(
...createSortKeyAccessorConfigs(
entity,
{},
baseMethodName,
target,
targetCollection,
foreignKeyName,
foreignKeyValue,
log,
),
);

break;
}

Expand All @@ -115,32 +171,19 @@ class Reference {
foreignKey: { name: foreignKeyName, value: foreignKeyValue },
});

const belongsToRef = targetCollection.schema.getReferenceByTypeAndTarget(
Reference.TYPES.BELONGS_TO,
entity.schema.getModelName(),
accessorConfigs.push(
...createSortKeyAccessorConfigs(
entity,
{ all: true },
baseMethodName,
target,
targetCollection,
foreignKeyName,
foreignKeyValue,
log,
),
);

if (!belongsToRef) {
log.warn(`Reciprocal reference not found for ${entity.schema.getModelName()} to ${target}`);
break;
}

const sortKeys = belongsToRef.getSortKeys();
if (!isNonEmptyArray(sortKeys)) {
log.debug(`No sort keys defined for ${entity.schema.getModelName()} to ${target}`);
break;
}

for (let i = 1; i <= sortKeys.length; i += 1) {
const subset = sortKeys.slice(0, i);
accessorConfigs.push({
name: keyNamesToMethodName(subset, `${baseMethodName}By`),
requiredKeys: subset,
all: true,
foreignKey: { name: foreignKeyName, value: foreignKeyValue },
});
}

break;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,15 @@ class SchemaBuilder {
});
}

withPrimaryPartitionKeys(partitionKeys) {
if (!isNonEmptyArray(partitionKeys)) {
throw new SchemaBuilderError('Partition keys are required and must be a non-empty array.');
}
this.rawIndexes.primary.pk.composite = partitionKeys;

return this;
}

/**
* Sets the sort keys for the primary index (main table). The given sort keys
* together with the entity id (partition key) will form the primary key. This will
Expand Down Expand Up @@ -289,7 +298,7 @@ class SchemaBuilder {
*
* @param {string} type - One of Reference.TYPES (BELONGS_TO, HAS_MANY, HAS_ONE).
* @param {string} entityName - The referenced entity name.
* @param {Array<string>} [sortKeys=['updatedAt']] - The attributes to form the sort key.
* @param {Array<string>} [sortKeys=[]] - The attributes to form the sort key.
* @param {object} [options] - Additional reference options.
* @param {boolean} [options.required=true] - Whether the reference is required. Only applies to
* BELONGS_TO references.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type * from './configuration/index.d.ts';
export type * from './base/index.d.ts';
export type * from './experiment/index.d.ts';
export type * from './key-event/index.d.ts';
export type * from './latest-audit/index.d.ts';
export type * from './opportunity/index.d.ts';
export type * from './organization/index.d.ts';
export type * from './site/index.d.ts';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './experiment/index.js';
export * from './import-job/index.js';
export * from './import-url/index.js';
export * from './key-event/index.js';
export * from './latest-audit/index.js';
export * from './opportunity/index.js';
export * from './organization/index.js';
export * from './site-candidate/index.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2024 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/

import type {
BaseCollection, BaseModel, Opportunity, Site,
} from '../index';

export interface LatestAudit extends BaseModel {
getAuditId(): object;
getAuditResult(): object;
getAuditType(): string;
getAuditedAt(): number;
getFullAuditRef(): string;
getIsError(): boolean;
getIsLive(): boolean;
getOpportunities(): Promise<Opportunity[]>;
getSite(): Promise<Site>;
getSiteId(): string;
}

export interface LatestAuditCollection extends BaseCollection<LatestAudit> {
allBySiteId(siteId: string): Promise<LatestAudit[]>;
findBySiteIdAndAuditType(siteId: string, auditType: string): Promise<LatestAudit[]>;
}
Loading

0 comments on commit 5111972

Please sign in to comment.