@singi-labs/sifa-sdk 0.19.84 → 0.19.86
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/dist/import/index.js +10 -2
- package/dist/import/index.js.map +1 -1
- package/dist/import/presentation-csv.js +4 -0
- package/dist/import/presentation-csv.js.map +1 -1
- package/dist/import/talks-import-validation.js +170 -0
- package/dist/import/talks-import-validation.js.map +1 -0
- package/dist/index.cjs +307 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +67 -4
- package/dist/index.d.ts +67 -4
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/{network-map-DMDU4liU.d.cts → network-map-BRoBXwPG.d.cts} +2 -2
- package/dist/{network-map-BEOM5v7g.d.ts → network-map-NbEQwn91.d.ts} +2 -2
- package/dist/{profile-summary-1oXL1guc.d.ts → profile-summary-BGIJ6ovO.d.ts} +1 -1
- package/dist/{profile-summary-CpM9l8iq.d.cts → profile-summary-BpfRgE0z.d.cts} +1 -1
- package/dist/query/fetchers/index.cjs +2 -1
- package/dist/query/fetchers/index.cjs.map +1 -1
- package/dist/query/fetchers/index.d.cts +5 -5
- package/dist/query/fetchers/index.d.ts +5 -5
- package/dist/query/fetchers/search.js +2 -1
- package/dist/query/fetchers/search.js.map +1 -1
- package/dist/query/hooks/index.cjs +2 -1
- package/dist/query/hooks/index.cjs.map +1 -1
- package/dist/query/hooks/index.d.cts +3 -3
- package/dist/query/hooks/index.d.ts +3 -3
- package/dist/query/index.cjs +2 -1
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +8 -8
- package/dist/query/index.d.ts +8 -8
- package/dist/schemas/write/index.d.cts +7 -7
- package/dist/schemas/write/index.d.ts +7 -7
- package/dist/{types-DQfWqSud.d.cts → types-CPi-Xcur.d.cts} +2 -0
- package/dist/{types-DB7UkX43.d.ts → types-D2IffOnb.d.ts} +2 -0
- package/dist/{use-user-settings-U-cIysUa.d.cts → use-user-settings-BxIVvuk3.d.cts} +2 -2
- package/dist/{use-user-settings-A_i6Nrj1.d.ts → use-user-settings-D_APuRP2.d.ts} +2 -2
- package/dist/{user-settings-DdWaL2AG.d.cts → user-settings-BFyyuR3C.d.cts} +1 -1
- package/dist/{user-settings-CGJPdwTM.d.ts → user-settings-BSlRJc0W.d.ts} +1 -1
- package/package.json +1 -1
package/dist/import/index.js
CHANGED
|
@@ -7,9 +7,16 @@ import {
|
|
|
7
7
|
normalizePresentationRole,
|
|
8
8
|
normalizePresentationMode,
|
|
9
9
|
presentationCsvRowToRecord,
|
|
10
|
-
presentationDeliveryCsvRowToRecord
|
|
10
|
+
presentationDeliveryCsvRowToRecord,
|
|
11
|
+
PRESENTATION_ROLE_VALUES,
|
|
12
|
+
PRESENTATION_MODE_VALUES
|
|
11
13
|
} from "./presentation-csv.js";
|
|
14
|
+
import {
|
|
15
|
+
validateTalksImport
|
|
16
|
+
} from "./talks-import-validation.js";
|
|
12
17
|
export {
|
|
18
|
+
PRESENTATION_MODE_VALUES,
|
|
19
|
+
PRESENTATION_ROLE_VALUES,
|
|
13
20
|
durationFromMinutes,
|
|
14
21
|
normalizePresentationMode,
|
|
15
22
|
normalizePresentationRole,
|
|
@@ -17,6 +24,7 @@ export {
|
|
|
17
24
|
parsePresentationDuration,
|
|
18
25
|
presentationCsvRowToRecord,
|
|
19
26
|
presentationDeliveryCsvRowToRecord,
|
|
20
|
-
stripHtmlToText
|
|
27
|
+
stripHtmlToText,
|
|
28
|
+
validateTalksImport
|
|
21
29
|
};
|
|
22
30
|
//# sourceMappingURL=index.js.map
|
package/dist/import/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/import/index.ts"],"sourcesContent":["export {\n parsePresentationDuration,\n durationFromMinutes,\n parseIntendedAudiences,\n stripHtmlToText,\n normalizePresentationRole,\n normalizePresentationMode,\n presentationCsvRowToRecord,\n presentationDeliveryCsvRowToRecord,\n type CsvRow,\n type ParsedPresentation,\n type ParsedDelivery,\n} from './presentation-csv.js';\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/import/index.ts"],"sourcesContent":["export {\n parsePresentationDuration,\n durationFromMinutes,\n parseIntendedAudiences,\n stripHtmlToText,\n normalizePresentationRole,\n normalizePresentationMode,\n presentationCsvRowToRecord,\n presentationDeliveryCsvRowToRecord,\n PRESENTATION_ROLE_VALUES,\n PRESENTATION_MODE_VALUES,\n type CsvRow,\n type ParsedPresentation,\n type ParsedDelivery,\n} from './presentation-csv.js';\nexport {\n validateTalksImport,\n type TalksImportIssue,\n type TalksImportIssueCode,\n type TalksImportValidation,\n} from './talks-import-validation.js';\n"],"mappings":";AAAA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AACP;AAAA,EACE;AAAA,OAIK;","names":[]}
|
|
@@ -41,6 +41,7 @@ var ROLE_TOKENS = {
|
|
|
41
41
|
workshop: "id.sifa.defs#workshop",
|
|
42
42
|
host: "id.sifa.defs#host"
|
|
43
43
|
};
|
|
44
|
+
var PRESENTATION_ROLE_VALUES = Object.keys(ROLE_TOKENS);
|
|
44
45
|
function normalizePresentationRole(value) {
|
|
45
46
|
const v = clean(value).toLowerCase();
|
|
46
47
|
if (!v) return void 0;
|
|
@@ -66,6 +67,7 @@ var MODE_FRAGMENTS = {
|
|
|
66
67
|
remote: "#virtual",
|
|
67
68
|
hybrid: "#hybrid"
|
|
68
69
|
};
|
|
70
|
+
var PRESENTATION_MODE_VALUES = Object.keys(MODE_FRAGMENTS);
|
|
69
71
|
function normalizePresentationMode(value) {
|
|
70
72
|
const v = clean(value).toLowerCase();
|
|
71
73
|
if (!v) return void 0;
|
|
@@ -115,6 +117,8 @@ function presentationDeliveryCsvRowToRecord(row) {
|
|
|
115
117
|
return { presentationKey: clean(row.presentation_key) || void 0, record };
|
|
116
118
|
}
|
|
117
119
|
export {
|
|
120
|
+
PRESENTATION_MODE_VALUES,
|
|
121
|
+
PRESENTATION_ROLE_VALUES,
|
|
118
122
|
durationFromMinutes,
|
|
119
123
|
normalizePresentationMode,
|
|
120
124
|
normalizePresentationRole,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/import/presentation-csv.ts"],"sourcesContent":["import type {\n ProfilePresentationRecord,\n ProfilePresentationDeliveryRecord,\n PresentationLink,\n PresentationDuration,\n} from '../schemas/index.js';\n\n/**\n * CSV parsers for the Talks & sessions import (two templates joined by a\n * presentation_key). The value parsers and row mappers are reusable and pure;\n * the actual PDS writing (create a presentation, then write its deliveries with\n * a presentationRef to it) is orchestrated by the consumer, which has the\n * created record's AT-URI.\n */\n\n/** A CSV row as a header-keyed map. Missing cells may be undefined or empty. */\nexport type CsvRow = Record<string, string | undefined>;\n\nfunction clean(value: string | undefined): string {\n return (value ?? '').trim();\n}\n\n/**\n * A number with an optional unit: \"30\", \"30 min\", \"1.5 h\", \"3 hours\". The\n * lookahead stops a unit from matching the start of a word (\"20 to 30\").\n */\nconst DURATION_PART =\n /(\\d{1,5}(?:[.,]\\d{1,3})?)\\s{0,3}(h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)?(?![a-z])/gi;\n\n/** Durations are short free text; cap the input so matching stays linear. */\nconst DURATION_MAX_LENGTH = 100;\n\n/**\n * Parse a free-text duration into minutes: a single value (\"30 min\", \"30\n * minutes\", \"30\", \"3 hours\") or a range (\"20-30 min\", \"20 to 30 minutes\",\n * \"40 minutes - 4 hours\"). Hours convert to minutes. A number without a unit\n * takes the unit of the next number (\"2-3 hours\" is 120 to 180), else minutes.\n * Returns undefined when no usable number is present. A second number is used\n * as the upper bound only when it is greater than or equal to the first.\n */\nexport function parsePresentationDuration(\n input: string | undefined,\n): PresentationDuration | undefined {\n const parts = [...clean(input).slice(0, DURATION_MAX_LENGTH).matchAll(DURATION_PART)].map(\n (m) => ({\n value: Number.parseFloat((m[1] ?? '').replace(',', '.')),\n unit: m[2]?.toLowerCase().startsWith('h') ? 'h' : m[2] ? 'm' : undefined,\n }),\n );\n const numbers = parts\n .map((part, i) => {\n const unit = part.unit ?? parts.slice(i + 1).find((p) => p.unit)?.unit ?? 'm';\n return Math.round(unit === 'h' ? part.value * 60 : part.value);\n })\n .filter((n) => Number.isInteger(n) && n > 0);\n // `.at()` is typed `number | undefined`, which both tsc (under\n // noUncheckedIndexedAccess) and eslint agree on, so the guards are clean.\n const min = numbers.at(0);\n if (min === undefined) return undefined;\n const max = numbers.at(1);\n return max !== undefined && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Build a duration from explicit min/max minute columns. */\nexport function durationFromMinutes(\n minRaw: string | undefined,\n maxRaw: string | undefined,\n): PresentationDuration | undefined {\n const min = Number.parseInt(clean(minRaw), 10);\n if (!Number.isInteger(min) || min < 1) return undefined;\n const max = Number.parseInt(clean(maxRaw), 10);\n return Number.isInteger(max) && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Split a delimited list into trimmed, non-empty values. Default delimiter \";\". */\nexport function parseIntendedAudiences(input: string | undefined, delimiter = ';'): string[] {\n return clean(input)\n .split(delimiter)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Strip HTML to plain text and decode the common entities, for descriptions\n * exported as HTML (e.g. Webflow). The AppView re-sanitizes on write; this is\n * only to produce readable text.\n */\nexport function stripHtmlToText(input: string | undefined): string {\n const text = clean(input)\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/\\s*(p|div|li|h[1-6])\\s*>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/�*39;|'/gi, \"'\");\n return text\n .replace(/[ \\t]+/g, ' ')\n .replace(/ *\\n */g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nconst ROLE_TOKENS: Record<string, string> = {\n presenter: 'id.sifa.defs#presenter',\n speaker: 'id.sifa.defs#presenter',\n panelist: 'id.sifa.defs#panelist',\n keynote: 'id.sifa.defs#keynote',\n workshop: 'id.sifa.defs#workshop',\n host: 'id.sifa.defs#host',\n};\n\n/**\n * Map a friendly role value to its token. An existing token passes through.\n * Free-text or compound values (e.g. \"Organizer & co-host/moderator\") are\n * mapped to the nearest token by keyword. An organizer-only value has no\n * speaking token, so it, and any other unrecognized value, is dropped rather\n * than stored raw -- matching normalizePresentationMode's drop-unknown\n * behavior, so a fixed role dropdown never has to render arbitrary strings.\n */\nexport function normalizePresentationRole(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('id.sifa.defs#')) return v;\n const exact = ROLE_TOKENS[v];\n if (exact) return exact;\n if (v.includes('keynote')) return ROLE_TOKENS.keynote;\n if (v.includes('workshop') || v.includes('masterclass') || v.includes('training'))\n return ROLE_TOKENS.workshop;\n if (v.includes('panel')) return ROLE_TOKENS.panelist;\n if (v.includes('host') || v.includes('moderat') || v.includes('emcee') || v.includes('chair'))\n return ROLE_TOKENS.host;\n if (v.includes('speaker') || v.includes('present') || v.includes('talk'))\n return ROLE_TOKENS.presenter;\n return undefined;\n}\n\nconst MODE_FRAGMENTS: Record<string, string> = {\n inperson: '#inperson',\n 'in person': '#inperson',\n 'in-person': '#inperson',\n virtual: '#virtual',\n online: '#virtual',\n remote: '#virtual',\n hybrid: '#hybrid',\n};\n\n/** Map a friendly mode value to the community calendar token; drops an unknown value. */\nexport function normalizePresentationMode(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('community.lexicon.calendar.event#')) return v;\n const fragment = MODE_FRAGMENTS[v];\n return fragment ? `community.lexicon.calendar.event${fragment}` : undefined;\n}\n\nfunction link(uri: string | undefined, type: string): PresentationLink | undefined {\n const u = clean(uri);\n return u ? { uri: u, type } : undefined;\n}\n\nexport interface ParsedPresentation {\n /** presentation_key used to link delivery rows to this presentation. */\n key?: string;\n record: Omit<ProfilePresentationRecord, 'createdAt'>;\n}\n\n/** Map a row of the presentations template to a presentation record (minus createdAt). */\nexport function presentationCsvRowToRecord(row: CsvRow): ParsedPresentation {\n const description = stripHtmlToText(row.description);\n const duration =\n parsePresentationDuration(row.duration) ??\n durationFromMinutes(row.duration_min_minutes, row.duration_max_minutes);\n const intendedAudiences = parseIntendedAudiences(row.intended_audiences);\n const links = [\n link(row.slides_url, 'id.sifa.defs#linkSlides'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n link(row.writeup_url, 'id.sifa.defs#linkWriteup'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationRecord, 'createdAt'> = { title: clean(row.title) };\n if (description) record.description = description;\n if (duration) record.duration = duration;\n if (intendedAudiences.length) record.intendedAudiences = intendedAudiences;\n if (links.length) record.links = links;\n\n return { key: clean(row.presentation_key) || undefined, record };\n}\n\nexport interface ParsedDelivery {\n /** presentation_key to resolve into a presentationRef once the presentation exists. */\n presentationKey?: string;\n record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'>;\n}\n\n/** Map a row of the deliveries template to a delivery record (minus createdAt and presentationRef). */\nexport function presentationDeliveryCsvRowToRecord(row: CsvRow): ParsedDelivery {\n const links = [\n link(row.event_url, 'id.sifa.defs#linkEvent'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'> = {};\n const title = clean(row.title);\n const eventName = clean(row.event_name);\n const date = clean(row.date);\n const location = clean(row.location);\n const role = normalizePresentationRole(row.role);\n const mode = normalizePresentationMode(row.mode);\n if (title) record.title = title;\n if (role) record.role = role;\n if (eventName) record.eventName = eventName;\n if (date) record.date = date;\n if (location) record.location = location;\n if (mode) record.mode = mode;\n if (links.length) record.links = links;\n\n return { presentationKey: clean(row.presentation_key) || undefined, record };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/import/presentation-csv.ts"],"sourcesContent":["import type {\n ProfilePresentationRecord,\n ProfilePresentationDeliveryRecord,\n PresentationLink,\n PresentationDuration,\n} from '../schemas/index.js';\n\n/**\n * CSV parsers for the Talks & sessions import (two templates joined by a\n * presentation_key). The value parsers and row mappers are reusable and pure;\n * the actual PDS writing (create a presentation, then write its deliveries with\n * a presentationRef to it) is orchestrated by the consumer, which has the\n * created record's AT-URI.\n */\n\n/** A CSV row as a header-keyed map. Missing cells may be undefined or empty. */\nexport type CsvRow = Record<string, string | undefined>;\n\nfunction clean(value: string | undefined): string {\n return (value ?? '').trim();\n}\n\n/**\n * A number with an optional unit: \"30\", \"30 min\", \"1.5 h\", \"3 hours\". The\n * lookahead stops a unit from matching the start of a word (\"20 to 30\").\n */\nconst DURATION_PART =\n /(\\d{1,5}(?:[.,]\\d{1,3})?)\\s{0,3}(h(?:ours?|rs?)?|m(?:in(?:ute)?s?)?)?(?![a-z])/gi;\n\n/** Durations are short free text; cap the input so matching stays linear. */\nconst DURATION_MAX_LENGTH = 100;\n\n/**\n * Parse a free-text duration into minutes: a single value (\"30 min\", \"30\n * minutes\", \"30\", \"3 hours\") or a range (\"20-30 min\", \"20 to 30 minutes\",\n * \"40 minutes - 4 hours\"). Hours convert to minutes. A number without a unit\n * takes the unit of the next number (\"2-3 hours\" is 120 to 180), else minutes.\n * Returns undefined when no usable number is present. A second number is used\n * as the upper bound only when it is greater than or equal to the first.\n */\nexport function parsePresentationDuration(\n input: string | undefined,\n): PresentationDuration | undefined {\n const parts = [...clean(input).slice(0, DURATION_MAX_LENGTH).matchAll(DURATION_PART)].map(\n (m) => ({\n value: Number.parseFloat((m[1] ?? '').replace(',', '.')),\n unit: m[2]?.toLowerCase().startsWith('h') ? 'h' : m[2] ? 'm' : undefined,\n }),\n );\n const numbers = parts\n .map((part, i) => {\n const unit = part.unit ?? parts.slice(i + 1).find((p) => p.unit)?.unit ?? 'm';\n return Math.round(unit === 'h' ? part.value * 60 : part.value);\n })\n .filter((n) => Number.isInteger(n) && n > 0);\n // `.at()` is typed `number | undefined`, which both tsc (under\n // noUncheckedIndexedAccess) and eslint agree on, so the guards are clean.\n const min = numbers.at(0);\n if (min === undefined) return undefined;\n const max = numbers.at(1);\n return max !== undefined && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Build a duration from explicit min/max minute columns. */\nexport function durationFromMinutes(\n minRaw: string | undefined,\n maxRaw: string | undefined,\n): PresentationDuration | undefined {\n const min = Number.parseInt(clean(minRaw), 10);\n if (!Number.isInteger(min) || min < 1) return undefined;\n const max = Number.parseInt(clean(maxRaw), 10);\n return Number.isInteger(max) && max >= min\n ? { minMinutes: min, maxMinutes: max }\n : { minMinutes: min };\n}\n\n/** Split a delimited list into trimmed, non-empty values. Default delimiter \";\". */\nexport function parseIntendedAudiences(input: string | undefined, delimiter = ';'): string[] {\n return clean(input)\n .split(delimiter)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\n/**\n * Strip HTML to plain text and decode the common entities, for descriptions\n * exported as HTML (e.g. Webflow). The AppView re-sanitizes on write; this is\n * only to produce readable text.\n */\nexport function stripHtmlToText(input: string | undefined): string {\n const text = clean(input)\n .replace(/<\\s*br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/\\s*(p|div|li|h[1-6])\\s*>/gi, '\\n')\n .replace(/<[^>]*>/g, '')\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/</gi, '<')\n .replace(/>/gi, '>')\n .replace(/"/gi, '\"')\n .replace(/�*39;|'/gi, \"'\");\n return text\n .replace(/[ \\t]+/g, ' ')\n .replace(/ *\\n */g, '\\n')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n}\n\nconst ROLE_TOKENS: Record<string, string> = {\n presenter: 'id.sifa.defs#presenter',\n speaker: 'id.sifa.defs#presenter',\n panelist: 'id.sifa.defs#panelist',\n keynote: 'id.sifa.defs#keynote',\n workshop: 'id.sifa.defs#workshop',\n host: 'id.sifa.defs#host',\n};\n\n/** Friendly role values the import maps to a token (keyword matches aside). */\nexport const PRESENTATION_ROLE_VALUES = Object.keys(ROLE_TOKENS);\n\n/**\n * Map a friendly role value to its token. An existing token passes through.\n * Free-text or compound values (e.g. \"Organizer & co-host/moderator\") are\n * mapped to the nearest token by keyword. An organizer-only value has no\n * speaking token, so it, and any other unrecognized value, is dropped rather\n * than stored raw -- matching normalizePresentationMode's drop-unknown\n * behavior, so a fixed role dropdown never has to render arbitrary strings.\n */\nexport function normalizePresentationRole(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('id.sifa.defs#')) return v;\n const exact = ROLE_TOKENS[v];\n if (exact) return exact;\n if (v.includes('keynote')) return ROLE_TOKENS.keynote;\n if (v.includes('workshop') || v.includes('masterclass') || v.includes('training'))\n return ROLE_TOKENS.workshop;\n if (v.includes('panel')) return ROLE_TOKENS.panelist;\n if (v.includes('host') || v.includes('moderat') || v.includes('emcee') || v.includes('chair'))\n return ROLE_TOKENS.host;\n if (v.includes('speaker') || v.includes('present') || v.includes('talk'))\n return ROLE_TOKENS.presenter;\n return undefined;\n}\n\nconst MODE_FRAGMENTS: Record<string, string> = {\n inperson: '#inperson',\n 'in person': '#inperson',\n 'in-person': '#inperson',\n virtual: '#virtual',\n online: '#virtual',\n remote: '#virtual',\n hybrid: '#hybrid',\n};\n\n/** Friendly mode values the import maps to a token. */\nexport const PRESENTATION_MODE_VALUES = Object.keys(MODE_FRAGMENTS);\n\n/** Map a friendly mode value to the community calendar token; drops an unknown value. */\nexport function normalizePresentationMode(value: string | undefined): string | undefined {\n const v = clean(value).toLowerCase();\n if (!v) return undefined;\n if (v.startsWith('community.lexicon.calendar.event#')) return v;\n const fragment = MODE_FRAGMENTS[v];\n return fragment ? `community.lexicon.calendar.event${fragment}` : undefined;\n}\n\nfunction link(uri: string | undefined, type: string): PresentationLink | undefined {\n const u = clean(uri);\n return u ? { uri: u, type } : undefined;\n}\n\nexport interface ParsedPresentation {\n /** presentation_key used to link delivery rows to this presentation. */\n key?: string;\n record: Omit<ProfilePresentationRecord, 'createdAt'>;\n}\n\n/** Map a row of the presentations template to a presentation record (minus createdAt). */\nexport function presentationCsvRowToRecord(row: CsvRow): ParsedPresentation {\n const description = stripHtmlToText(row.description);\n const duration =\n parsePresentationDuration(row.duration) ??\n durationFromMinutes(row.duration_min_minutes, row.duration_max_minutes);\n const intendedAudiences = parseIntendedAudiences(row.intended_audiences);\n const links = [\n link(row.slides_url, 'id.sifa.defs#linkSlides'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n link(row.writeup_url, 'id.sifa.defs#linkWriteup'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationRecord, 'createdAt'> = { title: clean(row.title) };\n if (description) record.description = description;\n if (duration) record.duration = duration;\n if (intendedAudiences.length) record.intendedAudiences = intendedAudiences;\n if (links.length) record.links = links;\n\n return { key: clean(row.presentation_key) || undefined, record };\n}\n\nexport interface ParsedDelivery {\n /** presentation_key to resolve into a presentationRef once the presentation exists. */\n presentationKey?: string;\n record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'>;\n}\n\n/** Map a row of the deliveries template to a delivery record (minus createdAt and presentationRef). */\nexport function presentationDeliveryCsvRowToRecord(row: CsvRow): ParsedDelivery {\n const links = [\n link(row.event_url, 'id.sifa.defs#linkEvent'),\n link(row.recording_url, 'id.sifa.defs#linkRecording'),\n ].filter((l): l is PresentationLink => l !== undefined);\n\n const record: Omit<ProfilePresentationDeliveryRecord, 'createdAt' | 'presentationRef'> = {};\n const title = clean(row.title);\n const eventName = clean(row.event_name);\n const date = clean(row.date);\n const location = clean(row.location);\n const role = normalizePresentationRole(row.role);\n const mode = normalizePresentationMode(row.mode);\n if (title) record.title = title;\n if (role) record.role = role;\n if (eventName) record.eventName = eventName;\n if (date) record.date = date;\n if (location) record.location = location;\n if (mode) record.mode = mode;\n if (links.length) record.links = links;\n\n return { presentationKey: clean(row.presentation_key) || undefined, record };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
// src/import/talks-import-validation.ts
|
|
2
|
+
import { DATE_LOOKAHEAD_MONTHS, isBeyondLookahead } from "../logic/date-lookahead.js";
|
|
3
|
+
import {
|
|
4
|
+
httpUrlOrNull,
|
|
5
|
+
isValidPartialDate,
|
|
6
|
+
PresentationDeliveryWriteSchema,
|
|
7
|
+
PresentationWriteSchema
|
|
8
|
+
} from "../schemas/write/index.js";
|
|
9
|
+
import {
|
|
10
|
+
presentationCsvRowToRecord,
|
|
11
|
+
presentationDeliveryCsvRowToRecord
|
|
12
|
+
} from "./presentation-csv.js";
|
|
13
|
+
var PRESENTATION_URL_COLUMNS = ["slides_url", "recording_url", "writeup_url"];
|
|
14
|
+
var DELIVERY_URL_COLUMNS = ["event_url", "recording_url"];
|
|
15
|
+
var RECORD_FIELD_COLUMNS = {
|
|
16
|
+
title: "title",
|
|
17
|
+
description: "description",
|
|
18
|
+
intendedAudiences: "intended_audiences",
|
|
19
|
+
eventName: "event_name",
|
|
20
|
+
location: "location"
|
|
21
|
+
};
|
|
22
|
+
function schemaErrorColumn(error) {
|
|
23
|
+
const field = error.issues[0]?.path[0];
|
|
24
|
+
return typeof field === "string" ? RECORD_FIELD_COLUMNS[field] : void 0;
|
|
25
|
+
}
|
|
26
|
+
function cell(row, column) {
|
|
27
|
+
return (row[column] ?? "").trim();
|
|
28
|
+
}
|
|
29
|
+
function urlIssues(row, rowNumber, file, columns) {
|
|
30
|
+
return columns.flatMap((column) => {
|
|
31
|
+
const value = cell(row, column);
|
|
32
|
+
return value && httpUrlOrNull(value) === null ? [{ file, row: rowNumber, column, value, code: "invalidUrl", severity: "error" }] : [];
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function validateTalksImport(input, now = /* @__PURE__ */ new Date()) {
|
|
36
|
+
const issues = [];
|
|
37
|
+
const presentations = [];
|
|
38
|
+
const deliveries = [];
|
|
39
|
+
const validKeys = /* @__PURE__ */ new Set();
|
|
40
|
+
const skippedKeys = /* @__PURE__ */ new Set();
|
|
41
|
+
input.presentationRows.forEach((row, index) => {
|
|
42
|
+
const rowNumber = index + 2;
|
|
43
|
+
const rowIssues = [];
|
|
44
|
+
const parsed = presentationCsvRowToRecord(row);
|
|
45
|
+
if (!parsed.record.title) {
|
|
46
|
+
rowIssues.push({
|
|
47
|
+
file: "presentations",
|
|
48
|
+
row: rowNumber,
|
|
49
|
+
column: "title",
|
|
50
|
+
code: "missingTitle",
|
|
51
|
+
severity: "error"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
rowIssues.push(...urlIssues(row, rowNumber, "presentations", PRESENTATION_URL_COLUMNS));
|
|
55
|
+
const schema = rowIssues.length === 0 ? PresentationWriteSchema.safeParse(parsed.record) : null;
|
|
56
|
+
if (schema && !schema.success) {
|
|
57
|
+
const column = schemaErrorColumn(schema.error);
|
|
58
|
+
rowIssues.push({
|
|
59
|
+
file: "presentations",
|
|
60
|
+
row: rowNumber,
|
|
61
|
+
...column ? { column } : {},
|
|
62
|
+
code: "invalidRecord",
|
|
63
|
+
severity: "error"
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
issues.push(...rowIssues);
|
|
67
|
+
if (rowIssues.length > 0) {
|
|
68
|
+
if (parsed.key) skippedKeys.add(parsed.key);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
presentations.push(parsed);
|
|
72
|
+
if (parsed.key) validKeys.add(parsed.key);
|
|
73
|
+
});
|
|
74
|
+
input.deliveryRows.forEach((row, index) => {
|
|
75
|
+
const rowNumber = index + 2;
|
|
76
|
+
const rowIssues = [];
|
|
77
|
+
const parsed = presentationDeliveryCsvRowToRecord(row);
|
|
78
|
+
const { record, presentationKey } = parsed;
|
|
79
|
+
const linked = presentationKey !== void 0 && validKeys.has(presentationKey);
|
|
80
|
+
if (presentationKey !== void 0 && skippedKeys.has(presentationKey) && !linked) {
|
|
81
|
+
rowIssues.push({
|
|
82
|
+
file: "deliveries",
|
|
83
|
+
row: rowNumber,
|
|
84
|
+
column: "presentation_key",
|
|
85
|
+
value: presentationKey,
|
|
86
|
+
code: "linkedTalkSkipped",
|
|
87
|
+
severity: "error"
|
|
88
|
+
});
|
|
89
|
+
} else if (presentationKey !== void 0 && !linked) {
|
|
90
|
+
rowIssues.push({
|
|
91
|
+
file: "deliveries",
|
|
92
|
+
row: rowNumber,
|
|
93
|
+
column: "presentation_key",
|
|
94
|
+
value: presentationKey,
|
|
95
|
+
code: "unknownPresentationKey",
|
|
96
|
+
severity: "warning"
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
if (!record.title && !record.eventName && !linked && !skippedKeys.has(presentationKey ?? "")) {
|
|
100
|
+
rowIssues.push({
|
|
101
|
+
file: "deliveries",
|
|
102
|
+
row: rowNumber,
|
|
103
|
+
code: "missingTitleOrEvent",
|
|
104
|
+
severity: "error"
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
if (record.date && !isValidPartialDate(record.date)) {
|
|
108
|
+
rowIssues.push({
|
|
109
|
+
file: "deliveries",
|
|
110
|
+
row: rowNumber,
|
|
111
|
+
column: "date",
|
|
112
|
+
value: record.date,
|
|
113
|
+
code: "invalidDate",
|
|
114
|
+
severity: "error"
|
|
115
|
+
});
|
|
116
|
+
} else if (record.date && isBeyondLookahead(record.date, DATE_LOOKAHEAD_MONTHS.talk, now)) {
|
|
117
|
+
rowIssues.push({
|
|
118
|
+
file: "deliveries",
|
|
119
|
+
row: rowNumber,
|
|
120
|
+
column: "date",
|
|
121
|
+
value: record.date,
|
|
122
|
+
code: "dateTooFarAhead",
|
|
123
|
+
severity: "error"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
rowIssues.push(...urlIssues(row, rowNumber, "deliveries", DELIVERY_URL_COLUMNS));
|
|
127
|
+
const role = cell(row, "role");
|
|
128
|
+
if (role && !record.role) {
|
|
129
|
+
rowIssues.push({
|
|
130
|
+
file: "deliveries",
|
|
131
|
+
row: rowNumber,
|
|
132
|
+
column: "role",
|
|
133
|
+
value: role,
|
|
134
|
+
code: "unknownRole",
|
|
135
|
+
severity: "warning"
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
const mode = cell(row, "mode");
|
|
139
|
+
if (mode && !record.mode) {
|
|
140
|
+
rowIssues.push({
|
|
141
|
+
file: "deliveries",
|
|
142
|
+
row: rowNumber,
|
|
143
|
+
column: "mode",
|
|
144
|
+
value: mode,
|
|
145
|
+
code: "unknownMode",
|
|
146
|
+
severity: "warning"
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
let hasError = rowIssues.some((i) => i.severity === "error");
|
|
150
|
+
const schema = hasError ? null : PresentationDeliveryWriteSchema.safeParse(record);
|
|
151
|
+
if (schema && !schema.success) {
|
|
152
|
+
const column = schemaErrorColumn(schema.error);
|
|
153
|
+
rowIssues.push({
|
|
154
|
+
file: "deliveries",
|
|
155
|
+
row: rowNumber,
|
|
156
|
+
...column ? { column } : {},
|
|
157
|
+
code: "invalidRecord",
|
|
158
|
+
severity: "error"
|
|
159
|
+
});
|
|
160
|
+
hasError = true;
|
|
161
|
+
}
|
|
162
|
+
issues.push(...rowIssues);
|
|
163
|
+
if (!hasError) deliveries.push(parsed);
|
|
164
|
+
});
|
|
165
|
+
return { presentations, deliveries, issues };
|
|
166
|
+
}
|
|
167
|
+
export {
|
|
168
|
+
validateTalksImport
|
|
169
|
+
};
|
|
170
|
+
//# sourceMappingURL=talks-import-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/import/talks-import-validation.ts"],"sourcesContent":["import { DATE_LOOKAHEAD_MONTHS, isBeyondLookahead } from '../logic/date-lookahead.js';\nimport {\n httpUrlOrNull,\n isValidPartialDate,\n PresentationDeliveryWriteSchema,\n PresentationWriteSchema,\n} from '../schemas/write/index.js';\nimport {\n presentationCsvRowToRecord,\n presentationDeliveryCsvRowToRecord,\n type CsvRow,\n type ParsedDelivery,\n type ParsedPresentation,\n} from './presentation-csv.js';\n\n/**\n * Why a Talks & sessions CSV row was skipped (`error`) or imported with a\n * caveat (`warning`). Consumers map the code to translated copy.\n */\nexport type TalksImportIssueCode =\n /** error: a talk row has no title. */\n | 'missingTitle'\n /** error: a URL column holds something that is not an http(s) URL. */\n | 'invalidUrl'\n /** error: the date is not YYYY, YYYY-MM or a real YYYY-MM-DD day. */\n | 'invalidDate'\n /** error: the session date is further ahead than the editor allows. */\n | 'dateTooFarAhead'\n /** error: a session has no title, no event name and no linked talk. */\n | 'missingTitleOrEvent'\n /** error: the session's presentation_key names a talk row that was skipped for an error. */\n | 'linkedTalkSkipped'\n /** error: the record fails the write schema for another reason (e.g. a value too long). */\n | 'invalidRecord'\n /** warning: presentation_key matches no talk in this upload; imported as a one-off session. */\n | 'unknownPresentationKey'\n /** warning: the role is not recognised; imported without a role. */\n | 'unknownRole'\n /** warning: the mode is not recognised; imported without a mode. */\n | 'unknownMode';\n\nexport interface TalksImportIssue {\n file: 'presentations' | 'deliveries';\n /**\n * Spreadsheet row number: the header is row 1, so the first data row is 2.\n * Off for files with multi-line quoted cells, which spreadsheets rarely emit.\n */\n row: number;\n /** CSV column the problem is in, when there is one. */\n column?: string;\n /** The offending cell value, when showing it helps. */\n value?: string;\n code: TalksImportIssueCode;\n severity: 'error' | 'warning';\n}\n\nexport interface TalksImportValidation {\n /** Talk rows without errors, ready to send to the import endpoint. */\n presentations: ParsedPresentation[];\n /** Session rows without errors, ready to send to the import endpoint. */\n deliveries: ParsedDelivery[];\n /** Every problem found, in file then row order. */\n issues: TalksImportIssue[];\n}\n\nconst PRESENTATION_URL_COLUMNS = ['slides_url', 'recording_url', 'writeup_url'] as const;\nconst DELIVERY_URL_COLUMNS = ['event_url', 'recording_url'] as const;\n\n/** Record field to CSV column, to point a write-schema failure at a cell. */\nconst RECORD_FIELD_COLUMNS: Record<string, string> = {\n title: 'title',\n description: 'description',\n intendedAudiences: 'intended_audiences',\n eventName: 'event_name',\n location: 'location',\n};\n\n/** CSV column of the first write-schema problem, when it maps to one. */\nfunction schemaErrorColumn(error: { issues: { path: PropertyKey[] }[] }): string | undefined {\n const field = error.issues[0]?.path[0];\n return typeof field === 'string' ? RECORD_FIELD_COLUMNS[field] : undefined;\n}\n\nfunction cell(row: CsvRow, column: string): string {\n return (row[column] ?? '').trim();\n}\n\nfunction urlIssues(\n row: CsvRow,\n rowNumber: number,\n file: TalksImportIssue['file'],\n columns: readonly string[],\n): TalksImportIssue[] {\n return columns.flatMap((column) => {\n const value = cell(row, column);\n return value && httpUrlOrNull(value) === null\n ? [{ file, row: rowNumber, column, value, code: 'invalidUrl', severity: 'error' } as const]\n : [];\n });\n}\n\n/**\n * Map and check the two Talks & sessions templates row by row, so a preview can\n * say which rows will be skipped and why instead of the whole import failing.\n * Rows with an error are left out of the returned records; rows with only a\n * warning are kept. Applies the same 12-month session-date limit as the editor\n * (`DATE_LOOKAHEAD_MONTHS.talk`). Pure; `now` is injectable for tests.\n */\nexport function validateTalksImport(\n input: { presentationRows: CsvRow[]; deliveryRows: CsvRow[] },\n now: Date = new Date(),\n): TalksImportValidation {\n const issues: TalksImportIssue[] = [];\n const presentations: ParsedPresentation[] = [];\n const deliveries: ParsedDelivery[] = [];\n const validKeys = new Set<string>();\n const skippedKeys = new Set<string>();\n\n input.presentationRows.forEach((row, index) => {\n const rowNumber = index + 2;\n const rowIssues: TalksImportIssue[] = [];\n const parsed = presentationCsvRowToRecord(row);\n if (!parsed.record.title) {\n rowIssues.push({\n file: 'presentations',\n row: rowNumber,\n column: 'title',\n code: 'missingTitle',\n severity: 'error',\n });\n }\n rowIssues.push(...urlIssues(row, rowNumber, 'presentations', PRESENTATION_URL_COLUMNS));\n const schema = rowIssues.length === 0 ? PresentationWriteSchema.safeParse(parsed.record) : null;\n if (schema && !schema.success) {\n const column = schemaErrorColumn(schema.error);\n rowIssues.push({\n file: 'presentations',\n row: rowNumber,\n ...(column ? { column } : {}),\n code: 'invalidRecord',\n severity: 'error',\n });\n }\n issues.push(...rowIssues);\n if (rowIssues.length > 0) {\n if (parsed.key) skippedKeys.add(parsed.key);\n return;\n }\n presentations.push(parsed);\n if (parsed.key) validKeys.add(parsed.key);\n });\n\n input.deliveryRows.forEach((row, index) => {\n const rowNumber = index + 2;\n const rowIssues: TalksImportIssue[] = [];\n const parsed = presentationDeliveryCsvRowToRecord(row);\n const { record, presentationKey } = parsed;\n\n const linked = presentationKey !== undefined && validKeys.has(presentationKey);\n if (presentationKey !== undefined && skippedKeys.has(presentationKey) && !linked) {\n // Importing it as a one-off would cut it off from its talk for good.\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'presentation_key',\n value: presentationKey,\n code: 'linkedTalkSkipped',\n severity: 'error',\n });\n } else if (presentationKey !== undefined && !linked) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'presentation_key',\n value: presentationKey,\n code: 'unknownPresentationKey',\n severity: 'warning',\n });\n }\n if (!record.title && !record.eventName && !linked && !skippedKeys.has(presentationKey ?? '')) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n code: 'missingTitleOrEvent',\n severity: 'error',\n });\n }\n if (record.date && !isValidPartialDate(record.date)) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'date',\n value: record.date,\n code: 'invalidDate',\n severity: 'error',\n });\n } else if (record.date && isBeyondLookahead(record.date, DATE_LOOKAHEAD_MONTHS.talk, now)) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'date',\n value: record.date,\n code: 'dateTooFarAhead',\n severity: 'error',\n });\n }\n rowIssues.push(...urlIssues(row, rowNumber, 'deliveries', DELIVERY_URL_COLUMNS));\n // The mapper drops a value it doesn't recognise, so a filled cell with no\n // field on the record was unknown.\n const role = cell(row, 'role');\n if (role && !record.role) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'role',\n value: role,\n code: 'unknownRole',\n severity: 'warning',\n });\n }\n const mode = cell(row, 'mode');\n if (mode && !record.mode) {\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n column: 'mode',\n value: mode,\n code: 'unknownMode',\n severity: 'warning',\n });\n }\n\n let hasError = rowIssues.some((i) => i.severity === 'error');\n const schema = hasError ? null : PresentationDeliveryWriteSchema.safeParse(record);\n if (schema && !schema.success) {\n const column = schemaErrorColumn(schema.error);\n rowIssues.push({\n file: 'deliveries',\n row: rowNumber,\n ...(column ? { column } : {}),\n code: 'invalidRecord',\n severity: 'error',\n });\n hasError = true;\n }\n issues.push(...rowIssues);\n if (!hasError) deliveries.push(parsed);\n });\n\n return { presentations, deliveries, issues };\n}\n"],"mappings":"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","names":[]}
|