UNPKG

dataverse-schema

Version:

A Schema library designed for Microsoft Dataverse to aid in Web API calls

1,619 lines (1,511 loc) 59.3 kB
const Etag = Symbol("etag"); const globalConfig = { url: `${location.origin}/api/data/v9.2`, headers: { "OData-MaxVersion": "4.0", "OData-Version": "4.0", "Content-Type": "application/json; charset=utf-8", "If-None-Match": "null", Accept: "application/json", MSCRMCallerID: localStorage.getItem("MSCRMCallerID") ?? "", CallerObjectId: localStorage.getItem("CallerObjectId") ?? "" } }; function setConfig(config) { globalConfig.headers = { ...globalConfig.headers, ...config.headers }; if (config.url) globalConfig.url = config.url; } const parenthesesRegEx = /\(([^)]*)\)/g; async function tryFetch(url, init) { const response = await fetch(url, { ...init, headers: { ...globalConfig.headers, ...init?.headers } }); if (response.headers.get("Content-Type")?.includes("application/json")) { const data = await response.json(); if (data.error) { if (data.error.code === "0x80060891") return null; throw data.error; } return data; } if (!response.ok) { throw new Error(response.status + "-" + response.statusText); } if (response.status == 204) { const entityId = response.headers.get("OData-EntityId"); if (entityId) return parenthesesRegEx.exec(entityId)?.[1]; return; } return await response.text(); } async function fetchChoices(name) { return tryFetch(`${globalConfig.url}/GlobalOptionSetDefinitions(Name=${wrapString(name)})`).then( (v) => mapChoices(v) ); } function mapChoices(data) { return [...data.Options].map((option) => ({ value: Number(option.Value), color: String(option.Color), label: String(option.Label.UserLocalizedLabel.Label), description: String(option.Description.UserLocalizedLabel.Label) })); } function xml(raw, ...values) { let result = String.raw(raw, values); result = result.replace(/>\s*/g, ">"); result = result.replace(/\s*</g, "<"); return result; } async function fetchXml(entitySetName, xml2) { return tryFetch(`${globalConfig.url}/${entitySetName}?fetchXml=${xml2}`).then( (v) => v.value ); } function toBase64(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const url = reader.result; const index = url?.toString().indexOf("base64") ?? 0; resolve(url?.slice(index + 7)); }; reader.onerror = reject; }); } function base64ImageToURL(base64) { return `data:${detectImageType(base64)};base64,${base64}`; } function detectImageType(base64) { if (base64.startsWith("iVBORw0KGgoAAAANSUhEUgAA")) { return "image/png"; } else if (base64.startsWith("/9j/4AAQSkZJRgABAQEAYABgAAD/")) { return "image/jpeg"; } else if (base64.startsWith("R0lGODlh")) { return "image/gif"; } else { return "image/png"; } } function getImageUrl(entity, name, id) { return `${location.origin}/Image/download.aspx?Entity=${entity}&Attribute=${name}&Id=${id}&Full=true`; } function isNonEmptyString(value) { return typeof value === "string" && value.length > 0; } const rxGUID = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/; const rxDateOnly = /^\d{4}-\d{2}-\d{2}$/; function wrapString(value) { return typeof value === "string" && !rxGUID.test(value) && !rxDateOnly.test(value) ? `'${value}'` : String(value); } function attachEtag(v) { if (v && typeof v === "object") v[Etag] = v["@odata.etag"]; return v; } function mergeRecords(prevRecords, newRecords) { const prevMap = new Map(prevRecords.map((v) => [v[Etag], v])); return newRecords.map((v) => prevMap.get(v[Etag]) ?? v); } async function RetrieveAadUserRoles(aadId) { return tryFetch( `${globalConfig.url}/RetrieveAadUserRoles(DirectoryObjectId=${aadId})?$select=name` ).then((d) => new Set(d.value.map((r) => r.name))); } async function RetrieveTotalRecordCount(logicalName) { return tryFetch( `${globalConfig.url}/RetrieveTotalRecordCount(EntityNames=['${logicalName}'])` ).then((d) => d.Values[0]); } async function WhoAmI() { return tryFetch(`${globalConfig.url}/WhoAmI()`).then((r) => ({ BusinessUnitId: r.BusinessUnitId, UserId: r.UserId, OrganizationId: r.OrganizationId })); } async function updatePropertyValue(entitySetName, id, propertyName, value) { await tryFetch( `${globalConfig.url}/${entitySetName}(${id})/${propertyName}`, { method: "PUT", body: JSON.stringify({ value }) } ); return id; } async function activateRecord(entitySetName, id) { return updatePropertyValue(entitySetName, id, "statecode", 0); } async function associateRecord(entitySetName, parentId, propertyName, childEntitySetName, childId) { await tryFetch( `${globalConfig.url}/${entitySetName}(${parentId})/${propertyName}/$ref`, { method: "PUT", body: JSON.stringify({ "@odata.id": `${globalConfig.url}/${childEntitySetName}(${childId})` }) } ); return childId; } async function disssociateRecord(entitySetName, parentId, propertyName, childId) { await tryFetch( `${globalConfig.url}/${entitySetName}(${parentId})/${propertyName}${childId ? `(${childId})` : ""}/$ref`, { method: "DELETE" } ); return childId ?? parentId; } async function getAssociatedRecords(entitySetName, id, navigationPropertyName, query = "") { return tryFetch( `${globalConfig.url}/${entitySetName}(${id})/${navigationPropertyName}?${query}` ).then((r) => r.value.map(attachEtag)); } async function associateRecordToList(entitySetName, parentId, propertyName, childEntitySetName, childPrimaryKeyName, childIds) { const currentIds = (await getAssociatedRecords( entitySetName, parentId, propertyName, `$select=${childPrimaryKeyName}` )).map((r) => r[childPrimaryKeyName]); const promises = []; for (const id of childIds) { if (!currentIds.includes(id)) promises.push( associateRecord( entitySetName, parentId, propertyName, childEntitySetName, id ) ); } for (const id of currentIds) { if (!childIds.includes(id)) promises.push( disssociateRecord(entitySetName, parentId, propertyName, id) ); } await Promise.all(promises); return childIds; } async function deactivateRecord(entitySetName, id) { return updatePropertyValue(entitySetName, id, "statecode", 1); } async function deletePropertyValue(entitySetName, id, propertyName) { await tryFetch( `${globalConfig.url}/${entitySetName}(${id})/${propertyName}`, { method: "DELETE" } ); return id; } async function deleteRecord(entitySetName, id) { await tryFetch(`${globalConfig.url}/${entitySetName}(${id})`, { method: "DELETE" }); return id; } async function getAssociatedRecord(entitySetName, id, navigationPropertyName, query) { return tryFetch( `${globalConfig.url}/${entitySetName}(${id})/${navigationPropertyName}?${query}` ).then(attachEtag); } async function getNextLink(result) { if (result["@odata.nextLink"]) { const r = await getNextLink(await tryFetch(result["@odata.nextLink"])); return [...result.value, ...r]; } return result.value; } function getPropertyRawValueURL(entitySetName, id, propertyName) { return `${globalConfig.url}/${entitySetName}(${id})/${propertyName}/$value`; } async function getPropertyRawValue(entitySetName, id, propertyName) { return tryFetch(getPropertyRawValueURL(entitySetName, id, propertyName)); } async function getPropertyValue(entitySetName, id, propertyName) { return tryFetch( `${globalConfig.url}/${entitySetName}(${id})/${propertyName}` ).then((r) => r.value); } async function getRecord(entitySetName, id, query) { return tryFetch(`${globalConfig.url}/${entitySetName}(${id})?${query}`).then(attachEtag); } async function getRecords(entitySetName, query) { return tryFetch(`${globalConfig.url}/${entitySetName}?${query}`).then(getNextLink).then((v) => v.map(attachEtag)); } async function patchRecord(entitySetName, id, value, query = "") { return tryFetch(`${globalConfig.url}/${entitySetName}(${id})?${query}`, { method: "PATCH", headers: { Prefer: "return=representation" }, body: JSON.stringify(value) }); } async function postRecord(entitySetName, value, query = "") { return tryFetch(`${globalConfig.url}/${entitySetName}?${query}`, { method: "POST", headers: { Prefer: "return=representation" }, body: JSON.stringify(value) }); } async function postRecordGetId(entitySetName, value) { return tryFetch(`${globalConfig.url}/${entitySetName}`, { method: "POST", body: JSON.stringify(value) }); } function required() { return (v) => { if (!v) return "Required"; }; } class Schema { name; kind = "schema"; type = "schema"; /** * The default value of the property. This is a private field. */ #default; /** * Creates a new Property instance. * * @param name The name of the property. * @param defaultValue The default value for the property. */ constructor(name, defaultValue) { this.name = name; this.#default = defaultValue; } /** * Sets the default value of the property. * * @param value The new default value. * @returns The Property instance for chaining. */ setDefault(value) { this.#default = value; return this; } /** * Gets the default value of the property. * * @returns The default value. */ getDefault() { return this.#default; } /** * Indicates if the property is read-only. This is a private field. */ #readOnly = false; /** * Sets whether the property is read-only. * * @param [value=true] True if the property should be read-only, false otherwise. Defaults to true. * @returns The Property instance for chaining. */ setReadOnly(value = true) { this.#readOnly = value; return this; } /** * Gets whether the property is read-only. * * @returns True if the property is read-only, false otherwise. */ getReadOnly() { return this.#readOnly; } /** * An array of validators associated with this property. */ #validators = []; /** * Adds a validator to the property. * * @param v The validator function or object to add. * @returns The Property instance for chaining. */ check(v) { this.#validators.push(v); return this; } /** * Adds a required validator to the property. * * @returns The Property instance for chaining. */ required() { return this.check(required()); } /** * Transforms a value received from Dataverse into the property's type. * By default, it returns the value as is. Subclasses can override this for custom transformations. * * @param value The value received from Dataverse. * @returns The transformed value of type T. */ transformValueFromDataverse(value) { return value; } /** * Transforms the property's value into a format suitable for sending to Dataverse. * By default, it returns the value as is. Subclasses can override this for custom transformations. * * @param value The property's value. * @returns The transformed value suitable for Dataverse. */ transformValueToDataverse(value) { return value; } getIssues(value, path = []) { const issues = []; this.#validators.forEach((fn) => { try { let message = fn(value); if (message) { issues.push({ message, path }); } } catch ({ message }) { issues.push({ message, path }); } }); return issues; } validate(value, path = []) { const issues = this.getIssues(value, path); return issues.length > 0 ? { issues } : { value }; } parse(value) { const result = this.validate(value); if (result.issues) { throw new Error(JSON.stringify(result.issues), {}); } else { return result.value; } } /** * Provides access to the standard schema properties for this property. * This is a computed property. * * @returns An object containing the standard schema properties, including version, vendor, and a validation function. */ get ["~standard"]() { return { version: 1, vendor: "dataverse-schema", validate: (v) => this.validate(v) }; } } function isType(type) { return (v) => { if (typeof v !== type) { return "Not of type " + type; } }; } class BooleanProperty extends Schema { /** * The kind of schema element for a boolean property, which is "value". */ kind = "value"; /** * The type of the property, which is "boolean". */ type = "boolean"; /** * Creates a new BooleanProperty instance. * * @param name The name of the boolean property. */ constructor(name) { super(name, false); this.check(isType("boolean")); } } function boolean(name) { return new BooleanProperty(name); } class CollectionProperty extends Schema { kind = "navigation"; type = "collection"; #getTable; /** * Creates a new CollectionProperty instance. * * @param name The name of the collection property. * @param getTable A function that, when called, returns the Table definition for the related records. This is used to avoid circular dependencies. */ constructor(name, getTable) { super(name, []); this.#getTable = getTable; this.check( (v) => !Array.isArray(v) ? "value is not an array" : void 0 ); } #table; get table() { return this.#table ??= this.#getTable(); } /** * Transforms an array of values received from Dataverse into an array of transformed related records. * It iterates over the input array and uses the `transformValueFromDataverse` method of the related Table to transform each individual record. * * @param value An array of raw data representing the related records from Dataverse. * @returns An array of transformed related records of type `Infer<TProperties>[]`. */ transformValueFromDataverse(value) { return Array.from(value).map( (v) => this.table.transformValueFromDataverse(v) ); } getIssues(value, path = []) { const issues = super.getIssues(value, path); if (Array.isArray(value)) { issues.push(...value.map((v, i) => this.table.getIssues(v, [...path, i])).flat(1)); } return issues; } } function collection(name, getTable) { return new CollectionProperty(name, getTable); } class DateProperty extends Schema { /** * The kind of schema element for a date property, which is "value". */ kind = "value"; /** * The type of the property, which is "date". */ type = "date"; /** * Creates a new DateProperty instance. * * @param name The name of the date property. */ constructor(name) { super(name, null); this.check( (v) => v === null || v instanceof Date ? void 0 : "value is not Date or null" ); } /** * Transforms a value received from Dataverse into a Date object or null. * If the value is null, it returns null. Otherwise, it creates a new Date object from the Dataverse value. * * @param value The value received from Dataverse. * @returns A Date object or null. */ transformValueFromDataverse(value) { if (value === null) return null; return new Date(value); } } function date(name) { return new DateProperty(name); } class DateOnlyProperty extends Schema { /** * The kind of schema element for a date property, which is "value". */ kind = "value"; /** * The type of the property, which is "date". */ type = "dateOnly"; /** * Creates a new DateProperty instance. * * @param name The name of the date property. */ constructor(name) { super(name, null); this.check( (v) => v === null || v instanceof Date ? void 0 : "value is not Date or null" ); } /** * Transforms a value received from Dataverse into a Date object or null. * If the value is null, it returns null. Otherwise, it creates a new Date object from the Dataverse value. * * @param value The value received from Dataverse. * @returns A Date object or null. */ transformValueFromDataverse(value) { if (value === null) return null; return parseDateOnly(value); } transformValueToDataverse(value) { return toDateOnly(value); } } function dateOnly(name) { return new DateOnlyProperty(name); } function parseDateOnly(dateString) { const [year, month, day] = dateString.slice(0, 10).split("-").map(Number); return new Date(year ?? 0, (month ?? 0) - 1, day); } function toDateOnly(date) { try { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, "0"); const day = String(date.getDate()).padStart(2, "0"); return `${year}-${month}-${day}`; } catch (e) { return null; } } class LookupProperty extends Schema { /** * The kind of schema element for an expand property, which is "navigation". */ kind = "navigation"; /** * The type of the property, which is "expand". */ type = "lookup"; /** * A function that returns the Table definition for the related record. */ #getTable; /** * Creates a new ExpandProperty instance. * * @param name The name of the expand property. * @param getTable A function that, when called, returns the Table definition for the related record. This is used to avoid circular dependencies. */ constructor(name, getTable) { super(name, null); this.#getTable = getTable; } #table; get table() { return this.#table ??= this.#getTable(); } /** * Transforms a value received from Dataverse into a transformed related record or null. * It uses the `transformValueFromDataverse` method of the related Table to transform the data. If the value is null or undefined, it returns null. * * @param value The raw data representing the related record from Dataverse. * @returns The transformed related record of type `Infer<TProperties>` or null. */ transformValueFromDataverse(value) { return value == null ? null : this.table.transformValueFromDataverse(value); } getIssues(value, path) { const issues = super.getIssues(value, path); if (value !== null) { issues.push(...this.table.getIssues(value, path)); } return issues; } } function lookup(name, getTable) { return new LookupProperty(name, getTable); } class FormattedProperty extends Schema { /** * The kind of schema element for a string property, which is "value". */ kind = "value"; /** * The type of the property, which is "string". */ type = "formatted"; /** * Creates a new StringProperty instance. * * @param name The name of the string property. */ constructor(name) { super(`${name}@OData.Community.Display.V1.FormattedValue`, null); this.setReadOnly(true); } } function formatted(name) { return new FormattedProperty(name); } function isTypeOrNull(type) { return (v) => { if (v === null) return; if (typeof v !== type) { return "Not of type " + type; } }; } class ImageProperty extends Schema { /** * The kind of schema element for an image property, which is "value". */ kind = "value"; /** * The type of the property, which is "image". */ type = "image"; /** * Creates a new ImageProperty instance. * * @param name The name of the image property. */ constructor(name) { super(name, null); this.check(isTypeOrNull("string")); } } function image(name) { return new ImageProperty(name); } class ListProperty extends Schema { /** * The kind of schema element for a list property, which is "value". */ kind = "value"; /** * The type of the property, which is "list". */ type = "list"; /** * The array of valid values for this list property. */ list; /** * Creates a new ListProperty instance. * * @param name The name of the list property. * @param list An array of valid string or number values for this property. */ constructor(name, list2) { super(name, null); this.list = list2; this.check((v) => { if (v !== null && !list2.includes(v)) { return `${v} not in [${list2}]`; } }); } } function list(name, list2) { return new ListProperty(name, list2); } function groupby(values, aggregations) { return `groupby((${values.filter(isNonEmptyString).join(",")})${aggregations ? "," + aggregations : ""})`; } function aggregate(...values) { return `aggregate(${values.filter(isNonEmptyString).join(",")})`; } function average(name, alias = name) { return `${name} with average as ${alias}`; } function sum(name, alias = name) { return `${name} with sum as ${alias}`; } function min(name, alias = name) { return `${name} with min as ${alias}`; } function max(name, alias = name) { return `${name} with max as ${alias}`; } function count(alias = "count") { return `$count as ${alias}`; } function and(...conditions) { const validConditions = conditions.filter(isNonEmptyString); if (validConditions.length === 0) return ""; return `(${validConditions.join(" and ")})`; } function or(...conditions) { const validConditions = conditions.filter(isNonEmptyString); if (validConditions.length === 0) return ""; return `(${validConditions.join(" or ")})`; } function not(condition) { return isNonEmptyString(condition) ? `not(${condition})` : ""; } function contains(name, value) { return `contains(${name},${wrapString(value)})`; } function startsWith(name, value) { return `startswith(${name},${wrapString(value)})`; } function endsWith(name, value) { return `endswith(${name},${wrapString(value)})`; } function equals(name, value) { return `(${name} eq ${wrapString(value)})`; } function notEquals(name, value) { return `(${name} ne ${wrapString(value)})`; } function greaterThan(name, value) { return `(${name} gt ${wrapString(value)})`; } function greaterThanOrEqual(name, value) { return `(${name} ge ${wrapString(value)})`; } function lessThan(name, value) { return `(${name} lt ${wrapString(value)})`; } function lessThanOrEqual(name, value) { return `(${name} le ${wrapString(value)})`; } function isActive() { return "statecode eq 0"; } function isInactive() { return "statecode eq 1"; } function isNull(name) { return `${name} eq null`; } function isNotNull(name) { return `${name} ne null`; } function Above(name, value) { return `Microsoft.Dynamics.CRM.Above(PropertyName=${wrapString(name)},PropertyValue=${wrapString(value)})`; } function AboveOrEqual(name, value) { return `Microsoft.Dynamics.CRM.AboveOrEqual(PropertyName=${wrapString(name)},PropertyValue=${wrapString(value)})`; } function Between(name, value1, value2) { return `Microsoft.Dynamics.CRM.Between(PropertyName=${wrapString(name)},PropertyValues=[${wrapString(value1)},${wrapString( value2 )}])`; } function Contains(name, value) { return `Microsoft.Dynamics.CRM.Contains(PropertyName=${wrapString(name)},PropertyValue=${wrapString(value)})`; } function ContainsValues(name, ...values) { return `Microsoft.Dynamics.CRM.ContainsValues(PropertyName=${wrapString(name)},PropertyValues=[${values.map(wrapString).join(",")}])`; } function DoesNotContainValues(name, ...values) { return `Microsoft.Dynamics.CRM.DoesNotContainValues(PropertyName=${wrapString(name)},PropertyValues=[${values.map(wrapString).join(",")}])`; } function EqualBusinessId(name) { return `Microsoft.Dynamics.CRM.EqualBusinessId(PropertyName=${wrapString(name)})`; } function EqualRoleBusinessId(name) { return `Microsoft.Dynamics.CRM.EqualRoleBusinessId(PropertyName=${wrapString(name)})`; } function EqualUserId(name) { return `Microsoft.Dynamics.CRM.EqualUserId(PropertyName=${wrapString(name)})`; } function EqualUserLanguage(name) { return `Microsoft.Dynamics.CRM.EqualUserLanguage(PropertyName=${wrapString(name)})`; } function EqualUserOrUserHierarchy(name) { return `Microsoft.Dynamics.CRM.EqualUserOrUserHierarchy(PropertyName=${wrapString(name)})`; } function EqualUserOrUserHierarchyAndTeams(name) { return `Microsoft.Dynamics.CRM.EqualUserOrUserHierarchyAndTeams(PropertyName=${wrapString(name)})`; } function EqualUserOrUserTeams(name) { return `Microsoft.Dynamics.CRM.EqualUserOrUserTeams(PropertyName=${wrapString(name)})`; } function EqualUserTeams(name) { return `Microsoft.Dynamics.CRM.EqualUserTeams(PropertyName=${wrapString(name)})`; } function In(name, ...values) { return `Microsoft.Dynamics.CRM.In(PropertyName=${wrapString(name)},PropertyValues=[${values.map(wrapString).join(",")}])`; } function InFiscalPeriod(name, value) { return `Microsoft.Dynamics.CRM.InFiscalPeriod(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function InFiscalPeriodAndYear(name, fiscalPeriod, fiscalYear) { return `Microsoft.Dynamics.CRM.InFiscalPeriodAndYear(PropertyName=${wrapString(name)},PropertyValue1=${fiscalPeriod},PropertyValue2=${fiscalYear})`; } function InFiscalYear(name, value) { return `Microsoft.Dynamics.CRM.InFiscalYear(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function InOrAfterFiscalPeriodAndYear(name, fiscalPeriod, fiscalYear) { return `Microsoft.Dynamics.CRM.InOrAfterFiscalPeriodAndYear(PropertyName=${wrapString(name)},PropertyValue1=${fiscalPeriod},PropertyValue2=${fiscalYear})`; } function InOrBeforeFiscalPeriodAndYear(name, fiscalPeriod, fiscalYear) { return `Microsoft.Dynamics.CRM.InOrBeforeFiscalPeriodAndYear(PropertyName=${wrapString(name)},PropertyValue1=${fiscalPeriod},PropertyValue2=${fiscalYear})`; } function Last7Days(name) { return `Microsoft.Dynamics.CRM.Last7Days(PropertyName=${wrapString(name)})`; } function LastFiscalPeriod(name) { return `Microsoft.Dynamics.CRM.LastFiscalPeriod(PropertyName=${wrapString(name)})`; } function LastFiscalYear(name) { return `Microsoft.Dynamics.CRM.LastFiscalYear(PropertyName=${wrapString(name)})`; } function LastMonth(name) { return `Microsoft.Dynamics.CRM.LastMonth(PropertyName=${wrapString(name)})`; } function LastWeek(name) { return `Microsoft.Dynamics.CRM.LastWeek(PropertyName=${wrapString(name)})`; } function LastXDays(name, value) { return `Microsoft.Dynamics.CRM.LastXDays(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXFiscalPeriods(name, value) { return `Microsoft.Dynamics.CRM.LastXFiscalPeriods(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXFiscalYears(name, value) { return `Microsoft.Dynamics.CRM.LastXFiscalYears(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXHours(name, value) { return `Microsoft.Dynamics.CRM.LastXHours(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXMonths(name, value) { return `Microsoft.Dynamics.CRM.LastXMonths(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXWeeks(name, value) { return `Microsoft.Dynamics.CRM.LastXWeeks(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastXYears(name, value) { return `Microsoft.Dynamics.CRM.LastXYears(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function LastYear(name) { return `Microsoft.Dynamics.CRM.LastYear(PropertyName=${wrapString(name)})`; } function Next7Days(name) { return `Microsoft.Dynamics.CRM.Next7Days(PropertyName=${wrapString(name)})`; } function NextFiscalPeriod(name) { return `Microsoft.Dynamics.CRM.NextFiscalPeriod(PropertyName=${wrapString(name)})`; } function NextFiscalYear(name) { return `Microsoft.Dynamics.CRM.NextFiscalYear(PropertyName=${wrapString(name)})`; } function NextMonth(name) { return `Microsoft.Dynamics.CRM.NextMonth(PropertyName=${wrapString(name)})`; } function NextWeek(name) { return `Microsoft.Dynamics.CRM.NextWeek(PropertyName=${wrapString(name)})`; } function NextXDays(name, value) { return `Microsoft.Dynamics.CRM.NextXDays(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXFiscalPeriods(name, value) { return `Microsoft.Dynamics.CRM.NextXFiscalPeriods(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXFiscalYears(name, value) { return `Microsoft.Dynamics.CRM.NextXFiscalYears(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXHours(name, value) { return `Microsoft.Dynamics.CRM.NextXHours(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXMonths(name, value) { return `Microsoft.Dynamics.CRM.NextXMonths(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXWeeks(name, value) { return `Microsoft.Dynamics.CRM.NextXWeeks(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextXYears(name, value) { return `Microsoft.Dynamics.CRM.NextXYears(PropertyName=${wrapString(name)},PropertyValue=${value})`; } function NextYear(name) { return `Microsoft.Dynamics.CRM.NextYear(PropertyName=${wrapString(name)})`; } function NotBetween(name, value1, value2) { return `Microsoft.Dynamics.CRM.NotBetween(PropertyName=${wrapString(name)},PropertyValues=[${wrapString( value1 )},${wrapString(value2)}])`; } function NotEqualBusinessId(name) { return `Microsoft.Dynamics.CRM.NotEqualBusinessId(PropertyName=${wrapString(name)})`; } function NotEqualUserId(name) { return `Microsoft.Dynamics.CRM.NotEqualUserId(PropertyName=${wrapString(name)})`; } function NotIn(name, ...values) { return `Microsoft.Dynamics.CRM.NotIn(PropertyName=${wrapString(name)},PropertyValues=[${values.map(wrapString).join(",")}])`; } function NotUnder(name, value) { return `Microsoft.Dynamics.CRM.NotUnder(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXDays(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXDays(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXHours(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXHours(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXMinutes(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXMinutes(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXMonths(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXMonths(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXWeeks(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXWeeks(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OlderThanXYears(name, value) { return `Microsoft.Dynamics.CRM.OlderThanXYears(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function On(name, value) { return `Microsoft.Dynamics.CRM.On(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OnOrAfter(name, value) { return `Microsoft.Dynamics.CRM.OnOrAfter(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function OnOrBefore(name, value) { return `Microsoft.Dynamics.CRM.OnOrBefore(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function ThisFiscalPeriod(name) { return `Microsoft.Dynamics.CRM.ThisFiscalPeriod(PropertyName=${wrapString(name)})`; } function ThisFiscalYear(name) { return `Microsoft.Dynamics.CRM.ThisFiscalYear(PropertyName=${wrapString(name)})`; } function ThisMonth(name) { return `Microsoft.Dynamics.CRM.ThisMonth(PropertyName=${wrapString(name)})`; } function ThisWeek(name) { return `Microsoft.Dynamics.CRM.ThisWeek(PropertyName=${wrapString(name)})`; } function ThisYear(name) { return `Microsoft.Dynamics.CRM.ThisYear(PropertyName=${wrapString(name)})`; } function Today(name) { return `Microsoft.Dynamics.CRM.Today(PropertyName=${wrapString(name)})`; } function Tomorrow(name) { return `Microsoft.Dynamics.CRM.Tomorrow(PropertyName=${wrapString(name)})`; } function Under(name, value) { return `Microsoft.Dynamics.CRM.Under(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function UnderOrEqual(name, value) { return `Microsoft.Dynamics.CRM.UnderOrEqual(PropertyName=${wrapString(name)},PropertyValue=${wrapString( value )})`; } function Yesterday(name) { return `Microsoft.Dynamics.CRM.Yesterday(PropertyName=${wrapString(name)})`; } function query(query2) { const params = new URLSearchParams(); if (query2?.select) params.set("$select", query2.select); if (query2?.expand) params.set("$expand", query2.expand); if (query2?.orderby) params.set("$orderby", query2.orderby); if (query2?.filter) params.set("$filter", query2.filter); if (query2?.top) params.set("$top", query2.top.toFixed(0)); if (query2?.apply) params.set("$apply", query2.apply); return params.toString(); } function keys(keys2) { return Object.entries(keys2).filter((kv) => isNonEmptyString(kv[1])).map(([k, v]) => `${k}=${wrapString(v)}`).join(","); } function select(...values) { return values.filter(isNonEmptyString).join(","); } function expand(values) { if (typeof values === "string") return values; return Object.entries(values).map( ([name, v]) => typeof v === "string" ? v : `${name}(${v.select ? `$select=${select(...Array.isArray(v.select) ? v.select : [v.select])};` : ""}${v.expand ? `$expand=${expand(v.expand)}` : ""})` ).join(","); } function orderby(values) { return Object.entries(values).filter((kv) => isNonEmptyString(kv[1])).map(([k, v]) => `${k} ${v}`).join(","); } class Table extends Schema { properties; kind = "table"; type = "table"; /** * Creates a new Table instance. * * @param entitySetName The entity set name of the Dataverse table. * @param props An object defining the properties of the table. */ constructor(entitySetName, props) { super(entitySetName, null); this.name = entitySetName; this.properties = props; } getIssues(value, path = []) { const issues = super.getIssues(value, path); if (typeof value !== "object") value = {}; for (const [key, property] of Object.entries(this.properties)) { if (!property.getReadOnly()) issues.push(...property.getIssues(value[key], [...path, key])); } return issues; } /** * Gets the default values for the table's properties, optionally merged with provided values. * * @param value Optional object containing values to merge with the defaults. * @returns An object containing the default values for the table. * * @example * // Example 1: Get all default values. * const defaultAccount = myAccountTable.getDefault(); * // Returns an object with all properties set to their defaults. * * // Example 2: Merge provided values with defaults. * const partialAccount = { name: "Initial Name" }; * const mergedAccount = myAccountTable.getDefault(partialAccount); * // Returns an object with defaults, but 'name' is set to "Initial Name". */ getDefault(value) { const result = {}; for (const [key, property] of Object.entries(this.properties)) { if (value === void 0 || !(key in value)) { result[key] = property.getDefault(); } else { result[key] = value[key]; } } return result; } /** * Retrieves a single record from the table by its ID. * * @param keys The unique identifier of the record to retrieve. * @returns A promise that resolves to the retrieved record, or null if not found. * * @example * const account = await myAccountTable.getRecord("12345678-90ab-cdef-1234-567890abcdef"); * if (account) { * console.log(account.name); // Access a property of the record * } */ async getRecord(id) { return getRecord(this.name, id, buildQuery(this)).then( (v) => this.transformValueFromDataverse(v) ); } getAlternateKeys(value) { return Object.entries(value).map((kv) => `${this.properties[kv[0]].name}=${kv[1]}`).join(","); } /** * Retrieves multiple records from the table, optionally with a query. * * @param query An optional object specifying query parameters such as order, filter, and top. * @returns A promise that resolves to an array of retrieved records. * * @example * // Example 1: Get all accounts * const allAccounts = await myAccountTable.getRecords(); * * // Example 2: Get accounts ordered by name, with a limit * const limitedAccounts = await myAccountTable.getRecords({ * orderby: { name: "asc" }, * top: 10, * }); * * // Example 3: Get accounts filtered by a condition * const filteredAccounts = await myAccountTable.getRecords({ * filter: equals("accountnumber", "123") * }); */ async getRecords(query2) { return getRecords(this.name, buildQuery(this, query2)).then( (values) => values.map((v) => this.transformValueFromDataverse(v)) ); } /** * Retrieves the value of a specific property for a record. * * @param key The key of the property to retrieve. * @param id The unique identifier of the record. * @param query Optional query parameters to apply. * @returns A promise that resolves to the property value. * * @example * //Get a single property * const accountName = await myAccountTable.getPropertyValue("name", "12345678-90ab-cdef-1234-567890abcdef"); * * //Get a collection valued property * const contacts = await myAccountTable.getPropertyValue("contact_customer_accounts", "12345678-90ab-cdef-1234-567890abcdef"); */ async getPropertyValue(key, id, query2) { const prop = this.properties[key]; if (prop.kind === "value" || prop.type === "lookupId") { return getPropertyValue(this.name, id, prop.name).then( (v) => prop.transformValueFromDataverse(v) ); } if (prop.type === "collection" || prop.type === "collectionIds") { return getAssociatedRecords( this.name, id, prop.name, buildQuery(this, query2) ).then( (v) => prop.transformValueFromDataverse(v) ); } if (prop.type === "lookup") { return getAssociatedRecord( this.name, id, prop.name, buildQuery(this, query2) ).then( (v) => prop.transformValueFromDataverse(v) ); } throw new Error("Invalid Property"); } /** * Updates the value of a specific property for a record. * * @param key The key of the property to update. * @param id The unique identifier of the record to update. * @param value The new value for the property. * @returns A promise that resolves to the ID of the updated record. * * @example * await myAccountTable.updatePropertyValue("name", "12345678-90ab-cdef-1234-567890abcdef", "New Name"); */ async updatePropertyValue(key, id, value) { const prop = this.properties[key]; if (prop.kind === "navigation") { await this.updateNavigationProperty(prop, id, value); } else { await updatePropertyValue( this.name, id, this.properties[key].name, prop.transformValueToDataverse(value) ); } return id; } /** * Handles updating navigation properties (lookups, expands, collections, lookups). * @param property The navigation property to update * @param id The id of the record being updated. * @param value The new value for the navigation property. */ async updateNavigationProperty(property, id, value) { if (property.type === "collection" || property.type === "collectionIds") { if (Array.isArray(value)) { const ids = property.type === "collection" ? await Promise.all( value.map((v) => property.table.saveRecord(v)) ) : value; return associateRecordToList( this.name, id, property.name, property.table.name, property.table.getPrimaryKey().property.name, ids ); } } if (property.type === "lookup" || property.type == "lookupId") { const name = property.type === "lookup" ? property.name : property.navigationName; if (value === null) { return disssociateRecord(this.name, id, name); } else { const childId = property.type === "lookup" ? await this.saveRecord(value) : value; return associateRecord( this.name, id, name, property.table.name, childId ); } } } /** * Associates a child record with a parent record through a navigation property. * * @param key The key of the navigation property to use for the association. * @param id The unique identifier of the parent record. * @param childId The unique identifier of the child record to associate. * @returns A promise that resolves to the ID of the parent record. * * @example * const accountId = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; * const contactId = "f9e8d7c6-b5a4-3210-fedc-ba9876543210"; * await myAccountTable.associateRecord("primarycontactid", accountId, contactId); */ async associateRecord(key, id, childId) { const prop = this.properties[key]; if (prop.kind === "navigation") { return associateRecord( this.name, id, prop.name, prop.table.name, childId ); } else { throw new Error("Can only associate to naigation properties"); } } async dissociateRecord(key, id, childId) { const prop = this.properties[key]; if (prop.kind === "navigation") { return disssociateRecord( this.name, id, prop.name, prop.type === "collection" || prop.type === "collectionIds" ? childId : void 0 ); } else { throw new Error("Can only associate to naigation properties"); } } /** * Saves a record to the table. Handles both creating new records and updating existing ones. * * @param value An object containing the data to save. The object structure should match the table's properties. * @returns A promise that resolves to the GUID of the saved record. * * @example * // Example 1: Creating a new account * const newAccountId = await myAccountTable.saveRecord({ * name: "New Account Name", * accountnumber: "NewAccount001", * }); * * // Example 2: Updating an existing account * const existingAccountId = "a1b2c3d4-e5f6-7890-1234-567890abcdef"; * const updatedAccountId = await myAccountTable.saveRecord({ * id: existingAccountId, * name: "Updated Account Name", * }); */ async saveRecord(value) { const promises = []; const pk = this.getPrimaryKey().property.name; let id = this.getPrimaryId(value); if (id) { promises.push( patchRecord( this.name, id, this.transformValueToDataverse(value), query({ select: pk }) ) ); } else { const record = await postRecord( this.name, this.transformValueToDataverse(value), query({ select: pk }) ); id = record[pk]; } for (const [key, property] of Object.entries(this.properties)) { if (property.getReadOnly() || !(key in value)) continue; if (property.kind === "navigation") { promises.push(this.updateNavigationProperty(property, id, value[key])); } } await Promise.all(promises); return id; } /** * Deletes a record from the table by its ID. * * @param id The unique identifier of the record to delete. * @returns A promise that resolves to the ID of the deleted record. * * @example * await myAccountTable.deleteRecord("12345678-90ab-cdef-1234-567890abcdef"); */ async deleteRecord(id) { return deleteRecord(this.name, id); } /** * Deletes the value of a specific property for a record. Only works for value properties. * * @param key The key of the property to delete the value of. * @param id The unique identifier of the record. * @returns A promise that resolves to the ID of the record. * * @example * await myAccountTable.deletePropertyValue("accountnumber", "12345678-90ab-cdef-1234-567890abcdef"); */ async deletePropertyValue(key, id) { const prop = this.properties[key]; if (prop.kind === "value") { return deletePropertyValue(this.name, id, prop.name); } throw new Error("Cannot delete navigation property values"); } /** * Gets the primary key property of the table. * * @returns An object containing the key and property definition of the primary key. * @throws Error if no primary key is found. * * @example * const primaryKeyInfo = myAccountTable.getPrimaryKey(); * console.log(primaryKeyInfo.key); // "id" (or whatever the primary key property is named) * console.log(primaryKeyInfo.property); // The PrimaryKeyProperty object */ getPrimaryKey() { const result = Object.entries(this.properties).find( (f) => f[1].type === "primaryKey" ); if (!result) throw new Error("No Primary Key"); return { key: result[0], property: result[1] }; } /** * Gets the primary key value from a record object. * * @param value An object representing a record, typically of type `Partial<Infer<TProperties>>`. * @returns The GUID of the primary key, or undefined if not found in the provided value. * * @example * const accountData = { id: "a1b2c3d4-e5f6-7890-1234-567890abcdef", name: "My Account" }; * const accountId = myAccountTable.getPrimaryId(accountData); // returns "a1b2c3d4-e5f6-7890-1234-567890abcdef" */ getPrimaryId(value) { const { key } = this.getPrimaryKey(); return value[key]; } /** * Transforms a record from Dataverse format to the format expected by the application. * This involves using the `transformValueFromDataverse` method of each property. * * @param value The record data in Dataverse format. * @returns The transformed record data in the application's format. * * @example * // Assuming Dataverse returns: { accountid: "...", name: "Account Name", ... } * const transformedAccount = myAccountTable.transformValueFromDataverse(dataverseAccountData); * // transformedAccount might look like: { id: "...", name: "Account Name", ... } */ transformValueFromDataverse(value) { if (value === null) return null; const result = {}; for (const [key, property] of Object.entries(this.properties)) { result[key] = property.transformValueFromDataverse(value[property.name]); } result[Etag] = value[Etag]; return result; } /** * Transforms a record from the application's format to Dataverse format. * This involves using the `transformValueToDataverse` method of each property. * * @param value The record data in the application's format. * @returns The record data in Dataverse format. * * @example * const appAccountData = { id: "...", name: "Account Name", ... }; * const dataverseAccountData = myAccountTable.transformValueToDataverse(appAccountData); * // dataverseAccountData might look like: { accountid: "...", name: "Account Name", ... } */ transformValueToDataverse(value) { if (value === null) return null; const result = {}; for (const [key, property] of Object.entries(this.properties)) { if (property.getReadOnly() || !(key in value)) continue; if (property.kind === "value") { const v = property.transformValueToDataverse(value[key]); result[property.name] = v; } } return result; } /** * Creates a new Table instance with a subset of the original table's properties. * * @param keys The keys of the properties to include in the new table. * @returns A new Table instance with the specified properties. * * @example * // Create a new table with only 'name' and 'accountnumber' properties. * const nameAndNumberTable = myAccountTable.pickProperties("name", "accountnumber"); */ pickProperties(...keys) { const properties = Object.fromEntries( Object.entries(this.properties).filter((v) => keys.includes(v[0])) ); return new Table(this.name, properties); } /** * Creates a new Table instance with all but the specified properties from the original table. * * @param keys The keys of the properties to exclude from the new table. * @returns A new Table instance with the remaining properties. * * @example * // Create a new table without the 'notes' and 'tasks' properties. * const noNotesAndTasksTable = myAccountTable.omitProperties("notes", "tasks"); */ omitProperties(...keys) { const properties = Object.fromEntries( Object.entries(this.properties).filter((v) => !keys.includes(v[0])) ); return new Table(this.name, properties); } /** * Creates a new Table instance with additional properties added to the original table's properties. * * @param properties An object defining the properties to append. * @returns A new Table instance with the appended properties. * * @example * // Create a new table with an added 'customField' property. * const extendedTable = myAccountTable.appendProperties({ * customField: string("custom_field"), * }); */ appendProperties(properties) { return new Table(this.name, { ...this.properties, ...properties }); } } function table(name, properties) { return new Table(name, properties); } function buildQuery(table2, q) { return query({ top: q?.top, filter: q?.filter, orderby: q?.orderby ? Object.entries(q?.orderby ?? {}).map(([key, value]) => `${table2.properties[key].name} ${value}`).join(",") : void 0, select: