@dataverse-kit/calendar-kit 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -0
- package/dist/{CalendarView-Dd8BHOLE.d.cts → CalendarView-Cx1rnN1I.d.cts} +1 -1
- package/dist/{CalendarView-CShiWFsR.d.ts → CalendarView-DokZ3UaA.d.ts} +1 -1
- package/dist/code-app.cjs +364 -0
- package/dist/code-app.cjs.map +1 -0
- package/dist/code-app.d.cts +53 -0
- package/dist/code-app.d.ts +53 -0
- package/dist/code-app.mjs +337 -0
- package/dist/code-app.mjs.map +1 -0
- package/dist/core.d.cts +2 -2
- package/dist/core.d.ts +2 -2
- package/dist/dataverse.cjs +85 -2
- package/dist/dataverse.cjs.map +1 -1
- package/dist/dataverse.d.cts +21 -114
- package/dist/dataverse.d.ts +21 -114
- package/dist/dataverse.mjs +78 -1
- package/dist/dataverse.mjs.map +1 -1
- package/dist/fake.d.cts +1 -1
- package/dist/fake.d.ts +1 -1
- package/dist/index.cjs +95 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +36 -2
- package/dist/index.d.ts +36 -2
- package/dist/index.mjs +94 -0
- package/dist/index.mjs.map +1 -1
- package/dist/mapping-DRx7vxQl.d.cts +121 -0
- package/dist/mapping-P6GFE8dF.d.ts +121 -0
- package/dist/pcf-dataset.d.cts +2 -2
- package/dist/pcf-dataset.d.ts +2 -2
- package/dist/portal.cjs +371 -0
- package/dist/portal.cjs.map +1 -0
- package/dist/portal.d.cts +40 -0
- package/dist/portal.d.ts +40 -0
- package/dist/portal.mjs +344 -0
- package/dist/portal.mjs.map +1 -0
- package/dist/pull-DXFuI2sU.d.ts +47 -0
- package/dist/pull-DuZIyoqY.d.cts +47 -0
- package/dist/{types-u3Gpxk53.d.cts → types-Bxkg9NIR.d.cts} +1 -1
- package/dist/{types-u3Gpxk53.d.ts → types-Bxkg9NIR.d.ts} +1 -1
- package/dist/v9.d.cts +2 -2
- package/dist/v9.d.ts +2 -2
- package/package.json +11 -1
package/dist/dataverse.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/dataverse/index.ts","../src/core/datetime.ts","../src/dataverse/dates.ts","../src/core/colors.ts","../src/dataverse/mapping.ts"],"sourcesContent":["/**\n * `@dataverse-kit/calendar-kit/dataverse` — Dataverse's date rules and the column mapping shared\n * by every Dataverse adapter (PCF dataset, code app, Power Pages). No React, no FullCalendar.\n */\nexport {\n DateTimeBehavior,\n behaviorOf,\n exclusiveEndToInclusive,\n inclusiveEndToExclusive,\n readDataverseDate,\n toFormParameterDate,\n toWebApiDate,\n} from './dates';\nexport {\n mapRecordToEvent,\n readYesNo,\n toQuickCreateParameters,\n toWebApiPatch,\n} from './mapping';\nexport type { ColumnMap, MapOptions, RecordAccessor } from './mapping';\n","import type { CalendarValue, ValueKind, ZoneOffset } from './types';\n\n/**\n * Value-kind detection and zone conversion.\n *\n * ★★ Nothing in this file may read the PROCESS time zone. Every `Date` is built and read through\n * its UTC fields, so the same input gives the same output on a laptop in Auckland and a CI\n * runner in UTC. `npm test` runs the suite under three TZ values to prove it — a single-zone\n * run cannot see a leak, because the developer's zone is the one the code was written in.\n */\n\nconst DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst FLOATING_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,9})?)?$/;\nconst INSTANT_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?(?:Z|[+-]\\d{2}:?\\d{2})$/i;\n\nconst pad = (n: number, w = 2): string => String(n).padStart(w, '0');\n\n/** The browser's own zone, east-positive. The default when a host supplies none. */\nexport const browserZone: ZoneOffset = (instant) => -instant.getTimezoneOffset();\n\n/** A fixed zone (no daylight saving). Mostly for tests and demos. */\nexport const fixedZone =\n (minutesEast: number): ZoneOffset =>\n () =>\n minutesEast;\n\nexport function kindOf(value: CalendarValue | Date): ValueKind {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) throw new RangeError('Invalid Date');\n return 'instant';\n }\n const local = DATE_RE.test(value) ? 'date' : FLOATING_RE.test(value) ? 'floating' : null;\n if (local) {\n // ★ The regex accepts `2026-02-31`; Date.UTC would silently roll it to March 3rd. Reject\n // any date whose fields do not survive a round trip.\n const m = (FLOATING_RE.exec(value) ?? DATE_RE.exec(value))!;\n const [, y, mo, d, h = '0', mi = '0', sec = '0'] = m;\n const t = new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +sec));\n if (t.getUTCFullYear() !== +y || t.getUTCMonth() !== +mo - 1 || t.getUTCDate() !== +d || +h > 23 || +mi > 59 || +sec > 59) {\n throw new RangeError(`Not a real calendar date or time: ${JSON.stringify(value)}`);\n }\n return local;\n }\n if (INSTANT_RE.test(value) && !Number.isNaN(new Date(value).getTime())) return 'instant';\n throw new RangeError(`Not an ISO-8601 date, wall-clock time or instant: ${JSON.stringify(value)}`);\n}\n\n/** An instant as canonical UTC ISO text (`…T…Z`, milliseconds kept). */\nexport function toInstant(value: CalendarValue | Date): CalendarValue {\n if (value instanceof Date) return value.toISOString();\n if (kindOf(value) !== 'instant') throw new RangeError(`Not an instant: ${value}`);\n return new Date(value).toISOString();\n}\n\n/** `Date.UTC` from wall-clock fields — i.e. the wall clock treated AS IF it were UTC. */\nfunction wallMillis(wall: string): number {\n const m = FLOATING_RE.exec(wall) ?? DATE_RE.exec(wall);\n if (!m) throw new RangeError(`Not a date or wall-clock time: ${wall}`);\n const [, y, mo, d, h = '0', mi = '0', s = '0'] = m;\n return Date.UTC(+y, +mo - 1, +d, +h, +mi, +s);\n}\n\nfunction formatWall(ms: number): string {\n const d = new Date(ms);\n return (\n `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +\n `T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`\n );\n}\n\n/** The wall-clock reading of an instant in `zone`, as floating text (`YYYY-MM-DDTHH:mm:ss`). */\nexport function toWall(instant: CalendarValue | Date, zone: ZoneOffset): CalendarValue {\n const at = instant instanceof Date ? instant : new Date(toInstant(instant));\n return formatWall(at.getTime() + zone(at) * 60_000);\n}\n\n/**\n * The instant at which `zone`'s clocks read `wall`.\n *\n * The offset depends on the instant being solved for, so this takes the offset in force a day\n * BEFORE and a day AFTER the wall reading (no zone changes offset twice in 48 h) and tries both:\n * - an ordinary time: both give the same instant;\n * - a wall time that exists TWICE (fall-back): both round-trip, and the EARLIER is returned;\n * - a wall time that DOES NOT EXIST (spring-forward gap): neither round-trips, and the\n * pre-transition offset is used, which pushes it forward by the gap (02:30 → 03:30), as JS\n * `Date` does. What Dataverse does with one is UNMEASURED.\n *\n * ★ A reviewer caught the first version returning the LATER occurrence east of UTC (Berlin) and\n * the earlier one west of it (Pacific): the two-pass guess settled on whichever offset the\n * first guess happened to land in. Tests now pin both hemispheres with synthetic zones.\n */\nexport function fromWall(wall: CalendarValue, zone: ZoneOffset): CalendarValue {\n const w = wallMillis(wall);\n const before = zone(new Date(w - 86_400_000));\n const after = zone(new Date(w + 86_400_000));\n const fits = (offset: number): number | null => {\n const t = w - offset * 60_000;\n return zone(new Date(t)) === offset ? t : null;\n };\n const candidates = [fits(before), fits(after)].filter((t): t is number => t !== null);\n const t = candidates.length > 0 ? Math.min(...candidates) : w - before * 60_000;\n return new Date(t).toISOString();\n}\n\n/** The calendar day (`YYYY-MM-DD`) of any value, in `zone` for instants. */\nexport function toDate(value: CalendarValue | Date, zone: ZoneOffset): CalendarValue {\n const k = kindOf(value);\n if (k === 'date') return value as string;\n if (k === 'floating') return (value as string).slice(0, 10);\n return toWall(value, zone).slice(0, 10);\n}\n\n/** Calendar-day arithmetic on a `YYYY-MM-DD` value (no zone involved). */\nexport function addDays(date: CalendarValue, days: number): CalendarValue {\n if (kindOf(date) !== 'date') throw new RangeError(`addDays needs a date, got ${date}`);\n return formatWall(wallMillis(date) + days * 86_400_000).slice(0, 10);\n}\n\n/**\n * The UTC fields of a `Date` as floating text. For reading a `Date` that some library has\n * produced in a \"UTC-as-wall-clock\" convention (FullCalendar with `timeZone: 'UTC'`).\n */\nexport function utcFieldsToWall(d: Date): CalendarValue {\n return formatWall(d.getTime());\n}\n\n/** Normalise floating text to seconds precision (`…T09:00` → `…T09:00:00`). */\nexport function normaliseFloating(value: CalendarValue): CalendarValue {\n return formatWall(wallMillis(value));\n}\n","import { addDays, fromWall, kindOf, normaliseFloating, toDate, toInstant, toWall } from '../core/datetime';\nimport type { CalendarValue, ZoneOffset } from '../core/types';\n\n/**\n * Dataverse's three DateTime behaviours, and the FOUR different shapes one value takes on its\n * way through a model-driven app. Every rule below was MEASURED on a live org (2026-09-26,\n * `spikes/calendar-view-fc7/FINDINGS.md` in the parent workspace); none is from documentation.\n *\n * ★★★ The two write paths are DIFFERENT CONTRACTS. The same text means different things to\n * `webAPI.updateRecord` and to `navigation.openForm` parameters:\n *\n * | behaviour | read (dataset getValue) | Web API write | form parameter (quick create) |\n * |------------|-----------------------------|------------------------|-------------------------------|\n * | UserLocal | `…T06:30:00.000Z` instant | instant `…Z` | wall clock, NO `Z` |\n * | DateOnly | `…T00:00:00.000Z` UTC-midn. | `YYYY-MM-DD` | `YYYY-MM-DDT00:00:00`, NO `Z` |\n * | TZI | `…T23:30:00.000Z` face+`Z` | wall clock WITH `Z` | wall clock, NO `Z` |\n *\n * Measured failures of the obvious choice, each of which a unit test here pins:\n * - a DateOnly read through `new Date()` lands on the PREVIOUS day anywhere west of UTC;\n * - a bare `YYYY-MM-DD` form parameter was stored as the previous day (Pacific user);\n * - a TZI form parameter WITH `Z` was treated as UTC and shifted −7 h.\n * The first two are why a well-known client control had a \"+1 day\" hack in its click handler.\n */\nexport const DateTimeBehavior = {\n UserLocal: 1,\n DateOnly: 2,\n TimeZoneIndependent: 3,\n} as const;\nexport type DateTimeBehavior = (typeof DateTimeBehavior)[keyof typeof DateTimeBehavior];\n\n/**\n * The behaviour of a column, from whatever metadata shape the host produced.\n *\n * ★ `getEntityMetadata` in a PCF host answers `Behavior: 1|2|3` (MEASURED). The Web API's\n * `EntityDefinitions` answers `DateTimeBehavior: { Value: 'UserLocal' }`. Both are accepted.\n * ★ A DateOnly column is also recognisable from its PCF `dataType` alone\n * (`DateAndTime.DateOnly`). TZI and UserLocal are NOT — both report `DateAndTime.DateAndTime` —\n * so without metadata a TZI column is indistinguishable and is treated as UserLocal. Callers\n * that can see that ambiguity should surface it (see `adapters/pcf-dataset`).\n */\nexport function behaviorOf(meta: unknown, dataType?: string): DateTimeBehavior {\n const m = (meta ?? {}) as { Behavior?: unknown; DateTimeBehavior?: { Value?: unknown } | string };\n if (m.Behavior === 1 || m.Behavior === 2 || m.Behavior === 3) return m.Behavior;\n const named = typeof m.DateTimeBehavior === 'string' ? m.DateTimeBehavior : m.DateTimeBehavior?.Value;\n if (named === 'DateOnly') return DateTimeBehavior.DateOnly;\n if (named === 'TimeZoneIndependent') return DateTimeBehavior.TimeZoneIndependent;\n if (named === 'UserLocal') return DateTimeBehavior.UserLocal;\n if (dataType === 'DateAndTime.DateOnly') return DateTimeBehavior.DateOnly;\n return DateTimeBehavior.UserLocal;\n}\n\nfunction rawText(raw: unknown): string | null {\n if (raw === null || raw === undefined || raw === '') return null;\n if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? null : raw.toISOString();\n if (typeof raw === 'string') return raw;\n return null;\n}\n\n/**\n * A raw column value (dataset `getValue()`, or a Web API property) → a calendar value.\n *\n * ★★★ `getValue()` returns an ISO STRING for all three behaviours, despite\n * `@types/powerapps-component-framework` declaring `Date`. A `Date` is still accepted, because a\n * test double or another host may produce one — and it is read through its UTC fields, which is\n * what the platform's strings encode.\n */\nexport function readDataverseDate(raw: unknown, behavior: DateTimeBehavior): CalendarValue | null {\n const s = rawText(raw);\n if (s === null) return null;\n switch (behavior) {\n case DateTimeBehavior.DateOnly:\n // `2026-09-29T00:00:00.000Z` (dataset) or `2026-09-29` (Web API): the first ten characters\n // ARE the day. Never through `new Date()`.\n return s.slice(0, 10);\n case DateTimeBehavior.TimeZoneIndependent:\n // Face value with a misleading `Z`: the digits are the wall clock. Strip the offset rather\n // than slicing a fixed width — `…T23:30Z` (no seconds) would keep its `Z` at 19 chars.\n return normaliseFloating(s.replace(/(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$/i, ''));\n default:\n return toInstant(s);\n }\n}\n\n/**\n * A calendar value → the text `webAPI.updateRecord` / `createRecord` wants for this behaviour.\n * `zone` is needed only to turn a value of a different kind into this one (e.g. an all-day drop\n * onto a UserLocal column); same-kind values pass straight through.\n */\nexport function toWebApiDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string {\n const k = kindOf(value);\n switch (behavior) {\n case DateTimeBehavior.DateOnly:\n return toDate(value, zone);\n case DateTimeBehavior.TimeZoneIndependent:\n // MEASURED: `…T13:00:00Z` stored and read back as a 1 PM face value.\n return `${k === 'instant' ? toWall(value, zone) : k === 'date' ? `${value}T00:00:00` : normaliseFloating(value)}Z`;\n default:\n return k === 'instant' ? toInstant(value) : fromWall(k === 'date' ? `${value}T00:00:00` : value, zone);\n }\n}\n\n/**\n * A calendar value → the text a `navigation.openForm` form PARAMETER wants (quick create\n * prefill). Wall clock in the user's zone, never with `Z`.\n *\n * ★ UNMEASURED: whether \"wall clock\" here means the Dynamics USER's zone or the BROWSER's when\n * they differ. Measured only with both in Pacific. Pass the user zone; if the org proof shows\n * the browser's is used, this is the one function to change.\n */\nexport function toFormParameterDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string {\n const k = kindOf(value);\n if (behavior === DateTimeBehavior.DateOnly) return `${toDate(value, zone)}T00:00:00`;\n if (k === 'instant') return toWall(value, zone);\n if (k === 'date') return `${value}T00:00:00`;\n return normaliseFloating(value);\n}\n\n/** Dataverse stores an all-day END inclusive; the calendar's is exclusive. */\nexport const inclusiveEndToExclusive = (lastDay: CalendarValue): CalendarValue => addDays(lastDay, 1);\nexport const exclusiveEndToInclusive = (dayAfter: CalendarValue): CalendarValue => addDays(dayAfter, -1);\n","/**\n * Event colours.\n *\n * ★ Without FullCalendar Premium there are no resource lanes, so \"appointments by location\"\n * is expressed as ONE COLOUR PER GROUP. `groupColor` is deterministic (same key → same colour\n * on every load and every machine), which is what lets a person learn the legend.\n * ★ Every palette entry carries white text at >= 4.5:1 (WCAG AA, normal text) — asserted by a\n * test, not by eye. Host-supplied colours (Dataverse option colours are often pale) get their\n * text colour from `readableTextOn` instead of assuming white; a well-known client control\n * hard-coded white on yellow, which is ~1.4:1.\n */\nexport const GROUP_PALETTE: readonly string[] = [\n '#0f6cbd', // blue\n '#107c10', // green\n '#c50f1f', // red\n '#8764b8', // lavender\n '#ae4b00', // dark orange\n '#03787c', // teal\n '#4f52b2', // indigo\n '#8a5b00', // dark gold\n '#c0077d', // magenta\n '#5c2e91', // purple\n];\n\n/** FNV-1a — tiny, stable across engines, good enough spread for a 10-colour palette. */\nfunction hash(key: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < key.length; i++) {\n h ^= key.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\nexport function groupColor(key: string, palette: readonly string[] = GROUP_PALETTE): string {\n return palette[hash(key) % palette.length];\n}\n\n/** `#rgb` / `#rrggbb` → [r, g, b] 0-255, or null for anything else (named colours, var()). */\nexport function parseHex(color: string): [number, number, number] | null {\n const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());\n if (!m) return null;\n const h = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];\n return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)) as [number, number, number];\n}\n\nfunction luminance([r, g, b]: [number, number, number]): number {\n const lin = (c: number): number => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\n/** WCAG contrast ratio between two hex colours, or null if either cannot be parsed. */\nexport function contrastRatio(a: string, b: string): number | null {\n const pa = parseHex(a);\n const pb = parseHex(b);\n if (!pa || !pb) return null;\n const [hi, lo] = [luminance(pa), luminance(pb)].sort((x, y) => y - x);\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/**\n * Black or white text, whichever reads better on `background`. Unparseable backgrounds\n * (a CSS variable, a named colour) get `undefined`, i.e. the theme's own event text colour.\n */\nexport function readableTextOn(background: string): '#000000' | '#ffffff' | undefined {\n const onWhite = contrastRatio(background, '#ffffff');\n const onBlack = contrastRatio(background, '#000000');\n if (onWhite === null || onBlack === null) return undefined;\n return onWhite >= onBlack ? '#ffffff' : '#000000';\n}\n","import { groupColor, parseHex } from '../core/colors';\nimport { addDays, kindOf, toDate, toInstant, toWall } from '../core/datetime';\nimport type { CalendarEvent, CalendarEventChange, CalendarValue, SlotSelection, ZoneOffset } from '../core/types';\nimport {\n DateTimeBehavior,\n exclusiveEndToInclusive,\n inclusiveEndToExclusive,\n readDataverseDate,\n toFormParameterDate,\n toWebApiDate,\n} from './dates';\n\n/**\n * Which columns hold what. Logical names. Only `start` is required — that is the whole\n * genericity story: the same calendar shows Appointments for one client and Orders for another,\n * because a maker names the columns rather than a developer.\n */\nexport interface ColumnMap {\n start: string;\n end?: string;\n title?: string;\n /** A hex colour column, or a choice column whose option colours `colorOf` resolves. */\n color?: string;\n /** Location / staff / queue. Drives `resourceId` and, failing `color`, a group colour. */\n group?: string;\n /** A Yes/No column that forces all-day. */\n allDay?: string;\n}\n\n/** The minimum a record source must answer — a dataset record, a Web API row, a fixture. */\nexport interface RecordAccessor {\n id: string;\n value(column: string): unknown;\n formatted(column: string): string;\n}\n\nexport interface MapOptions {\n /** Behaviour per DATE column (logical name). Missing ⇒ UserLocal. */\n behaviors: Readonly<Record<string, DateTimeBehavior>>;\n zone: ZoneOffset;\n /** Resolve a colour column's value (e.g. choice option colours). Return undefined to fall back. */\n colorOf?: (column: string, raw: unknown, formatted: string) => string | undefined;\n /** Per-record read-only, e.g. a status that forbids rescheduling. */\n isReadOnly?: (record: RecordAccessor) => boolean;\n}\n\nconst behaviorFor = (opts: Pick<MapOptions, 'behaviors'>, column: string): DateTimeBehavior =>\n opts.behaviors[column] ?? DateTimeBehavior.UserLocal;\n\n/**\n * ★★ A Yes/No column's raw value in a PCF dataset is the STRING `\"0\"` / `\"1\"` — and\n * `Boolean(\"0\")` is `true`. MEASURED on a sibling gallery control, where every `false` row\n * rendered ON. Read the digits, never truthiness.\n */\nexport function readYesNo(raw: unknown): boolean {\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number') return raw === 1;\n if (typeof raw === 'string') return raw === '1' || raw.toLowerCase() === 'true';\n return false;\n}\n\n/**\n * ★★★ MEASURED (appointment, 2026-09-27): an event FLAGGED all-day on date-time columns stores its\n * calendar date at UTC MIDNIGHT — `2026-10-01T00:00:00Z`→`2026-10-02T00:00:00Z` for \"Oct 1\",\n * even though the columns are UserLocal and the user is in Pacific. The platform's own formatted\n * value then reads \"9/30 5:00 PM\"; the form shows Oct 1. Reading it through the user's zone put\n * a one-day appointment on Wed AND Thu. So a flagged all-day value's DAY is its UTC date.\n */\nfunction utcDayOf(value: CalendarValue): CalendarValue {\n return kindOf(value) === 'instant' ? toInstant(value).slice(0, 10) : value.slice(0, 10);\n}\nfunction utcExclusiveDayOf(end: CalendarValue): CalendarValue {\n const text = kindOf(end) === 'instant' ? toInstant(end) : end;\n return text.length > 10 && text.slice(11, 19) !== '00:00:00' ? addDays(text.slice(0, 10), 1) : text.slice(0, 10);\n}\n\n/** A date-time end → the exclusive day after the event, for an all-day rendering. */\nfunction exclusiveDayOf(end: CalendarValue, zone: ZoneOffset): CalendarValue {\n const day = toDate(end, zone);\n const wall = kindOf(end) === 'instant' ? toWall(end, zone) : end;\n // Ending exactly at midnight already means \"up to, not including, this day\".\n return wall.length > 10 && wall.slice(11, 19) === '00:00:00' ? day : addDays(day, 1);\n}\n\n/** One record → one event, or null when it has no usable start. */\nexport function mapRecordToEvent(record: RecordAccessor, map: ColumnMap, opts: MapOptions): CalendarEvent | null {\n const startBehavior = behaviorFor(opts, map.start);\n let start = readDataverseDate(record.value(map.start), startBehavior);\n if (start === null) return null;\n\n const endBehavior = map.end ? behaviorFor(opts, map.end) : startBehavior;\n let end = map.end ? readDataverseDate(record.value(map.end), endBehavior) : null;\n\n const forcedAllDay = map.allDay ? readYesNo(record.value(map.allDay)) : false;\n const allDay = forcedAllDay || kindOf(start) === 'date';\n\n if (allDay) {\n const flagged = forcedAllDay && startBehavior !== DateTimeBehavior.DateOnly;\n start = flagged ? utcDayOf(start) : toDate(start, opts.zone);\n if (end !== null) {\n end =\n endBehavior === DateTimeBehavior.DateOnly\n ? inclusiveEndToExclusive(end)\n : flagged\n ? utcExclusiveDayOf(end)\n : exclusiveDayOf(end, opts.zone);\n }\n if (end !== null && end <= start) end = addDays(start, 1);\n } else if (end !== null && kindOf(end) !== kindOf(start)) {\n // A timed start with a date-only end (mixed behaviours): not renderable as one span.\n end = null;\n } else if (end !== null && end <= start) {\n // A zero or negative duration renders as nothing in a time grid. Use the default length.\n end = null;\n }\n\n const title = (map.title ? record.formatted(map.title) : '') || '(untitled)';\n const groupKey = map.group ? record.formatted(map.group) || undefined : undefined;\n\n let color: string | undefined;\n if (map.color) {\n const raw = record.value(map.color);\n const formatted = record.formatted(map.color);\n color = opts.colorOf?.(map.color, raw, formatted);\n if (!color && typeof raw === 'string' && parseHex(raw)) color = raw;\n }\n if (!color && groupKey) color = groupColor(groupKey);\n\n return {\n id: record.id,\n start,\n end: end ?? undefined,\n title,\n color,\n resourceId: groupKey,\n allDay,\n readOnly: opts.isReadOnly?.(record) ?? false,\n };\n}\n\n/**\n * A move/resize → the column values to PATCH through the Web API.\n * Columns the map does not name are never written.\n */\nexport function toWebApiPatch(\n change: CalendarEventChange,\n map: ColumnMap,\n opts: Pick<MapOptions, 'behaviors' | 'zone'>\n): Record<string, string> {\n const patch: Record<string, string> = {};\n // ★★★ An all-day change on DATE-TIME columns is written at UTC midnight (the platform's own\n // convention for flagged all-day events — see utcDayOf), never at the user's midnight.\n const write = (value: CalendarValue, behavior: DateTimeBehavior): string =>\n change.allDay && behavior !== DateTimeBehavior.DateOnly && kindOf(value) === 'date'\n ? `${value}T00:00:00Z`\n : toWebApiDate(value, behavior, opts.zone);\n const sb = behaviorFor(opts, map.start);\n patch[map.start] = write(change.start, sb);\n if (map.end && change.end !== undefined) {\n const eb = behaviorFor(opts, map.end);\n const end = change.allDay && eb === DateTimeBehavior.DateOnly ? exclusiveEndToInclusive(change.end) : change.end;\n patch[map.end] = write(end, eb);\n }\n return patch;\n}\n\n/**\n * A selected empty range → quick-create form parameters. `extra` is merged underneath, so a\n * host can prefill a lookup (the parent record) without it being overwritten by dates.\n *\n * ★ The all-day column is set to boolean `true` for an all-day selection. The accepted shape of\n * a Yes/No FORM PARAMETER is UNMEASURED; the date shapes are measured.\n */\nexport function toQuickCreateParameters(\n selection: SlotSelection,\n map: ColumnMap,\n opts: Pick<MapOptions, 'behaviors' | 'zone'>,\n extra: Record<string, unknown> = {}\n): Record<string, unknown> {\n const params: Record<string, unknown> = { ...extra };\n const sb = behaviorFor(opts, map.start);\n params[map.start] = toFormParameterDate(selection.start, sb, opts.zone);\n if (map.end) {\n const eb = behaviorFor(opts, map.end);\n const end =\n selection.allDay && eb === DateTimeBehavior.DateOnly ? exclusiveEndToInclusive(selection.end) : selection.end;\n params[map.end] = toFormParameterDate(end, eb, opts.zone);\n }\n if (map.allDay && selection.allDay) params[map.allDay] = true;\n return params;\n}\n"],"mappings":"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","names":["t"]}
|
|
1
|
+
{"version":3,"sources":["../src/dataverse/index.ts","../src/core/datetime.ts","../src/dataverse/dates.ts","../src/core/colors.ts","../src/dataverse/mapping.ts","../src/dataverse/odata.ts","../src/dataverse/pull.ts"],"sourcesContent":["/**\n * `@dataverse-kit/calendar-kit/dataverse` — Dataverse's date rules and the column mapping shared\n * by every Dataverse adapter (PCF dataset, code app, Power Pages). No React, no FullCalendar.\n */\nexport {\n DateTimeBehavior,\n behaviorOf,\n exclusiveEndToInclusive,\n inclusiveEndToExclusive,\n readDataverseDate,\n toFormParameterDate,\n toWebApiDate,\n} from './dates';\nexport {\n mapRecordToEvent,\n readYesNo,\n toQuickCreateParameters,\n toWebApiPatch,\n} from './mapping';\nexport type { ColumnMap, MapOptions, RecordAccessor } from './mapping';\nexport { FORMATTED_VALUES_PREFER, andFilters, buildODataRangeFilter, selectFor, webApiRowAccessor } from './odata';\nexport { createPullSource } from './pull';\nexport type { PullQuery, PullSource, PullSourceConfig, Row } from './pull';\n","import type { CalendarValue, ValueKind, ZoneOffset } from './types';\n\n/**\n * Value-kind detection and zone conversion.\n *\n * ★★ Nothing in this file may read the PROCESS time zone. Every `Date` is built and read through\n * its UTC fields, so the same input gives the same output on a laptop in Auckland and a CI\n * runner in UTC. `npm test` runs the suite under three TZ values to prove it — a single-zone\n * run cannot see a leak, because the developer's zone is the one the code was written in.\n */\n\nconst DATE_RE = /^(\\d{4})-(\\d{2})-(\\d{2})$/;\nconst FLOATING_RE = /^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2})(?::(\\d{2})(?:\\.\\d{1,9})?)?$/;\nconst INSTANT_RE = /^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}(?::\\d{2}(?:\\.\\d{1,9})?)?(?:Z|[+-]\\d{2}:?\\d{2})$/i;\n\nconst pad = (n: number, w = 2): string => String(n).padStart(w, '0');\n\n/** The browser's own zone, east-positive. The default when a host supplies none. */\nexport const browserZone: ZoneOffset = (instant) => -instant.getTimezoneOffset();\n\n/** A fixed zone (no daylight saving). Mostly for tests and demos. */\nexport const fixedZone =\n (minutesEast: number): ZoneOffset =>\n () =>\n minutesEast;\n\nexport function kindOf(value: CalendarValue | Date): ValueKind {\n if (value instanceof Date) {\n if (Number.isNaN(value.getTime())) throw new RangeError('Invalid Date');\n return 'instant';\n }\n const local = DATE_RE.test(value) ? 'date' : FLOATING_RE.test(value) ? 'floating' : null;\n if (local) {\n // ★ The regex accepts `2026-02-31`; Date.UTC would silently roll it to March 3rd. Reject\n // any date whose fields do not survive a round trip.\n const m = (FLOATING_RE.exec(value) ?? DATE_RE.exec(value))!;\n const [, y, mo, d, h = '0', mi = '0', sec = '0'] = m;\n const t = new Date(Date.UTC(+y, +mo - 1, +d, +h, +mi, +sec));\n if (t.getUTCFullYear() !== +y || t.getUTCMonth() !== +mo - 1 || t.getUTCDate() !== +d || +h > 23 || +mi > 59 || +sec > 59) {\n throw new RangeError(`Not a real calendar date or time: ${JSON.stringify(value)}`);\n }\n return local;\n }\n if (INSTANT_RE.test(value) && !Number.isNaN(new Date(value).getTime())) return 'instant';\n throw new RangeError(`Not an ISO-8601 date, wall-clock time or instant: ${JSON.stringify(value)}`);\n}\n\n/** An instant as canonical UTC ISO text (`…T…Z`, milliseconds kept). */\nexport function toInstant(value: CalendarValue | Date): CalendarValue {\n if (value instanceof Date) return value.toISOString();\n if (kindOf(value) !== 'instant') throw new RangeError(`Not an instant: ${value}`);\n return new Date(value).toISOString();\n}\n\n/** `Date.UTC` from wall-clock fields — i.e. the wall clock treated AS IF it were UTC. */\nfunction wallMillis(wall: string): number {\n const m = FLOATING_RE.exec(wall) ?? DATE_RE.exec(wall);\n if (!m) throw new RangeError(`Not a date or wall-clock time: ${wall}`);\n const [, y, mo, d, h = '0', mi = '0', s = '0'] = m;\n return Date.UTC(+y, +mo - 1, +d, +h, +mi, +s);\n}\n\nfunction formatWall(ms: number): string {\n const d = new Date(ms);\n return (\n `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())}` +\n `T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}`\n );\n}\n\n/** The wall-clock reading of an instant in `zone`, as floating text (`YYYY-MM-DDTHH:mm:ss`). */\nexport function toWall(instant: CalendarValue | Date, zone: ZoneOffset): CalendarValue {\n const at = instant instanceof Date ? instant : new Date(toInstant(instant));\n return formatWall(at.getTime() + zone(at) * 60_000);\n}\n\n/**\n * The instant at which `zone`'s clocks read `wall`.\n *\n * The offset depends on the instant being solved for, so this takes the offset in force a day\n * BEFORE and a day AFTER the wall reading (no zone changes offset twice in 48 h) and tries both:\n * - an ordinary time: both give the same instant;\n * - a wall time that exists TWICE (fall-back): both round-trip, and the EARLIER is returned;\n * - a wall time that DOES NOT EXIST (spring-forward gap): neither round-trips, and the\n * pre-transition offset is used, which pushes it forward by the gap (02:30 → 03:30), as JS\n * `Date` does. What Dataverse does with one is UNMEASURED.\n *\n * ★ A reviewer caught the first version returning the LATER occurrence east of UTC (Berlin) and\n * the earlier one west of it (Pacific): the two-pass guess settled on whichever offset the\n * first guess happened to land in. Tests now pin both hemispheres with synthetic zones.\n */\nexport function fromWall(wall: CalendarValue, zone: ZoneOffset): CalendarValue {\n const w = wallMillis(wall);\n const before = zone(new Date(w - 86_400_000));\n const after = zone(new Date(w + 86_400_000));\n const fits = (offset: number): number | null => {\n const t = w - offset * 60_000;\n return zone(new Date(t)) === offset ? t : null;\n };\n const candidates = [fits(before), fits(after)].filter((t): t is number => t !== null);\n const t = candidates.length > 0 ? Math.min(...candidates) : w - before * 60_000;\n return new Date(t).toISOString();\n}\n\n/** The calendar day (`YYYY-MM-DD`) of any value, in `zone` for instants. */\nexport function toDate(value: CalendarValue | Date, zone: ZoneOffset): CalendarValue {\n const k = kindOf(value);\n if (k === 'date') return value as string;\n if (k === 'floating') return (value as string).slice(0, 10);\n return toWall(value, zone).slice(0, 10);\n}\n\n/** Calendar-day arithmetic on a `YYYY-MM-DD` value (no zone involved). */\nexport function addDays(date: CalendarValue, days: number): CalendarValue {\n if (kindOf(date) !== 'date') throw new RangeError(`addDays needs a date, got ${date}`);\n return formatWall(wallMillis(date) + days * 86_400_000).slice(0, 10);\n}\n\n/**\n * The UTC fields of a `Date` as floating text. For reading a `Date` that some library has\n * produced in a \"UTC-as-wall-clock\" convention (FullCalendar with `timeZone: 'UTC'`).\n */\nexport function utcFieldsToWall(d: Date): CalendarValue {\n return formatWall(d.getTime());\n}\n\n/** Normalise floating text to seconds precision (`…T09:00` → `…T09:00:00`). */\nexport function normaliseFloating(value: CalendarValue): CalendarValue {\n return formatWall(wallMillis(value));\n}\n","import { addDays, fromWall, kindOf, normaliseFloating, toDate, toInstant, toWall } from '../core/datetime';\nimport type { CalendarValue, ZoneOffset } from '../core/types';\n\n/**\n * Dataverse's three DateTime behaviours, and the FOUR different shapes one value takes on its\n * way through a model-driven app. Every rule below was MEASURED on a live org (2026-09-26,\n * `spikes/calendar-view-fc7/FINDINGS.md` in the parent workspace); none is from documentation.\n *\n * ★★★ The two write paths are DIFFERENT CONTRACTS. The same text means different things to\n * `webAPI.updateRecord` and to `navigation.openForm` parameters:\n *\n * | behaviour | read (dataset getValue) | Web API write | form parameter (quick create) |\n * |------------|-----------------------------|------------------------|-------------------------------|\n * | UserLocal | `…T06:30:00.000Z` instant | instant `…Z` | wall clock, NO `Z` |\n * | DateOnly | `…T00:00:00.000Z` UTC-midn. | `YYYY-MM-DD` | `YYYY-MM-DDT00:00:00`, NO `Z` |\n * | TZI | `…T23:30:00.000Z` face+`Z` | wall clock WITH `Z` | wall clock, NO `Z` |\n *\n * Measured failures of the obvious choice, each of which a unit test here pins:\n * - a DateOnly read through `new Date()` lands on the PREVIOUS day anywhere west of UTC;\n * - a bare `YYYY-MM-DD` form parameter was stored as the previous day (Pacific user);\n * - a TZI form parameter WITH `Z` was treated as UTC and shifted −7 h.\n * The first two are why a well-known client control had a \"+1 day\" hack in its click handler.\n */\nexport const DateTimeBehavior = {\n UserLocal: 1,\n DateOnly: 2,\n TimeZoneIndependent: 3,\n} as const;\nexport type DateTimeBehavior = (typeof DateTimeBehavior)[keyof typeof DateTimeBehavior];\n\n/**\n * The behaviour of a column, from whatever metadata shape the host produced.\n *\n * ★ `getEntityMetadata` in a PCF host answers `Behavior: 1|2|3` (MEASURED). The Web API's\n * `EntityDefinitions` answers `DateTimeBehavior: { Value: 'UserLocal' }`. Both are accepted.\n * ★ A DateOnly column is also recognisable from its PCF `dataType` alone\n * (`DateAndTime.DateOnly`). TZI and UserLocal are NOT — both report `DateAndTime.DateAndTime` —\n * so without metadata a TZI column is indistinguishable and is treated as UserLocal. Callers\n * that can see that ambiguity should surface it (see `adapters/pcf-dataset`).\n */\nexport function behaviorOf(meta: unknown, dataType?: string): DateTimeBehavior {\n const m = (meta ?? {}) as { Behavior?: unknown; DateTimeBehavior?: { Value?: unknown } | string };\n if (m.Behavior === 1 || m.Behavior === 2 || m.Behavior === 3) return m.Behavior;\n const named = typeof m.DateTimeBehavior === 'string' ? m.DateTimeBehavior : m.DateTimeBehavior?.Value;\n if (named === 'DateOnly') return DateTimeBehavior.DateOnly;\n if (named === 'TimeZoneIndependent') return DateTimeBehavior.TimeZoneIndependent;\n if (named === 'UserLocal') return DateTimeBehavior.UserLocal;\n if (dataType === 'DateAndTime.DateOnly') return DateTimeBehavior.DateOnly;\n return DateTimeBehavior.UserLocal;\n}\n\nfunction rawText(raw: unknown): string | null {\n if (raw === null || raw === undefined || raw === '') return null;\n if (raw instanceof Date) return Number.isNaN(raw.getTime()) ? null : raw.toISOString();\n if (typeof raw === 'string') return raw;\n return null;\n}\n\n/**\n * A raw column value (dataset `getValue()`, or a Web API property) → a calendar value.\n *\n * ★★★ `getValue()` returns an ISO STRING for all three behaviours, despite\n * `@types/powerapps-component-framework` declaring `Date`. A `Date` is still accepted, because a\n * test double or another host may produce one — and it is read through its UTC fields, which is\n * what the platform's strings encode.\n */\nexport function readDataverseDate(raw: unknown, behavior: DateTimeBehavior): CalendarValue | null {\n const s = rawText(raw);\n if (s === null) return null;\n switch (behavior) {\n case DateTimeBehavior.DateOnly:\n // `2026-09-29T00:00:00.000Z` (dataset) or `2026-09-29` (Web API): the first ten characters\n // ARE the day. Never through `new Date()`.\n return s.slice(0, 10);\n case DateTimeBehavior.TimeZoneIndependent:\n // Face value with a misleading `Z`: the digits are the wall clock. Strip the offset rather\n // than slicing a fixed width — `…T23:30Z` (no seconds) would keep its `Z` at 19 chars.\n return normaliseFloating(s.replace(/(\\.\\d+)?(Z|[+-]\\d{2}:?\\d{2})$/i, ''));\n default:\n return toInstant(s);\n }\n}\n\n/**\n * A calendar value → the text `webAPI.updateRecord` / `createRecord` wants for this behaviour.\n * `zone` is needed only to turn a value of a different kind into this one (e.g. an all-day drop\n * onto a UserLocal column); same-kind values pass straight through.\n */\nexport function toWebApiDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string {\n const k = kindOf(value);\n switch (behavior) {\n case DateTimeBehavior.DateOnly:\n return toDate(value, zone);\n case DateTimeBehavior.TimeZoneIndependent:\n // MEASURED: `…T13:00:00Z` stored and read back as a 1 PM face value.\n return `${k === 'instant' ? toWall(value, zone) : k === 'date' ? `${value}T00:00:00` : normaliseFloating(value)}Z`;\n default:\n return k === 'instant' ? toInstant(value) : fromWall(k === 'date' ? `${value}T00:00:00` : value, zone);\n }\n}\n\n/**\n * A calendar value → the text a `navigation.openForm` form PARAMETER wants (quick create\n * prefill). Wall clock in the user's zone, never with `Z`.\n *\n * ★ UNMEASURED: whether \"wall clock\" here means the Dynamics USER's zone or the BROWSER's when\n * they differ. Measured only with both in Pacific. Pass the user zone; if the org proof shows\n * the browser's is used, this is the one function to change.\n */\nexport function toFormParameterDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string {\n const k = kindOf(value);\n if (behavior === DateTimeBehavior.DateOnly) return `${toDate(value, zone)}T00:00:00`;\n if (k === 'instant') return toWall(value, zone);\n if (k === 'date') return `${value}T00:00:00`;\n return normaliseFloating(value);\n}\n\n/** Dataverse stores an all-day END inclusive; the calendar's is exclusive. */\nexport const inclusiveEndToExclusive = (lastDay: CalendarValue): CalendarValue => addDays(lastDay, 1);\nexport const exclusiveEndToInclusive = (dayAfter: CalendarValue): CalendarValue => addDays(dayAfter, -1);\n","/**\n * Event colours.\n *\n * ★ Without FullCalendar Premium there are no resource lanes, so \"appointments by location\"\n * is expressed as ONE COLOUR PER GROUP. `groupColor` is deterministic (same key → same colour\n * on every load and every machine), which is what lets a person learn the legend.\n * ★ Every palette entry carries white text at >= 4.5:1 (WCAG AA, normal text) — asserted by a\n * test, not by eye. Host-supplied colours (Dataverse option colours are often pale) get their\n * text colour from `readableTextOn` instead of assuming white; a well-known client control\n * hard-coded white on yellow, which is ~1.4:1.\n */\nexport const GROUP_PALETTE: readonly string[] = [\n '#0f6cbd', // blue\n '#107c10', // green\n '#c50f1f', // red\n '#8764b8', // lavender\n '#ae4b00', // dark orange\n '#03787c', // teal\n '#4f52b2', // indigo\n '#8a5b00', // dark gold\n '#c0077d', // magenta\n '#5c2e91', // purple\n];\n\n/** FNV-1a — tiny, stable across engines, good enough spread for a 10-colour palette. */\nfunction hash(key: string): number {\n let h = 0x811c9dc5;\n for (let i = 0; i < key.length; i++) {\n h ^= key.charCodeAt(i);\n h = Math.imul(h, 0x01000193);\n }\n return h >>> 0;\n}\n\nexport function groupColor(key: string, palette: readonly string[] = GROUP_PALETTE): string {\n return palette[hash(key) % palette.length];\n}\n\n/** `#rgb` / `#rrggbb` → [r, g, b] 0-255, or null for anything else (named colours, var()). */\nexport function parseHex(color: string): [number, number, number] | null {\n const m = /^#?([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(color.trim());\n if (!m) return null;\n const h = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];\n return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16)) as [number, number, number];\n}\n\nfunction luminance([r, g, b]: [number, number, number]): number {\n const lin = (c: number): number => {\n const s = c / 255;\n return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b);\n}\n\n/** WCAG contrast ratio between two hex colours, or null if either cannot be parsed. */\nexport function contrastRatio(a: string, b: string): number | null {\n const pa = parseHex(a);\n const pb = parseHex(b);\n if (!pa || !pb) return null;\n const [hi, lo] = [luminance(pa), luminance(pb)].sort((x, y) => y - x);\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/**\n * Black or white text, whichever reads better on `background`. Unparseable backgrounds\n * (a CSS variable, a named colour) get `undefined`, i.e. the theme's own event text colour.\n */\nexport function readableTextOn(background: string): '#000000' | '#ffffff' | undefined {\n const onWhite = contrastRatio(background, '#ffffff');\n const onBlack = contrastRatio(background, '#000000');\n if (onWhite === null || onBlack === null) return undefined;\n return onWhite >= onBlack ? '#ffffff' : '#000000';\n}\n","import { groupColor, parseHex } from '../core/colors';\nimport { addDays, kindOf, toDate, toInstant, toWall } from '../core/datetime';\nimport type { CalendarEvent, CalendarEventChange, CalendarValue, SlotSelection, ZoneOffset } from '../core/types';\nimport {\n DateTimeBehavior,\n exclusiveEndToInclusive,\n inclusiveEndToExclusive,\n readDataverseDate,\n toFormParameterDate,\n toWebApiDate,\n} from './dates';\n\n/**\n * Which columns hold what. Logical names. Only `start` is required — that is the whole\n * genericity story: the same calendar shows Appointments for one client and Orders for another,\n * because a maker names the columns rather than a developer.\n */\nexport interface ColumnMap {\n start: string;\n end?: string;\n title?: string;\n /** A hex colour column, or a choice column whose option colours `colorOf` resolves. */\n color?: string;\n /** Location / staff / queue. Drives `resourceId` and, failing `color`, a group colour. */\n group?: string;\n /** A Yes/No column that forces all-day. */\n allDay?: string;\n}\n\n/** The minimum a record source must answer — a dataset record, a Web API row, a fixture. */\nexport interface RecordAccessor {\n id: string;\n value(column: string): unknown;\n formatted(column: string): string;\n}\n\nexport interface MapOptions {\n /** Behaviour per DATE column (logical name). Missing ⇒ UserLocal. */\n behaviors: Readonly<Record<string, DateTimeBehavior>>;\n zone: ZoneOffset;\n /** Resolve a colour column's value (e.g. choice option colours). Return undefined to fall back. */\n colorOf?: (column: string, raw: unknown, formatted: string) => string | undefined;\n /** Per-record read-only, e.g. a status that forbids rescheduling. */\n isReadOnly?: (record: RecordAccessor) => boolean;\n}\n\nconst behaviorFor = (opts: Pick<MapOptions, 'behaviors'>, column: string): DateTimeBehavior =>\n opts.behaviors[column] ?? DateTimeBehavior.UserLocal;\n\n/**\n * ★★ A Yes/No column's raw value in a PCF dataset is the STRING `\"0\"` / `\"1\"` — and\n * `Boolean(\"0\")` is `true`. MEASURED on a sibling gallery control, where every `false` row\n * rendered ON. Read the digits, never truthiness.\n */\nexport function readYesNo(raw: unknown): boolean {\n if (typeof raw === 'boolean') return raw;\n if (typeof raw === 'number') return raw === 1;\n if (typeof raw === 'string') return raw === '1' || raw.toLowerCase() === 'true';\n return false;\n}\n\n/**\n * ★★★ MEASURED (appointment, 2026-09-27): an event FLAGGED all-day on date-time columns stores its\n * calendar date at UTC MIDNIGHT — `2026-10-01T00:00:00Z`→`2026-10-02T00:00:00Z` for \"Oct 1\",\n * even though the columns are UserLocal and the user is in Pacific. The platform's own formatted\n * value then reads \"9/30 5:00 PM\"; the form shows Oct 1. Reading it through the user's zone put\n * a one-day appointment on Wed AND Thu. So a flagged all-day value's DAY is its UTC date.\n */\nfunction utcDayOf(value: CalendarValue): CalendarValue {\n return kindOf(value) === 'instant' ? toInstant(value).slice(0, 10) : value.slice(0, 10);\n}\nfunction utcExclusiveDayOf(end: CalendarValue): CalendarValue {\n const text = kindOf(end) === 'instant' ? toInstant(end) : end;\n return text.length > 10 && text.slice(11, 19) !== '00:00:00' ? addDays(text.slice(0, 10), 1) : text.slice(0, 10);\n}\n\n/** A date-time end → the exclusive day after the event, for an all-day rendering. */\nfunction exclusiveDayOf(end: CalendarValue, zone: ZoneOffset): CalendarValue {\n const day = toDate(end, zone);\n const wall = kindOf(end) === 'instant' ? toWall(end, zone) : end;\n // Ending exactly at midnight already means \"up to, not including, this day\".\n return wall.length > 10 && wall.slice(11, 19) === '00:00:00' ? day : addDays(day, 1);\n}\n\n/** One record → one event, or null when it has no usable start. */\nexport function mapRecordToEvent(record: RecordAccessor, map: ColumnMap, opts: MapOptions): CalendarEvent | null {\n const startBehavior = behaviorFor(opts, map.start);\n let start = readDataverseDate(record.value(map.start), startBehavior);\n if (start === null) return null;\n\n const endBehavior = map.end ? behaviorFor(opts, map.end) : startBehavior;\n let end = map.end ? readDataverseDate(record.value(map.end), endBehavior) : null;\n\n const forcedAllDay = map.allDay ? readYesNo(record.value(map.allDay)) : false;\n const allDay = forcedAllDay || kindOf(start) === 'date';\n\n if (allDay) {\n const flagged = forcedAllDay && startBehavior !== DateTimeBehavior.DateOnly;\n start = flagged ? utcDayOf(start) : toDate(start, opts.zone);\n if (end !== null) {\n end =\n endBehavior === DateTimeBehavior.DateOnly\n ? inclusiveEndToExclusive(end)\n : flagged\n ? utcExclusiveDayOf(end)\n : exclusiveDayOf(end, opts.zone);\n }\n if (end !== null && end <= start) end = addDays(start, 1);\n } else if (end !== null && kindOf(end) !== kindOf(start)) {\n // A timed start with a date-only end (mixed behaviours): not renderable as one span.\n end = null;\n } else if (end !== null && end <= start) {\n // A zero or negative duration renders as nothing in a time grid. Use the default length.\n end = null;\n }\n\n const title = (map.title ? record.formatted(map.title) : '') || '(untitled)';\n const groupKey = map.group ? record.formatted(map.group) || undefined : undefined;\n\n let color: string | undefined;\n if (map.color) {\n const raw = record.value(map.color);\n const formatted = record.formatted(map.color);\n color = opts.colorOf?.(map.color, raw, formatted);\n if (!color && typeof raw === 'string' && parseHex(raw)) color = raw;\n }\n if (!color && groupKey) color = groupColor(groupKey);\n\n return {\n id: record.id,\n start,\n end: end ?? undefined,\n title,\n color,\n resourceId: groupKey,\n allDay,\n readOnly: opts.isReadOnly?.(record) ?? false,\n };\n}\n\n/**\n * A move/resize → the column values to PATCH through the Web API.\n * Columns the map does not name are never written.\n */\nexport function toWebApiPatch(\n change: CalendarEventChange,\n map: ColumnMap,\n opts: Pick<MapOptions, 'behaviors' | 'zone'>\n): Record<string, string> {\n const patch: Record<string, string> = {};\n // ★★★ An all-day change on DATE-TIME columns is written at UTC midnight (the platform's own\n // convention for flagged all-day events — see utcDayOf), never at the user's midnight.\n const write = (value: CalendarValue, behavior: DateTimeBehavior): string =>\n change.allDay && behavior !== DateTimeBehavior.DateOnly && kindOf(value) === 'date'\n ? `${value}T00:00:00Z`\n : toWebApiDate(value, behavior, opts.zone);\n const sb = behaviorFor(opts, map.start);\n patch[map.start] = write(change.start, sb);\n if (map.end && change.end !== undefined) {\n const eb = behaviorFor(opts, map.end);\n const end = change.allDay && eb === DateTimeBehavior.DateOnly ? exclusiveEndToInclusive(change.end) : change.end;\n patch[map.end] = write(end, eb);\n }\n return patch;\n}\n\n/**\n * A selected empty range → quick-create form parameters. `extra` is merged underneath, so a\n * host can prefill a lookup (the parent record) without it being overwritten by dates.\n *\n * ★ The all-day column is set to boolean `true` for an all-day selection. The accepted shape of\n * a Yes/No FORM PARAMETER is UNMEASURED; the date shapes are measured.\n */\nexport function toQuickCreateParameters(\n selection: SlotSelection,\n map: ColumnMap,\n opts: Pick<MapOptions, 'behaviors' | 'zone'>,\n extra: Record<string, unknown> = {}\n): Record<string, unknown> {\n const params: Record<string, unknown> = { ...extra };\n const sb = behaviorFor(opts, map.start);\n params[map.start] = toFormParameterDate(selection.start, sb, opts.zone);\n if (map.end) {\n const eb = behaviorFor(opts, map.end);\n const end =\n selection.allDay && eb === DateTimeBehavior.DateOnly ? exclusiveEndToInclusive(selection.end) : selection.end;\n params[map.end] = toFormParameterDate(end, eb, opts.zone);\n }\n if (map.allDay && selection.allDay) params[map.allDay] = true;\n return params;\n}\n","import { toWall } from '../core/datetime';\nimport type { VisibleRange, ZoneOffset } from '../core/types';\nimport { DateTimeBehavior } from './dates';\nimport type { ColumnMap, RecordAccessor } from './mapping';\n\n/**\n * OData helpers for sources that are QUERIED rather than pushed — a code app's generated service\n * and Power Pages `/_api` both pass an OData `$filter` / `$select` through to Dataverse.\n *\n * ★ Column names here are Web API PROPERTY names: a lookup is `_parentcustomerid_value`, not\n * `parentcustomerid`. `$select` on a lookup by its logical name is silently dropped by the\n * Web API (measured on a sibling gallery control), so the caller names the property.\n */\n\nconst FORMATTED = '@OData.Community.Display.V1.FormattedValue';\n\n/** A bound value as an OData literal, for a column of `behavior`. */\nfunction literal(range: VisibleRange, edge: 'start' | 'end', behavior: DateTimeBehavior, zone: ZoneOffset): string {\n if (behavior === DateTimeBehavior.DateOnly) return edge === 'start' ? range.startDate : range.endDate;\n // ★ UNMEASURED as a FILTER: a TZI column compared by its face value, sent the way a TZI WRITE is\n // sent (wall clock + Z — measured correct for writes). Verify on an org before relying on edges.\n if (behavior === DateTimeBehavior.TimeZoneIndependent) return `${toWall(range[edge], zone)}Z`;\n return range[edge];\n}\n\n/**\n * The `$filter` for \"events overlapping the visible range\" — the same overlap rule the PCF\n * adapter uses: `start < rangeEnd AND (end >= rangeStart OR start >= rangeStart)`, so a\n * multi-day event that began before the first visible day is kept.\n */\nexport function buildODataRangeFilter(\n range: VisibleRange,\n map: ColumnMap,\n behaviors: Readonly<Record<string, DateTimeBehavior>>,\n zone: ZoneOffset\n): string {\n const sb = behaviors[map.start] ?? DateTimeBehavior.UserLocal;\n const startsBeforeEnd = `${map.start} lt ${literal(range, 'end', sb, zone)}`;\n const startsInRange = `${map.start} ge ${literal(range, 'start', sb, zone)}`;\n if (!map.end) return `${startsBeforeEnd} and ${startsInRange}`;\n const eb = behaviors[map.end] ?? DateTimeBehavior.UserLocal;\n return `${startsBeforeEnd} and (${map.end} ge ${literal(range, 'start', eb, zone)} or ${startsInRange})`;\n}\n\n/** AND a caller's own filter (e.g. \"only open slots\") with the range filter. */\nexport function andFilters(...parts: (string | undefined)[]): string {\n const present = parts.filter((p): p is string => !!p && p.trim() !== '');\n return present.length <= 1 ? present[0] ?? '' : present.map((p) => `(${p})`).join(' and ');\n}\n\n/** Every Web API property the map needs, plus the id, de-duplicated. */\nexport function selectFor(map: ColumnMap, idColumn: string): string[] {\n return [...new Set([idColumn, ...Object.values(map).filter((c): c is string => !!c)])];\n}\n\n/**\n * A Web API row (Dataverse, a code app's connector, or Power Pages `/_api`) as the accessor the\n * mapping reads.\n *\n * ★ Formatted values come from the `@OData.Community.Display.V1.FormattedValue` annotation, which\n * the request must ask for (`Prefer: odata.include-annotations=…`). Without it a choice or a\n * lookup reads as its raw value / GUID — never silently as empty.\n */\nexport function webApiRowAccessor(row: Readonly<Record<string, unknown>>, id: string): RecordAccessor {\n return {\n id,\n value: (c) => row[c] ?? null,\n formatted: (c) => {\n const f = row[`${c}${FORMATTED}`];\n if (typeof f === 'string') return f;\n const raw = row[c];\n return raw === null || raw === undefined ? '' : String(raw);\n },\n };\n}\n\n/** The request header that makes the Web API return formatted values. */\nexport const FORMATTED_VALUES_PREFER = 'odata.include-annotations=\"OData.Community.Display.V1.FormattedValue\"';\n","import { browserZone } from '../core/datetime';\nimport type { CalendarDataSource, CalendarEvent, CalendarEventChange, SlotSelection, VisibleRange, ZoneOffset } from '../core/types';\nimport { DateTimeBehavior } from './dates';\nimport { mapRecordToEvent, toWebApiPatch, type ColumnMap } from './mapping';\nimport { andFilters, buildODataRangeFilter, selectFor, webApiRowAccessor } from './odata';\n\n/**\n * A `CalendarDataSource` over any backend that answers an OData query with Web API–shaped rows.\n * The code-app and Power Pages adapters are thin wrappers that supply `query`.\n *\n * ★★ DATE BEHAVIOURS ARE CONFIGURATION HERE, NOT METADATA. Neither host exposes them: a code app's\n * `getMetadata` returns base `AttributeMetadata` only, and `DateTimeBehavior` lives on the\n * derived `DateTimeAttributeMetadata` (documented: derived-type properties are unavailable);\n * Power Pages has no metadata endpoint at all. A date-time column NOT listed is treated as\n * UserLocal — and reported in `guessed`, because a TimeZoneIndependent column guessed that way\n * renders shifted.\n */\nexport interface PullQuery {\n select: string[];\n filter: string;\n orderBy: string;\n}\n\nexport type Row = Readonly<Record<string, unknown>>;\n\nexport interface PullSourceConfig {\n /** Run the query; return every row (paging is the adapter's job). Honour `signal`. */\n query: (q: PullQuery, signal?: AbortSignal) => Promise<readonly Row[]>;\n /** The primary-key property, e.g. `appointmentid` or `dvk_slotid`. */\n idColumn: string;\n /** Web API property names (a lookup is `_x_value`). */\n map: ColumnMap;\n /** Behaviour per date column. Unlisted ⇒ UserLocal (and reported in `guessed`). */\n behaviors?: Readonly<Record<string, DateTimeBehavior>>;\n /** The zone to interpret instants in. Default: the browser's. */\n zone?: ZoneOffset;\n /** An extra OData filter ANDed with the range, e.g. `statecode eq 0`. */\n filter?: string;\n /** Persist a move/resize. Omit ⇒ read-only. */\n update?: (id: string, patch: Record<string, string>) => Promise<void>;\n open?: (event: CalendarEvent) => void;\n create?: (slot: SlotSelection) => Promise<void> | void;\n /** Receives rows whose dates could not be read (skipped, never thrown). */\n onInvalid?: (id: string, error: unknown) => void;\n}\n\nexport interface PullSource extends CalendarDataSource {\n /** Date columns treated as UserLocal because no behaviour was configured for them. */\n readonly guessed: readonly string[];\n}\n\nexport function createPullSource(config: PullSourceConfig): PullSource {\n const zone = config.zone ?? browserZone;\n const behaviors = config.behaviors ?? {};\n const dateColumns = [config.map.start, config.map.end].filter((c): c is string => !!c);\n const guessed = dateColumns.filter((c) => behaviors[c] === undefined);\n const select = selectFor(config.map, config.idColumn);\n\n return {\n guessed,\n capabilities: { update: !!config.update, create: !!config.create, open: !!config.open },\n\n async load(range: VisibleRange, signal?: AbortSignal): Promise<CalendarEvent[]> {\n const filter = andFilters(config.filter, buildODataRangeFilter(range, config.map, behaviors, zone));\n const rows = await config.query({ select, filter, orderBy: `${config.map.start} asc` }, signal);\n const events: CalendarEvent[] = [];\n for (const row of rows) {\n const id = String(row[config.idColumn] ?? '');\n if (!id) continue;\n try {\n const e = mapRecordToEvent(webApiRowAccessor(row, id), config.map, { behaviors, zone });\n if (e) events.push(e);\n } catch (error) {\n config.onInvalid?.(id, error);\n }\n }\n return events;\n },\n\n update: config.update\n ? async (change: CalendarEventChange): Promise<void> => {\n await config.update!(change.event.id, toWebApiPatch(change, config.map, { behaviors, zone }));\n }\n : undefined,\n open: config.open,\n create: config.create,\n };\n}\n"],"mappings":"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","names":["t"]}
|
package/dist/dataverse.d.cts
CHANGED
|
@@ -1,121 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as ColumnMap, D as DateTimeBehavior, R as RecordAccessor } from './mapping-DRx7vxQl.cjs';
|
|
2
|
+
export { M as MapOptions, b as behaviorOf, e as exclusiveEndToInclusive, i as inclusiveEndToExclusive, m as mapRecordToEvent, r as readDataverseDate, a as readYesNo, t as toFormParameterDate, c as toQuickCreateParameters, d as toWebApiDate, f as toWebApiPatch } from './mapping-DRx7vxQl.cjs';
|
|
3
|
+
import { V as VisibleRange, Z as ZoneOffset } from './types-Bxkg9NIR.cjs';
|
|
4
|
+
export { a as PullQuery, P as PullSource, b as PullSourceConfig, R as Row, c as createPullSource } from './pull-DuZIyoqY.cjs';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ★★★ The two write paths are DIFFERENT CONTRACTS. The same text means different things to
|
|
9
|
-
* `webAPI.updateRecord` and to `navigation.openForm` parameters:
|
|
10
|
-
*
|
|
11
|
-
* | behaviour | read (dataset getValue) | Web API write | form parameter (quick create) |
|
|
12
|
-
* |------------|-----------------------------|------------------------|-------------------------------|
|
|
13
|
-
* | UserLocal | `…T06:30:00.000Z` instant | instant `…Z` | wall clock, NO `Z` |
|
|
14
|
-
* | DateOnly | `…T00:00:00.000Z` UTC-midn. | `YYYY-MM-DD` | `YYYY-MM-DDT00:00:00`, NO `Z` |
|
|
15
|
-
* | TZI | `…T23:30:00.000Z` face+`Z` | wall clock WITH `Z` | wall clock, NO `Z` |
|
|
16
|
-
*
|
|
17
|
-
* Measured failures of the obvious choice, each of which a unit test here pins:
|
|
18
|
-
* - a DateOnly read through `new Date()` lands on the PREVIOUS day anywhere west of UTC;
|
|
19
|
-
* - a bare `YYYY-MM-DD` form parameter was stored as the previous day (Pacific user);
|
|
20
|
-
* - a TZI form parameter WITH `Z` was treated as UTC and shifted −7 h.
|
|
21
|
-
* The first two are why a well-known client control had a "+1 day" hack in its click handler.
|
|
22
|
-
*/
|
|
23
|
-
declare const DateTimeBehavior: {
|
|
24
|
-
readonly UserLocal: 1;
|
|
25
|
-
readonly DateOnly: 2;
|
|
26
|
-
readonly TimeZoneIndependent: 3;
|
|
27
|
-
};
|
|
28
|
-
type DateTimeBehavior = (typeof DateTimeBehavior)[keyof typeof DateTimeBehavior];
|
|
29
|
-
/**
|
|
30
|
-
* The behaviour of a column, from whatever metadata shape the host produced.
|
|
31
|
-
*
|
|
32
|
-
* ★ `getEntityMetadata` in a PCF host answers `Behavior: 1|2|3` (MEASURED). The Web API's
|
|
33
|
-
* `EntityDefinitions` answers `DateTimeBehavior: { Value: 'UserLocal' }`. Both are accepted.
|
|
34
|
-
* ★ A DateOnly column is also recognisable from its PCF `dataType` alone
|
|
35
|
-
* (`DateAndTime.DateOnly`). TZI and UserLocal are NOT — both report `DateAndTime.DateAndTime` —
|
|
36
|
-
* so without metadata a TZI column is indistinguishable and is treated as UserLocal. Callers
|
|
37
|
-
* that can see that ambiguity should surface it (see `adapters/pcf-dataset`).
|
|
38
|
-
*/
|
|
39
|
-
declare function behaviorOf(meta: unknown, dataType?: string): DateTimeBehavior;
|
|
40
|
-
/**
|
|
41
|
-
* A raw column value (dataset `getValue()`, or a Web API property) → a calendar value.
|
|
42
|
-
*
|
|
43
|
-
* ★★★ `getValue()` returns an ISO STRING for all three behaviours, despite
|
|
44
|
-
* `@types/powerapps-component-framework` declaring `Date`. A `Date` is still accepted, because a
|
|
45
|
-
* test double or another host may produce one — and it is read through its UTC fields, which is
|
|
46
|
-
* what the platform's strings encode.
|
|
47
|
-
*/
|
|
48
|
-
declare function readDataverseDate(raw: unknown, behavior: DateTimeBehavior): CalendarValue | null;
|
|
49
|
-
/**
|
|
50
|
-
* A calendar value → the text `webAPI.updateRecord` / `createRecord` wants for this behaviour.
|
|
51
|
-
* `zone` is needed only to turn a value of a different kind into this one (e.g. an all-day drop
|
|
52
|
-
* onto a UserLocal column); same-kind values pass straight through.
|
|
53
|
-
*/
|
|
54
|
-
declare function toWebApiDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string;
|
|
55
|
-
/**
|
|
56
|
-
* A calendar value → the text a `navigation.openForm` form PARAMETER wants (quick create
|
|
57
|
-
* prefill). Wall clock in the user's zone, never with `Z`.
|
|
58
|
-
*
|
|
59
|
-
* ★ UNMEASURED: whether "wall clock" here means the Dynamics USER's zone or the BROWSER's when
|
|
60
|
-
* they differ. Measured only with both in Pacific. Pass the user zone; if the org proof shows
|
|
61
|
-
* the browser's is used, this is the one function to change.
|
|
62
|
-
*/
|
|
63
|
-
declare function toFormParameterDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string;
|
|
64
|
-
/** Dataverse stores an all-day END inclusive; the calendar's is exclusive. */
|
|
65
|
-
declare const inclusiveEndToExclusive: (lastDay: CalendarValue) => CalendarValue;
|
|
66
|
-
declare const exclusiveEndToInclusive: (dayAfter: CalendarValue) => CalendarValue;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Which columns hold what. Logical names. Only `start` is required — that is the whole
|
|
70
|
-
* genericity story: the same calendar shows Appointments for one client and Orders for another,
|
|
71
|
-
* because a maker names the columns rather than a developer.
|
|
72
|
-
*/
|
|
73
|
-
interface ColumnMap {
|
|
74
|
-
start: string;
|
|
75
|
-
end?: string;
|
|
76
|
-
title?: string;
|
|
77
|
-
/** A hex colour column, or a choice column whose option colours `colorOf` resolves. */
|
|
78
|
-
color?: string;
|
|
79
|
-
/** Location / staff / queue. Drives `resourceId` and, failing `color`, a group colour. */
|
|
80
|
-
group?: string;
|
|
81
|
-
/** A Yes/No column that forces all-day. */
|
|
82
|
-
allDay?: string;
|
|
83
|
-
}
|
|
84
|
-
/** The minimum a record source must answer — a dataset record, a Web API row, a fixture. */
|
|
85
|
-
interface RecordAccessor {
|
|
86
|
-
id: string;
|
|
87
|
-
value(column: string): unknown;
|
|
88
|
-
formatted(column: string): string;
|
|
89
|
-
}
|
|
90
|
-
interface MapOptions {
|
|
91
|
-
/** Behaviour per DATE column (logical name). Missing ⇒ UserLocal. */
|
|
92
|
-
behaviors: Readonly<Record<string, DateTimeBehavior>>;
|
|
93
|
-
zone: ZoneOffset;
|
|
94
|
-
/** Resolve a colour column's value (e.g. choice option colours). Return undefined to fall back. */
|
|
95
|
-
colorOf?: (column: string, raw: unknown, formatted: string) => string | undefined;
|
|
96
|
-
/** Per-record read-only, e.g. a status that forbids rescheduling. */
|
|
97
|
-
isReadOnly?: (record: RecordAccessor) => boolean;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* ★★ A Yes/No column's raw value in a PCF dataset is the STRING `"0"` / `"1"` — and
|
|
101
|
-
* `Boolean("0")` is `true`. MEASURED on a sibling gallery control, where every `false` row
|
|
102
|
-
* rendered ON. Read the digits, never truthiness.
|
|
103
|
-
*/
|
|
104
|
-
declare function readYesNo(raw: unknown): boolean;
|
|
105
|
-
/** One record → one event, or null when it has no usable start. */
|
|
106
|
-
declare function mapRecordToEvent(record: RecordAccessor, map: ColumnMap, opts: MapOptions): CalendarEvent | null;
|
|
107
|
-
/**
|
|
108
|
-
* A move/resize → the column values to PATCH through the Web API.
|
|
109
|
-
* Columns the map does not name are never written.
|
|
7
|
+
* The `$filter` for "events overlapping the visible range" — the same overlap rule the PCF
|
|
8
|
+
* adapter uses: `start < rangeEnd AND (end >= rangeStart OR start >= rangeStart)`, so a
|
|
9
|
+
* multi-day event that began before the first visible day is kept.
|
|
110
10
|
*/
|
|
111
|
-
declare function
|
|
11
|
+
declare function buildODataRangeFilter(range: VisibleRange, map: ColumnMap, behaviors: Readonly<Record<string, DateTimeBehavior>>, zone: ZoneOffset): string;
|
|
12
|
+
/** AND a caller's own filter (e.g. "only open slots") with the range filter. */
|
|
13
|
+
declare function andFilters(...parts: (string | undefined)[]): string;
|
|
14
|
+
/** Every Web API property the map needs, plus the id, de-duplicated. */
|
|
15
|
+
declare function selectFor(map: ColumnMap, idColumn: string): string[];
|
|
112
16
|
/**
|
|
113
|
-
* A
|
|
114
|
-
*
|
|
17
|
+
* A Web API row (Dataverse, a code app's connector, or Power Pages `/_api`) as the accessor the
|
|
18
|
+
* mapping reads.
|
|
115
19
|
*
|
|
116
|
-
* ★
|
|
117
|
-
*
|
|
20
|
+
* ★ Formatted values come from the `@OData.Community.Display.V1.FormattedValue` annotation, which
|
|
21
|
+
* the request must ask for (`Prefer: odata.include-annotations=…`). Without it a choice or a
|
|
22
|
+
* lookup reads as its raw value / GUID — never silently as empty.
|
|
118
23
|
*/
|
|
119
|
-
declare function
|
|
24
|
+
declare function webApiRowAccessor(row: Readonly<Record<string, unknown>>, id: string): RecordAccessor;
|
|
25
|
+
/** The request header that makes the Web API return formatted values. */
|
|
26
|
+
declare const FORMATTED_VALUES_PREFER = "odata.include-annotations=\"OData.Community.Display.V1.FormattedValue\"";
|
|
120
27
|
|
|
121
|
-
export {
|
|
28
|
+
export { ColumnMap, DateTimeBehavior, FORMATTED_VALUES_PREFER, RecordAccessor, andFilters, buildODataRangeFilter, selectFor, webApiRowAccessor };
|
package/dist/dataverse.d.ts
CHANGED
|
@@ -1,121 +1,28 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as ColumnMap, D as DateTimeBehavior, R as RecordAccessor } from './mapping-P6GFE8dF.js';
|
|
2
|
+
export { M as MapOptions, b as behaviorOf, e as exclusiveEndToInclusive, i as inclusiveEndToExclusive, m as mapRecordToEvent, r as readDataverseDate, a as readYesNo, t as toFormParameterDate, c as toQuickCreateParameters, d as toWebApiDate, f as toWebApiPatch } from './mapping-P6GFE8dF.js';
|
|
3
|
+
import { V as VisibleRange, Z as ZoneOffset } from './types-Bxkg9NIR.js';
|
|
4
|
+
export { a as PullQuery, P as PullSource, b as PullSourceConfig, R as Row, c as createPullSource } from './pull-DXFuI2sU.js';
|
|
2
5
|
|
|
3
6
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* ★★★ The two write paths are DIFFERENT CONTRACTS. The same text means different things to
|
|
9
|
-
* `webAPI.updateRecord` and to `navigation.openForm` parameters:
|
|
10
|
-
*
|
|
11
|
-
* | behaviour | read (dataset getValue) | Web API write | form parameter (quick create) |
|
|
12
|
-
* |------------|-----------------------------|------------------------|-------------------------------|
|
|
13
|
-
* | UserLocal | `…T06:30:00.000Z` instant | instant `…Z` | wall clock, NO `Z` |
|
|
14
|
-
* | DateOnly | `…T00:00:00.000Z` UTC-midn. | `YYYY-MM-DD` | `YYYY-MM-DDT00:00:00`, NO `Z` |
|
|
15
|
-
* | TZI | `…T23:30:00.000Z` face+`Z` | wall clock WITH `Z` | wall clock, NO `Z` |
|
|
16
|
-
*
|
|
17
|
-
* Measured failures of the obvious choice, each of which a unit test here pins:
|
|
18
|
-
* - a DateOnly read through `new Date()` lands on the PREVIOUS day anywhere west of UTC;
|
|
19
|
-
* - a bare `YYYY-MM-DD` form parameter was stored as the previous day (Pacific user);
|
|
20
|
-
* - a TZI form parameter WITH `Z` was treated as UTC and shifted −7 h.
|
|
21
|
-
* The first two are why a well-known client control had a "+1 day" hack in its click handler.
|
|
22
|
-
*/
|
|
23
|
-
declare const DateTimeBehavior: {
|
|
24
|
-
readonly UserLocal: 1;
|
|
25
|
-
readonly DateOnly: 2;
|
|
26
|
-
readonly TimeZoneIndependent: 3;
|
|
27
|
-
};
|
|
28
|
-
type DateTimeBehavior = (typeof DateTimeBehavior)[keyof typeof DateTimeBehavior];
|
|
29
|
-
/**
|
|
30
|
-
* The behaviour of a column, from whatever metadata shape the host produced.
|
|
31
|
-
*
|
|
32
|
-
* ★ `getEntityMetadata` in a PCF host answers `Behavior: 1|2|3` (MEASURED). The Web API's
|
|
33
|
-
* `EntityDefinitions` answers `DateTimeBehavior: { Value: 'UserLocal' }`. Both are accepted.
|
|
34
|
-
* ★ A DateOnly column is also recognisable from its PCF `dataType` alone
|
|
35
|
-
* (`DateAndTime.DateOnly`). TZI and UserLocal are NOT — both report `DateAndTime.DateAndTime` —
|
|
36
|
-
* so without metadata a TZI column is indistinguishable and is treated as UserLocal. Callers
|
|
37
|
-
* that can see that ambiguity should surface it (see `adapters/pcf-dataset`).
|
|
38
|
-
*/
|
|
39
|
-
declare function behaviorOf(meta: unknown, dataType?: string): DateTimeBehavior;
|
|
40
|
-
/**
|
|
41
|
-
* A raw column value (dataset `getValue()`, or a Web API property) → a calendar value.
|
|
42
|
-
*
|
|
43
|
-
* ★★★ `getValue()` returns an ISO STRING for all three behaviours, despite
|
|
44
|
-
* `@types/powerapps-component-framework` declaring `Date`. A `Date` is still accepted, because a
|
|
45
|
-
* test double or another host may produce one — and it is read through its UTC fields, which is
|
|
46
|
-
* what the platform's strings encode.
|
|
47
|
-
*/
|
|
48
|
-
declare function readDataverseDate(raw: unknown, behavior: DateTimeBehavior): CalendarValue | null;
|
|
49
|
-
/**
|
|
50
|
-
* A calendar value → the text `webAPI.updateRecord` / `createRecord` wants for this behaviour.
|
|
51
|
-
* `zone` is needed only to turn a value of a different kind into this one (e.g. an all-day drop
|
|
52
|
-
* onto a UserLocal column); same-kind values pass straight through.
|
|
53
|
-
*/
|
|
54
|
-
declare function toWebApiDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string;
|
|
55
|
-
/**
|
|
56
|
-
* A calendar value → the text a `navigation.openForm` form PARAMETER wants (quick create
|
|
57
|
-
* prefill). Wall clock in the user's zone, never with `Z`.
|
|
58
|
-
*
|
|
59
|
-
* ★ UNMEASURED: whether "wall clock" here means the Dynamics USER's zone or the BROWSER's when
|
|
60
|
-
* they differ. Measured only with both in Pacific. Pass the user zone; if the org proof shows
|
|
61
|
-
* the browser's is used, this is the one function to change.
|
|
62
|
-
*/
|
|
63
|
-
declare function toFormParameterDate(value: CalendarValue, behavior: DateTimeBehavior, zone: ZoneOffset): string;
|
|
64
|
-
/** Dataverse stores an all-day END inclusive; the calendar's is exclusive. */
|
|
65
|
-
declare const inclusiveEndToExclusive: (lastDay: CalendarValue) => CalendarValue;
|
|
66
|
-
declare const exclusiveEndToInclusive: (dayAfter: CalendarValue) => CalendarValue;
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Which columns hold what. Logical names. Only `start` is required — that is the whole
|
|
70
|
-
* genericity story: the same calendar shows Appointments for one client and Orders for another,
|
|
71
|
-
* because a maker names the columns rather than a developer.
|
|
72
|
-
*/
|
|
73
|
-
interface ColumnMap {
|
|
74
|
-
start: string;
|
|
75
|
-
end?: string;
|
|
76
|
-
title?: string;
|
|
77
|
-
/** A hex colour column, or a choice column whose option colours `colorOf` resolves. */
|
|
78
|
-
color?: string;
|
|
79
|
-
/** Location / staff / queue. Drives `resourceId` and, failing `color`, a group colour. */
|
|
80
|
-
group?: string;
|
|
81
|
-
/** A Yes/No column that forces all-day. */
|
|
82
|
-
allDay?: string;
|
|
83
|
-
}
|
|
84
|
-
/** The minimum a record source must answer — a dataset record, a Web API row, a fixture. */
|
|
85
|
-
interface RecordAccessor {
|
|
86
|
-
id: string;
|
|
87
|
-
value(column: string): unknown;
|
|
88
|
-
formatted(column: string): string;
|
|
89
|
-
}
|
|
90
|
-
interface MapOptions {
|
|
91
|
-
/** Behaviour per DATE column (logical name). Missing ⇒ UserLocal. */
|
|
92
|
-
behaviors: Readonly<Record<string, DateTimeBehavior>>;
|
|
93
|
-
zone: ZoneOffset;
|
|
94
|
-
/** Resolve a colour column's value (e.g. choice option colours). Return undefined to fall back. */
|
|
95
|
-
colorOf?: (column: string, raw: unknown, formatted: string) => string | undefined;
|
|
96
|
-
/** Per-record read-only, e.g. a status that forbids rescheduling. */
|
|
97
|
-
isReadOnly?: (record: RecordAccessor) => boolean;
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* ★★ A Yes/No column's raw value in a PCF dataset is the STRING `"0"` / `"1"` — and
|
|
101
|
-
* `Boolean("0")` is `true`. MEASURED on a sibling gallery control, where every `false` row
|
|
102
|
-
* rendered ON. Read the digits, never truthiness.
|
|
103
|
-
*/
|
|
104
|
-
declare function readYesNo(raw: unknown): boolean;
|
|
105
|
-
/** One record → one event, or null when it has no usable start. */
|
|
106
|
-
declare function mapRecordToEvent(record: RecordAccessor, map: ColumnMap, opts: MapOptions): CalendarEvent | null;
|
|
107
|
-
/**
|
|
108
|
-
* A move/resize → the column values to PATCH through the Web API.
|
|
109
|
-
* Columns the map does not name are never written.
|
|
7
|
+
* The `$filter` for "events overlapping the visible range" — the same overlap rule the PCF
|
|
8
|
+
* adapter uses: `start < rangeEnd AND (end >= rangeStart OR start >= rangeStart)`, so a
|
|
9
|
+
* multi-day event that began before the first visible day is kept.
|
|
110
10
|
*/
|
|
111
|
-
declare function
|
|
11
|
+
declare function buildODataRangeFilter(range: VisibleRange, map: ColumnMap, behaviors: Readonly<Record<string, DateTimeBehavior>>, zone: ZoneOffset): string;
|
|
12
|
+
/** AND a caller's own filter (e.g. "only open slots") with the range filter. */
|
|
13
|
+
declare function andFilters(...parts: (string | undefined)[]): string;
|
|
14
|
+
/** Every Web API property the map needs, plus the id, de-duplicated. */
|
|
15
|
+
declare function selectFor(map: ColumnMap, idColumn: string): string[];
|
|
112
16
|
/**
|
|
113
|
-
* A
|
|
114
|
-
*
|
|
17
|
+
* A Web API row (Dataverse, a code app's connector, or Power Pages `/_api`) as the accessor the
|
|
18
|
+
* mapping reads.
|
|
115
19
|
*
|
|
116
|
-
* ★
|
|
117
|
-
*
|
|
20
|
+
* ★ Formatted values come from the `@OData.Community.Display.V1.FormattedValue` annotation, which
|
|
21
|
+
* the request must ask for (`Prefer: odata.include-annotations=…`). Without it a choice or a
|
|
22
|
+
* lookup reads as its raw value / GUID — never silently as empty.
|
|
118
23
|
*/
|
|
119
|
-
declare function
|
|
24
|
+
declare function webApiRowAccessor(row: Readonly<Record<string, unknown>>, id: string): RecordAccessor;
|
|
25
|
+
/** The request header that makes the Web API return formatted values. */
|
|
26
|
+
declare const FORMATTED_VALUES_PREFER = "odata.include-annotations=\"OData.Community.Display.V1.FormattedValue\"";
|
|
120
27
|
|
|
121
|
-
export {
|
|
28
|
+
export { ColumnMap, DateTimeBehavior, FORMATTED_VALUES_PREFER, RecordAccessor, andFilters, buildODataRangeFilter, selectFor, webApiRowAccessor };
|
package/dist/dataverse.mjs
CHANGED
|
@@ -3,6 +3,7 @@ var DATE_RE = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
|
3
3
|
var FLOATING_RE = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,9})?)?$/;
|
|
4
4
|
var INSTANT_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
5
5
|
var pad = (n, w = 2) => String(n).padStart(w, "0");
|
|
6
|
+
var browserZone = (instant) => -instant.getTimezoneOffset();
|
|
6
7
|
function kindOf(value) {
|
|
7
8
|
if (value instanceof Date) {
|
|
8
9
|
if (Number.isNaN(value.getTime())) throw new RangeError("Invalid Date");
|
|
@@ -247,17 +248,93 @@ function toQuickCreateParameters(selection, map, opts, extra = {}) {
|
|
|
247
248
|
if (map.allDay && selection.allDay) params[map.allDay] = true;
|
|
248
249
|
return params;
|
|
249
250
|
}
|
|
251
|
+
|
|
252
|
+
// src/dataverse/odata.ts
|
|
253
|
+
var FORMATTED = "@OData.Community.Display.V1.FormattedValue";
|
|
254
|
+
function literal(range, edge, behavior, zone) {
|
|
255
|
+
if (behavior === DateTimeBehavior.DateOnly) return edge === "start" ? range.startDate : range.endDate;
|
|
256
|
+
if (behavior === DateTimeBehavior.TimeZoneIndependent) return `${toWall(range[edge], zone)}Z`;
|
|
257
|
+
return range[edge];
|
|
258
|
+
}
|
|
259
|
+
function buildODataRangeFilter(range, map, behaviors, zone) {
|
|
260
|
+
const sb = behaviors[map.start] ?? DateTimeBehavior.UserLocal;
|
|
261
|
+
const startsBeforeEnd = `${map.start} lt ${literal(range, "end", sb, zone)}`;
|
|
262
|
+
const startsInRange = `${map.start} ge ${literal(range, "start", sb, zone)}`;
|
|
263
|
+
if (!map.end) return `${startsBeforeEnd} and ${startsInRange}`;
|
|
264
|
+
const eb = behaviors[map.end] ?? DateTimeBehavior.UserLocal;
|
|
265
|
+
return `${startsBeforeEnd} and (${map.end} ge ${literal(range, "start", eb, zone)} or ${startsInRange})`;
|
|
266
|
+
}
|
|
267
|
+
function andFilters(...parts) {
|
|
268
|
+
const present = parts.filter((p) => !!p && p.trim() !== "");
|
|
269
|
+
return present.length <= 1 ? present[0] ?? "" : present.map((p) => `(${p})`).join(" and ");
|
|
270
|
+
}
|
|
271
|
+
function selectFor(map, idColumn) {
|
|
272
|
+
return [.../* @__PURE__ */ new Set([idColumn, ...Object.values(map).filter((c) => !!c)])];
|
|
273
|
+
}
|
|
274
|
+
function webApiRowAccessor(row, id) {
|
|
275
|
+
return {
|
|
276
|
+
id,
|
|
277
|
+
value: (c) => row[c] ?? null,
|
|
278
|
+
formatted: (c) => {
|
|
279
|
+
const f = row[`${c}${FORMATTED}`];
|
|
280
|
+
if (typeof f === "string") return f;
|
|
281
|
+
const raw = row[c];
|
|
282
|
+
return raw === null || raw === void 0 ? "" : String(raw);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
var FORMATTED_VALUES_PREFER = 'odata.include-annotations="OData.Community.Display.V1.FormattedValue"';
|
|
287
|
+
|
|
288
|
+
// src/dataverse/pull.ts
|
|
289
|
+
function createPullSource(config) {
|
|
290
|
+
const zone = config.zone ?? browserZone;
|
|
291
|
+
const behaviors = config.behaviors ?? {};
|
|
292
|
+
const dateColumns = [config.map.start, config.map.end].filter((c) => !!c);
|
|
293
|
+
const guessed = dateColumns.filter((c) => behaviors[c] === void 0);
|
|
294
|
+
const select = selectFor(config.map, config.idColumn);
|
|
295
|
+
return {
|
|
296
|
+
guessed,
|
|
297
|
+
capabilities: { update: !!config.update, create: !!config.create, open: !!config.open },
|
|
298
|
+
async load(range, signal) {
|
|
299
|
+
const filter = andFilters(config.filter, buildODataRangeFilter(range, config.map, behaviors, zone));
|
|
300
|
+
const rows = await config.query({ select, filter, orderBy: `${config.map.start} asc` }, signal);
|
|
301
|
+
const events = [];
|
|
302
|
+
for (const row of rows) {
|
|
303
|
+
const id = String(row[config.idColumn] ?? "");
|
|
304
|
+
if (!id) continue;
|
|
305
|
+
try {
|
|
306
|
+
const e = mapRecordToEvent(webApiRowAccessor(row, id), config.map, { behaviors, zone });
|
|
307
|
+
if (e) events.push(e);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
config.onInvalid?.(id, error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return events;
|
|
313
|
+
},
|
|
314
|
+
update: config.update ? async (change) => {
|
|
315
|
+
await config.update(change.event.id, toWebApiPatch(change, config.map, { behaviors, zone }));
|
|
316
|
+
} : void 0,
|
|
317
|
+
open: config.open,
|
|
318
|
+
create: config.create
|
|
319
|
+
};
|
|
320
|
+
}
|
|
250
321
|
export {
|
|
251
322
|
DateTimeBehavior,
|
|
323
|
+
FORMATTED_VALUES_PREFER,
|
|
324
|
+
andFilters,
|
|
252
325
|
behaviorOf,
|
|
326
|
+
buildODataRangeFilter,
|
|
327
|
+
createPullSource,
|
|
253
328
|
exclusiveEndToInclusive,
|
|
254
329
|
inclusiveEndToExclusive,
|
|
255
330
|
mapRecordToEvent,
|
|
256
331
|
readDataverseDate,
|
|
257
332
|
readYesNo,
|
|
333
|
+
selectFor,
|
|
258
334
|
toFormParameterDate,
|
|
259
335
|
toQuickCreateParameters,
|
|
260
336
|
toWebApiDate,
|
|
261
|
-
toWebApiPatch
|
|
337
|
+
toWebApiPatch,
|
|
338
|
+
webApiRowAccessor
|
|
262
339
|
};
|
|
263
340
|
//# sourceMappingURL=dataverse.mjs.map
|