diff --git a/package.json b/package.json index f2ad489e6aa..0e89601312e 100644 --- a/package.json +++ b/package.json @@ -96,7 +96,8 @@ }, "patchedDependencies": { "@crowdin/ota-client@1.0.0": "patches/@crowdin__ota-client@1.0.0.patch", - "trpc-panel@1.3.4": "patches/trpc-panel@1.3.4.patch" + "trpc-panel@1.3.4": "patches/trpc-panel@1.3.4.patch", + "social-links@1.14.0": "patches/social-links@1.14.0.patch" }, "peerDependencyRules": { "allowedVersions": { diff --git a/packages/api/router/organization/query.searchDistance.handler.ts b/packages/api/router/organization/query.searchDistance.handler.ts index dac62e9c930..44973080a09 100644 --- a/packages/api/router/organization/query.searchDistance.handler.ts +++ b/packages/api/router/organization/query.searchDistance.handler.ts @@ -105,38 +105,52 @@ services AS ( ), service_area as ( SELECT - country."geoDataId" as "countryGeoId", - district."geoDataId" as "districtGeoId", - district.slug AS "districtSlug", - country.cca3 as "cca3", - sa."organizationId", - sa."orgLocationId" + (CASE + WHEN sa."organizationId" IS NOT NULL THEN sa."organizationId" + WHEN sa."orgLocationId" IS NOT NULL THEN (SELECT "orgId" FROM "OrgLocation" loc WHERE loc.id = sa."orgLocationId" ) + WHEN sa."orgServiceId" IS NOT NULL THEN COALESCE((SELECT DISTINCT loc."orgId" FROM "OrgLocationService" ols LEFT JOIN "OrgLocation" loc ON ols."orgLocationId" = loc.id WHERE ols."serviceId" = sa."orgServiceId"), + (SELECT os."organizationId" FROM "OrgService" os WHERE os.id = sa."orgServiceId") + ) + END + ) AS "orgId", + ARRAY_agg(DISTINCT CASE + WHEN country."geoDataId" IS NOT NULL THEN country."geoDataId" + WHEN district."geoDataId" IS NOT NULL THEN district."geoDataId" + END) AS "geoId", + array_remove(array_agg(DISTINCT district.slug), NULL) AS "matchedDistricts", + array_remove(array_agg(DISTINCT country.cca3),NULL) AS "matchedCountries" FROM "ServiceArea" sa - LEFT JOIN "ServiceAreaCountry" sac ON sac. "serviceAreaId" = sa.id AND sac.active - LEFT JOIN "ServiceAreaDist" sad ON sad. "serviceAreaId" = sa.id AND sad.active - LEFT JOIN "Country" country ON country.id = sac. "countryId" - LEFT JOIN "GovDist" district ON district.id = sad. "govDistId" + LEFT JOIN "ServiceAreaCountry" sac ON sac. "serviceAreaId" = sa.id + AND sac.active + LEFT JOIN "ServiceAreaDist" sad ON sad. "serviceAreaId" = sa.id + AND sad.active + LEFT JOIN "Country" country ON country.id = sac. "countryId" AND country. "geoDataId" = ANY( + SELECT id + FROM covered_areas + ) + LEFT JOIN "GovDist" district ON district.id = sad. "govDistId" AND district. "geoDataId" = ANY( + SELECT id + FROM covered_areas + ) WHERE sa.active - AND sa."organizationId" is not null AND ( - country."geoDataId" is not null - OR district."geoDataId" is not null + country. "geoDataId" = ANY( + SELECT id + FROM covered_areas ) - AND (country."geoDataId" = ANY( - SELECT id - FROM covered_areas - ) - OR district."geoDataId" = ANY( - SELECT id - FROM covered_areas - )) + OR district. "geoDataId" = ANY( + SELECT id + FROM covered_areas + ) + ) + GROUP BY "orgId" ) SELECT *, COUNT(*) OVER ()::int AS total FROM ( SELECT - loc. "orgId", + org.id, ${ hasServiceFilter ? Prisma.sql`ARRAY_REMOVE(ARRAY_AGG(DISTINCT services. "tagId"), NULL) AS "matchedServices",` @@ -155,16 +169,14 @@ service_area as ( ST_Distance(ST_Transform(loc.geo, 3857), (SELECT meters FROM points))::int ) ) AS distance, - ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3), NULL) AS "national", - ARRAY_LENGTH(ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3), NULL),1) is not NULL AS "isNational", - ARRAY_REMOVE(ARRAY_AGG(DISTINCT sa.cca3) || ARRAY_AGG( DISTINCT sa."districtSlug"), NULL) AS "serviceAreas" - FROM "OrgLocation" loc - INNER JOIN "Organization" org ON org.id = loc. "orgId" - LEFT JOIN service_area sa ON sa. "organizationId" = loc. "orgId" + sa."matchedCountries" AS "national" + FROM "Organization" org + INNER JOIN "OrgLocation" loc ON org.id = loc. "orgId" + LEFT JOIN service_area sa ON sa. "orgId" = org.id ${ hasServiceFilter ? Prisma.sql` - INNER JOIN services ON services."organizationId" = loc."orgId"` + INNER JOIN services ON services."organizationId" = org.id` : Prisma.empty } ${ @@ -176,15 +188,14 @@ service_area as ( WHERE ( ST_DWithin(ST_Transform(loc.geo, 3857), (SELECT meters FROM points), ${searchRadius}) - OR sa."countryGeoId" = ANY(SELECT id FROM covered_areas) - OR sa."districtGeoId" = ANY(SELECT id FROM covered_areas) + OR sa."geoId" && ARRAY(SELECT id FROM covered_areas) ) AND loc.published AND org.published AND NOT loc.deleted AND NOT org.deleted GROUP BY - loc."orgId" + org.id, sa."matchedCountries" ORDER BY distance ) result @@ -197,21 +208,21 @@ OFFSET ${skip}` const formattedResults = results.map((result) => { if (parseInt(result.total) !== total) total = parseInt(result.total) return { - id: result.orgId, + id: result.id, distMeters: parseInt(result.distance), - national: result.national, + national: result.national ?? [], } }) return { results: formattedResults, total } } type SearchResult = { - orgId: string + id: string matchedServices?: string[] matchedAttributes?: string[] distance: string - national: string[] - isNational: boolean - serviceAreas: string[] + national: string[] | null + // isNational: boolean + // serviceAreas: string[] total: string } const prismaDistSearchDetails = async (input: TSearchDistanceSchema & { resultIds: string[] }) => { diff --git a/packages/db/lib/generateFreeText.ts b/packages/db/lib/generateFreeText.ts index 65aabeb1ab8..b8fc44215c4 100644 --- a/packages/db/lib/generateFreeText.ts +++ b/packages/db/lib/generateFreeText.ts @@ -49,92 +49,49 @@ export const generateFreeText = ({ }), } } -export const generateNestedFreeText = ({ - orgId: orgSlug, - itemId, - text, - type, - freeTextId, -}: GenerateFreeTextParams) => { - const key = (() => { - switch (type) { - case 'orgDesc': { - return createKey([orgSlug, 'description']) - } - case 'attSupp': { - invariant(itemId) - return createKey([orgSlug, 'attribute', itemId]) - } - case 'svcName': { - invariant(itemId) - return createKey([orgSlug, itemId, 'name']) - } - case 'websiteDesc': - case 'phoneDesc': - case 'emailDesc': - case 'svcDesc': { - invariant(itemId) - return createKey([orgSlug, itemId, 'description']) - } - } - })() - invariant(key, 'Error creating key') - const ns = namespaces.orgData +export const generateNestedFreeText = (args: GenerateFreeTextParams) => { + const { freeText, translationKey } = generateFreeText(args) return { create: { - id: freeTextId ?? generateId('freeText'), - tsKey: { create: { key, text, namespace: { connect: { name: ns } } } }, + id: freeText.id, + tsKey: { + create: { + key: translationKey.key, + text: translationKey.text, + namespace: { connect: { name: translationKey.ns } }, + }, + }, }, } } -export const generateNestedFreeTextUpsert = ({ - orgId: orgSlug, - itemId, - text, - type, - freeTextId, -}: GenerateFreeTextParams) => { - const key = (() => { - switch (type) { - case 'orgDesc': { - return createKey([orgSlug, 'description']) - } - case 'attSupp': { - invariant(itemId) - return createKey([orgSlug, 'attribute', itemId]) - } - case 'svcName': { - invariant(itemId) - return createKey([orgSlug, itemId, 'name']) - } - case 'websiteDesc': - case 'phoneDesc': - case 'emailDesc': - case 'svcDesc': { - invariant(itemId) - return createKey([orgSlug, itemId, 'description']) - } - } - })() - invariant(key, 'Error creating key') - const ns = namespaces.orgData - - const id = freeTextId ?? generateId('freeText') +export const generateNestedFreeTextUpsert = ( + args: GenerateFreeTextParams +): Prisma.FreeTextUpdateOneWithoutOrgEmailNestedInput => { + const { freeText, translationKey } = generateFreeText(args) return { upsert: { - where: { id }, + // where: { id: freeText.id }, create: { - id, - tsKey: { create: { key, text, namespace: { connect: { name: ns } } } }, + id: freeText.id, + tsKey: { + create: { + key: translationKey.key, + text: translationKey.text, + namespace: { connect: { name: translationKey.ns } }, + }, + }, }, update: { tsKey: { - upsert: { - where: { key }, - create: { key, text, namespace: { connect: { name: ns } } }, - update: { text }, - }, + // upsert: { + // create: { + // key: translationKey.key, + // text: translationKey.text, + // namespace: { connect: { name: translationKey.ns } }, + // }, + update: { text: translationKey.text }, + // }, }, }, }, diff --git a/packages/db/package.json b/packages/db/package.json index a6972ea11e3..0e0852e9371 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -97,6 +97,7 @@ "prisma-query-inspector": "1.4.4", "prisma-query-log": "3.2.0", "slugify": "1.6.6", + "social-links": "1.14.0", "sql-bricks-postgres": "0.6.0", "string-byte-length": "3.0.0", "tiny-invariant": "1.3.1", diff --git a/packages/db/prisma/data-migrations/2024-02-19_attach-orphan-text.ts b/packages/db/prisma/data-migrations/2024-02-19_attach-orphan-text.ts new file mode 100644 index 00000000000..c2a79f1195a --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-19_attach-orphan-text.ts @@ -0,0 +1,81 @@ +import { prisma } from '~db/client' +import { isIdFor } from '~db/index' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-02-21_attach-orphan-text', + title: 'attach orphan text', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240221_attach_orphan_text = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + + const data = await prisma.freeText.findMany({ + where: { + AND: [ + { AttributeSupplement: null }, + { Organization: null }, + { OrgEmail: null }, + { OrgPhone: null }, + { OrgService: null }, + { OrgLocation: null }, + { OrgServiceName: null }, + { OrgWebsite: null }, + ], + }, + }) + await prisma.$transaction( + async (tx) => { + for (const item of data) { + const recordToAttachTo = item.key.split('.')[1] + if (typeof recordToAttachTo !== 'string') { + throw new Error('Unable to get record to attach to') + } + if (isIdFor('orgPhone', recordToAttachTo)) { + const result = await tx.orgPhone.update({ + where: { id: recordToAttachTo }, + data: { descriptionId: item.id }, + }) + log(`Attached orphan text ${item.key} to ${result.id}`) + } else if (isIdFor('orgEmail', recordToAttachTo)) { + const result = await tx.orgEmail.update({ + where: { id: recordToAttachTo }, + data: { descriptionId: item.id }, + }) + log(`Attached orphan text ${item.key} to ${result.id}`) + } + } + }, + { timeout: 180_000 } + ) + + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob diff --git a/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!load.ts b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!load.ts new file mode 100644 index 00000000000..374f0045acc --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!load.ts @@ -0,0 +1,113 @@ +/* eslint-disable node/no-process-env */ +import { JWT } from 'google-auth-library' +import { GoogleSpreadsheet } from 'google-spreadsheet' +import PQueue from 'p-queue' +import PRetry from 'p-retry' +import papa from 'papaparse' + +import fs from 'fs' +import path from 'path' + +const creds = JSON.parse(process.env.GOOGLE_SERVICE_ACCT_CREDS as string) +const scopes = ['https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive.file'] +const jwt = new JWT({ + email: creds.client_email, + key: creds.private_key, + scopes, +}) +const sheetID = '17Egecl5U8_o8Nx8qic5cUE7oD3A8__2KgXilz-7yoMU' + +const queue = new PQueue({ + concurrency: 1, + interval: 2250, + intervalCap: 1, + autoStart: false, + carryoverConcurrencyCount: true, +}) +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} +const main = async () => { + const wb = new GoogleSpreadsheet(sheetID, jwt) + await wb.loadInfo() + + const sheetsToGet = { + Orgs: 'organization', + Emails: 'orgEmail', + 'Access Instructions': 'svcAccess', + Phones: 'orgPhone', + Locations: 'orgLocation', + OrgSocial: 'orgSocial', + Services: 'orgService', + } + const joinsToGet = { + OrgServicePhone: 'orgServicePhone', + OrgServiceEmail: 'orgServiceEmail', + OrgLocationEmail: 'orgLocationEmail', + OrgLocationService: 'orgLocationService', + OrgLocationPhone: 'orgLocationPhone', + } + const data = {} + const joins = {} + + const getData = async (sheetName: string) => { + const sheet = wb.sheetsByTitle[sheetName] + console.log('Parsing', sheetName) + if (!sheet) throw new Error(`Sheet ${sheetName} not found in spreadsheet ${sheetID}`) + const csv = await sheet.downloadAsCSV() + const parsed = papa.parse(csv.toString(), { header: true, skipEmptyLines: 'greedy' }) + const dataName = sheetsToGet[sheetName] + console.log(sheetName, `returned ${parsed.data.length} rows`) + data[dataName] = parsed.data + } + + const getJoin = async (joinName: string) => { + const sheet = wb.sheetsByTitle[joinName] + console.log('Parsing', joinName) + if (!sheet) throw new Error(`Sheet ${joinName} not found in spreadsheet ${sheetID}`) + const csv = await sheet.downloadAsCSV() + const parsed = papa.parse(csv.toString(), { header: true, skipEmptyLines: true }) + console.log(joinName, `returned ${parsed.data.length} rows`) + const dataName = joinsToGet[joinName] + joins[dataName] = parsed.data + } + + for (const sheetName of Object.keys(sheetsToGet)) { + queue.add(async () => { + await PRetry(() => getData(sheetName), { + onFailedAttempt: async (err) => { + console.error(`[${err.attemptNumber}/${err.retriesLeft}] ${err.message} -- Trying again`) + // await sleep(5000) + }, + // factor: 3, + randomize: true, + }) + }) + } + queue.add(async () => { + console.log("Let google catch it's breath") + await sleep(5000) + }) + + for (const joinName of Object.keys(joinsToGet)) { + queue.add(async () => { + await PRetry(() => getJoin(joinName), { + onFailedAttempt: async (err) => { + console.error( + `[${err.attemptNumber}/${err.retriesLeft}] ${err.message} -- Trying again in 5 seconds` + ) + await sleep(5000) + }, + }) + }) + } + queue.add(() => { + console.log('writing data.json') + fs.writeFileSync(path.resolve(__dirname, 'load.json'), JSON.stringify(data)) + console.log('writing joins.json') + fs.writeFileSync(path.resolve(__dirname, 'joins.json'), JSON.stringify(joins)) + }) + queue.start() +} + +main() diff --git a/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!prep-single.ts b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!prep-single.ts new file mode 100644 index 00000000000..05440757bae --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!prep-single.ts @@ -0,0 +1,891 @@ +/* eslint-disable node/no-process-env */ +import compact from 'just-compact' +import { isSupportedCountry, parsePhoneNumberWithError } from 'libphonenumber-js' +import superjson from 'superjson' + +import fs from 'fs' +import path from 'path' + +import { socialParser } from '@weareinreach/util/social-parser' +import { type Prisma, prisma } from '~db/client' +import { generateNestedFreeText, generateNestedFreeTextUpsert } from '~db/lib/generateFreeText' +import { generateId, isIdFor } from '~db/lib/idGen' +import { generateUniqueSlug } from '~db/lib/slugGen' +import { JsonInputOrNull, accessInstructions as zAccessInstructions } from '~db/zod_util' + +import { DataFile, JoinFile } from './!schemas' + +const rawData = JSON.parse(fs.readFileSync(path.resolve(__dirname, '!load.json'), 'utf8')) +const rawJoins = JSON.parse(fs.readFileSync(path.resolve(__dirname, '!joins.json'), 'utf8')) + +const parsedData = DataFile.safeParse(rawData) +const parsedJoins = JoinFile.safeParse(rawJoins) + +export interface Output { + records: { + organization: Prisma.OrganizationUpsertArgs + orgLocation: Prisma.OrgLocationUpsertArgs[] + orgEmail: Prisma.OrgEmailUpsertArgs[] + orgPhone: Prisma.OrgPhoneUpsertArgs[] + orgService: Prisma.OrgServiceUpsertArgs[] + }[] + + handledSuggestions: Prisma.SuggestionUpdateManyArgs +} + +const handledSuggestions: string[] = [] + +const output: Output = { + records: [], + handledSuggestions: { + where: { organizationId: { in: handledSuggestions } }, + data: { handled: true }, + }, +} +const orgAttributes = [ + 'asylum-seekers', + 'bipoc-comm', + 'bipoc-led', + 'black-led', + 'gender-nc', + 'hiv-comm', + 'immigrant-comm', + 'immigrant-led', + 'lgbtq-youth-focus', + 'resettled-refugees', + 'spanish-speakers', + 'trans-comm', + 'trans-fem', + 'trans-led', + 'trans-masc', + 'trans-youth-focus', +] +const activeCountries = ['UM', 'US', 'MH', 'PW', 'AS', 'MX', 'CA', 'MP', 'GU', 'PR', 'VI'] + +const attributes = { + alertMessage: 'attr_01GYSVX1NAMR6RDV6M69H4KN3T', + serviceAccess: { + email: 'attr_01GW2HHFVKFM4TDY4QRK4AR2ZW', + phone: 'attr_01GW2HHFVMKTFWCKBVVFJ5GMY0', + file: 'attr_01GW2HHFVKMRHFD8SMDAZM3SSM', + link: 'attr_01GW2HHFVMYXMS8ARA3GE7HZFD', + }, + 'at-capacity': 'attr_01GW2HHFV3YJ2AWADHVKG79BQ0', + 'cost-fees': 'attr_01GW2HHFVGWKWB53HWAAHQ9AAZ', + 'cost-free': 'attr_01GW2HHFVGDTNW9PDQNXK6TF1T', + 'elig-age-min': 'attr_01GW2HHFVGSAZXGR4JAVHEK6ZC', + 'elig-age-max': 'attr_01GW2HHFVGSAZXGR4JAVHEK6ZC', + 'has-confidentiality-policy': 'attr_01GW2HHFV3BADK80TG0DXXFPMM', + 'lang-offered': 'attr_01GW2HHFVJ8K180CNX339BTXM2', + 'offers-remote-services': 'attr_01GW2HHFV5Q7XN2ZNTYFR1AD3M', + 'other-describe': 'attr_01GW2HHFVJDKVF1HV7559CNZCY', + 'req-medical-insurance': 'attr_01GW2HHFVH9DPBZ968VXGE50E7', + 'req-photo-id': 'attr_01GW2HHFVHZ599M48CMSPGDCSC', + 'req-proof-of-age': 'attr_01GW2HHFVH0GQK0GAJR5D952V3', + 'req-proof-of-income': 'attr_01GW2HHFVHEVX4PMNN077ASQMG', + 'req-referral': 'attr_01GW2HHFVJH8MADHYTHBV54CER', +} + +const serviceAttributes = { + boolean: [ + 'at-capacity', + 'cost-free', + 'has-confidentiality-policy', + 'offers-remote-services', + 'req-medical-insurance', + 'req-photo-id', + 'req-proof-of-age', + 'req-proof-of-income', + 'req-referral', + ], + cost: ['cost-fees'], + age: ['elig-age-max', 'elig-age-min'], + languages: ['lang-offered'], + text: ['other-describe'], + all: [ + 'at-capacity', + 'cost-free', + 'has-confidentiality-policy', + 'offers-remote-services', + 'req-medical-insurance', + 'req-photo-id', + 'req-proof-of-age', + 'req-proof-of-income', + 'req-referral', + 'cost-fees', + 'elig-age-max', + 'elig-age-min', + 'lang-offered', + 'other-describe', + ], +} as const +const zServAccess = zAccessInstructions.getAll() +const prep = async () => { + const attributes = await prisma.attribute.findMany({ select: { id: true, tag: true } }) + const attributeMap = new Map(attributes.map(({ id, tag }) => [tag, id])) + const countries = await prisma.country.findMany({ select: { id: true, cca2: true } }) + const countryMap = new Map(countries.map(({ cca2, id }) => [id, cca2])) + const govDist = await prisma.govDist.findMany({ + select: { id: true, abbrev: true }, + where: { isPrimary: true }, + }) + const govDistMap = new Map(govDist.map(({ abbrev, id }) => [id, abbrev])) + const existingOrgs = await prisma.organization.findMany() + const orgMap = new Map(existingOrgs.map(({ id, ...rest }) => [id, rest])) + const existingLocations = await prisma.orgLocation.findMany({ include: { serviceAreas: true } }) + const locationMap = new Map(existingLocations.map(({ id, ...rest }) => [id, rest])) + const geoCache = new Map() + if (fs.existsSync(path.resolve(__dirname, '!geocache.json'))) { + const cacheData = JSON.parse(fs.readFileSync(path.resolve(__dirname, '!geocache.json'), 'utf8')) as [ + string, + { lat: number; lon: number }, + ][] + if (Array.isArray(cacheData)) { + for (const [id, loc] of cacheData) { + geoCache.set(id, loc) + } + console.log(geoCache.size) + } + } + + const socialMediaServices = await prisma.socialMediaService.findMany({ select: { id: true, name: true } }) + const socialMediaMap = new Map(socialMediaServices.map(({ name, id }) => [name.toLowerCase(), id])) + + return { attributeMap, countryMap, govDistMap, orgMap, locationMap, geoCache, socialMediaMap } +} + +function throttleApiCalls(fn: () => Promise): () => Promise { + let count = 0 + const interval = 1000 // 1 second + + return async function apiCall() { + if (count >= 5) { + await new Promise((resolve) => setTimeout(resolve, interval)) + count = 0 + } + + count++ + return await fn() + } +} + +const run = async () => { + const { attributeMap, countryMap, govDistMap, orgMap, locationMap, geoCache, socialMediaMap } = await prep() + if (!parsedData.success || !parsedJoins.success) { + if (!parsedData.success) console.error(parsedData.error.format()) + if (!parsedJoins.success) console.error(parsedJoins.error.format()) + return + } + const data = parsedData.data + const joins = parsedJoins.data + + const idMap = new Map() + + for (const org of data.organization) { + if (org['reviewed?'] !== true) { + console.info(`Skipping ${org.Name} (${org.id}) --> Not ready for upload`) + continue + } + const existingOrgRecord = orgMap.get(org.id) + + const isNew = !isIdFor('organization', org.id) || !existingOrgRecord + const orgId = isNew ? generateId('organization') : org.id + console.info(`Processing ${org.id} -- ${org.Name}`) + if (!isNew) { + handledSuggestions.push(orgId) + } + idMap.set(org.id, orgId) + + const orgData: Prisma.OrganizationUpsertArgs['create'] = { + name: org.Name.trim(), + slug: existingOrgRecord?.slug ?? (await generateUniqueSlug({ name: org.Name.trim(), id: orgId })), + source: { connect: { id: 'srce_01GXD88N4X2XNE3DW0G1AZJ403' } }, + lastVerified: new Date(), + published: true, + } as const + + const record: Output['records'][number] = { + organization: { + where: { id: orgId }, + create: { id: orgId, ...orgData }, + update: orgData, + }, + orgLocation: [], + orgEmail: [], + orgPhone: [], + orgService: [], + } + const organizationAttributes: OrganizationAttributes = { + connectOrCreate: [], + upsert: [], + } + + if (org.Description) { + const descriptionId = existingOrgRecord?.descriptionId ?? generateId('freeText') + const generateFreetextArgs = { + orgId, + text: org.Description.trim(), + type: 'orgDesc', + freeTextId: descriptionId, + } as const + record.organization.create.description = generateNestedFreeText(generateFreetextArgs) + record.organization.update.description = generateNestedFreeTextUpsert(generateFreetextArgs) + } + if (org['Alert Message']) { + const existingAlert = await prisma.attributeSupplement.findFirst({ + where: { attributeId: attributes.alertMessage, organizationId: orgId }, + }) + const supplementId = existingAlert?.id ?? generateId('attributeSupplement') + const alertMessageArgs = { + orgId, + text: org['Alert Message'].trim(), + type: 'attSupp', + itemId: supplementId, + } as const + organizationAttributes.connectOrCreate.push({ + where: { id: supplementId }, + create: { + id: supplementId, + attribute: { connect: { id: attributes.alertMessage } }, + text: generateNestedFreeText(alertMessageArgs), + }, + }) + organizationAttributes.upsert.push({ + where: { id: supplementId }, + update: { text: generateNestedFreeTextUpsert(alertMessageArgs) }, + create: { + attribute: { connect: { id: attributes.alertMessage } }, + text: generateNestedFreeText(alertMessageArgs), + }, + }) + } + for (const attrib of orgAttributes) { + if (org[attrib]) { + const attributeId = attributeMap.get(attrib) + if (!attributeId) continue + const existingAttrib = await prisma.attributeSupplement.findFirst({ + where: { attributeId, organizationId: orgId }, + }) + const id = existingAttrib?.id ?? generateId('attributeSupplement') + const connectOrCreateArgs = { where: { id }, create: { id, attributeId } } as const + organizationAttributes.connectOrCreate.push(connectOrCreateArgs) + organizationAttributes.upsert.push({ + ...connectOrCreateArgs, + update: {}, + }) + } + } + record.organization.create.attributes = { connectOrCreate: organizationAttributes.connectOrCreate } + record.organization.update.attributes = { upsert: organizationAttributes.upsert } + if (org.URL) { + const existingWebsite = await prisma.orgWebsite.findFirst({ + where: { organizationId: orgId }, + }) + const id = existingWebsite?.id ?? generateId('orgWebsite') + record.organization.create.websites = { + create: { url: org.URL, id: generateId('orgWebsite') }, + } + record.organization.update.websites = { + upsert: { where: { id }, create: { id, url: org.URL }, update: { url: org.URL } }, + } + } + const socialMedias = data.orgSocial.filter(({ organizationId }) => organizationId === org.id) + if (socialMedias.length) { + record.organization.create.socialMedia = { + createMany: { + data: compact( + socialMedias.map(({ id, url, ...rest }) => { + const smId = isIdFor('orgSocialMedia', id) ? id : generateId('orgSocialMedia') + const service = socialParser.detectProfile(url) + if (!service) { + return + } + const username = socialParser.getProfileId(service, url) + const sanitizedUrl = socialParser.sanitize(service, url) + const serviceId = socialMediaMap.get(service) + if (!serviceId) { + throw new Error(`Social media service ${service} not found`) + } + return { id: smId, serviceId, url: sanitizedUrl, username, legacyId: id, published: true } + }) + ), + skipDuplicates: true, + }, + } + record.organization.update.socialMedia = { + upsert: compact( + socialMedias.map(({ id, url, ...rest }) => { + const smId = isIdFor('orgSocialMedia', id) ? id : generateId('orgSocialMedia') + const service = socialParser.detectProfile(url) + if (!service) { + return + } + const username = socialParser.getProfileId(service, url) + const serviceId = socialMediaMap.get(service) + const sanitizedUrl = socialParser.sanitize(service, url) + if (!serviceId) { + throw new Error(`Social media service ${service} not found`) + } + return { + where: { id: smId }, + create: { + id: smId, + legacyId: id, + serviceId, + url: sanitizedUrl, + username, + published: true, + }, + update: { + legacyId: id, + serviceId, + url: sanitizedUrl, + username, + published: true, + }, + } + }) + ), + } + } + const locations = data.orgLocation.filter(({ organizationId }) => organizationId === org.id) + const locationIds = locations.map(({ id }) => id) + + const services = data.orgService.filter(({ organizationId }) => organizationId === org.id) + const serviceIds = services.map(({ id }) => id) + + const emailIdsToProcess = [ + ...new Set( + joins.orgLocationEmail + .filter(({ locationId }) => locationIds.includes(locationId)) + .map(({ emailId }) => emailId) + .concat( + joins.orgServiceEmail + .filter(({ serviceId }) => serviceIds.includes(serviceId)) + .map(({ emailId }) => emailId) + ) + ), + ] + const phoneIdsToProcess = [ + ...new Set( + joins.orgLocationPhone + .filter(({ locationId }) => locationIds.includes(locationId)) + .map(({ phoneId }) => phoneId) + .concat( + joins.orgServicePhone + .filter(({ serviceId }) => serviceIds.includes(serviceId)) + .map(({ phoneId }) => phoneId) + ) + ), + ] + const serviceIdsToProcess = [ + ...new Set( + joins.orgLocationService + .filter(({ locationId }) => locationIds.includes(locationId)) + .map(({ serviceId }) => serviceId) + ), + ] + for (const legacyId of emailIdsToProcess) { + const email = data.orgEmail.find(({ id }) => id === legacyId) + if (!email) { + console.error(`Cannot locate email record ${legacyId}`) + continue + } + const existingRecord = await prisma.orgEmail.findFirst({ + where: { OR: [{ id: email.id }, { legacyId: email.id }] }, + }) + const emailId = isIdFor('orgEmail', email.id) ? email.id : existingRecord?.id ?? generateId('orgEmail') + idMap.set(legacyId, emailId) + const emailDescArgs = { + orgId, + type: 'emailDesc', + itemId: emailId, + text: email.description!, + freeTextId: existingRecord?.descriptionId, + } as const + record.orgEmail.push({ + where: { id: emailId }, + create: { + id: emailId, + legacyId, + email: email.email, + description: email.description ? generateNestedFreeText(emailDescArgs) : undefined, + }, + update: { + legacyId, + email: email.email, + description: email.description ? generateNestedFreeTextUpsert(emailDescArgs) : undefined, + }, + }) + } + for (const legacyId of phoneIdsToProcess) { + const phone = data.orgPhone.find(({ id }) => id === legacyId) + if (!phone) { + console.error(`Cannot locate phone record ${legacyId}`) + continue + } + const existingRecord = await prisma.orgPhone.findFirst({ + where: { OR: [{ id: phone.id }, { legacyId: phone.id }] }, + }) + const phoneId = isIdFor('orgPhone', phone.id) ? phone.id : existingRecord?.id ?? generateId('orgPhone') + idMap.set(legacyId, phoneId) + const phoneDescArgs = { + orgId, + type: 'phoneDesc', + itemId: phoneId, + text: phone.description, + freeTextId: existingRecord?.descriptionId, + } as const + const locationLinkCandidate = data.orgLocation.find(({ id }) => { + const { locationId } = joins.orgLocationPhone.find(({ phoneId }) => phoneId === phone.id) ?? {} + return locationId === id + }) + + const cca2val = locationLinkCandidate?.Country ?? 'US' + const countrycode = isSupportedCountry(cca2val) ? cca2val : 'US' + const parsedPhone = parsePhoneNumberWithError( + compact([phone.number, phone.ext]).join(' ').trim(), + countrycode + ) + record.orgPhone.push({ + where: { id: phoneId }, + create: { + id: phoneId, + legacyId, + number: parsedPhone.nationalNumber, + ext: parsedPhone.ext, + country: { connect: { cca2: countrycode } }, + description: phone.description ? generateNestedFreeText(phoneDescArgs) : undefined, + }, + update: { + legacyId, + number: parsedPhone.nationalNumber, + ext: parsedPhone.ext, + country: { connect: { cca2: countrycode } }, + description: phone.description ? generateNestedFreeTextUpsert(phoneDescArgs) : undefined, + }, + }) + } + for (const legacyId of serviceIdsToProcess) { + const service = data.orgService.find(({ id }) => id === legacyId) + if (!service) { + console.error(`Cannot locate service record ${legacyId}`) + continue + } + const existingRecord = await prisma.orgService.findFirst({ + where: { OR: [{ id: service.id }, { legacyId: service.id }] }, + }) + const serviceId = isIdFor('orgService', service.id) + ? service.id + : existingRecord?.id ?? generateId('orgService') + idMap.set(legacyId, serviceId) + const serviceNameArgs = { + orgId, + type: 'svcName', + itemId: serviceId, + text: service.Title, + freeTextId: existingRecord?.serviceNameId, + } as const + const serviceDescArgs = { + orgId, + type: 'svcDesc', + itemId: serviceId, + text: service.Description, + freeTextId: existingRecord?.descriptionId, + } as const + + const generateAttribRecords = async (): Promise<{ + create: Prisma.AttributeSupplementCreateNestedManyWithoutServiceInput + update: Prisma.AttributeSupplementUpdateManyWithoutServiceNestedInput + }> => { + const connectOrCreate: Prisma.AttributeSupplementCreateNestedManyWithoutServiceInput['connectOrCreate'] = + [] + const upsert: Prisma.AttributeSupplementUpdateManyWithoutServiceNestedInput['upsert'] = [] + + for (const tag of serviceAttributes.all) { + if (Object.keys(service).includes(tag) && service[tag]) { + const attributeId = attributes[tag] + if (!attributeId) throw new Error(`Unknown attribute -> ${tag}`) + + const existingRecord = await prisma.attributeSupplement.findFirst({ + where: { + attributeId, + serviceId, + }, + }) + const supplementId = existingRecord?.id ?? generateId('attributeSupplement') + const where = { id: supplementId } + + switch (tag) { + case 'other-describe': + case 'cost-fees': { + const content = service[tag] + if (typeof content !== 'string') break + const freeTextArgs = { + orgId, + type: 'attSupp', + itemId: supplementId, + text: content, + freeTextId: existingRecord?.textId, + } as const + + const create = { + id: supplementId, + attribute: { connect: { id: attributeId } }, + text: generateNestedFreeText(freeTextArgs), + } + connectOrCreate.push({ + where, + create, + }) + upsert.push({ + where, + create, + update: { + text: generateNestedFreeTextUpsert(freeTextArgs), + }, + }) + break + } + case 'elig-age-max': + case 'elig-age-min': { + const data = JsonInputOrNull.parse( + superjson.serialize({ + ...(service['elig-age-min'] ? { min: service['elig-age-min'] } : {}), + ...(service['elig-age-max'] ? { max: service['elig-age-max'] } : {}), + }) + ) + const create = { + id: supplementId, + attribute: { connect: { id: attributeId } }, + data, + } + connectOrCreate.push({ + where, + create, + }) + upsert.push({ + where, + create, + update: { + data, + }, + }) + + break + } + case 'lang-offered': { + if (!service['lang-offered']) break + const langs = service['lang-offered'] + for (const langId of langs) { + const create = { + id: supplementId, + attribute: { connect: { id: attributeId } }, + language: { connect: { id: langId } }, + } + connectOrCreate.push({ + where, + create, + }) + upsert.push({ + where, + create, + update: { + language: { connect: { id: langId } }, + }, + }) + } + break + } + default: { + const create = { + id: supplementId, + attribute: { connect: { id: attributeId } }, + } + connectOrCreate.push({ + where, + create, + }) + upsert.push({ + where, + create, + update: { + attribute: { connect: { id: attributeId } }, + }, + }) + } + } + } + } + const serviceAccessToAdd = data.svcAccess.filter(({ serviceId }) => serviceId === service.id) + for (const { type: accessType, value } of serviceAccessToAdd) { + if (accessType === '') continue + const attributeId = attributes.serviceAccess[accessType] + const existingRecord = await prisma.attributeSupplement.findFirst({ + where: { + attributeId, + serviceId, + }, + }) + const supplementId = existingRecord?.id ?? generateId('attributeSupplement') + const where = { id: supplementId } + const data = JsonInputOrNull.parse( + superjson.serialize( + zServAccess.parse({ + access_type: accessType, + access_value: value, + }) + ) + ) + const create = { + id: supplementId, + attribute: { connect: { id: attributeId } }, + data, + } + connectOrCreate.push({ + where, + create, + }) + upsert.push({ + where, + create, + update: { + attribute: { connect: { id: attributeId } }, + data, + }, + }) + } + return { + create: { connectOrCreate }, + update: { upsert }, + } + } + const attributeRecords = await generateAttribRecords() + const servicePhones = compact( + joins.orgServicePhone + .filter(({ serviceId }) => serviceId === service.id) + .map(({ phoneId }) => idMap.get(phoneId)) + ) + const serviceEmails = compact( + joins.orgServiceEmail + .filter(({ serviceId }) => serviceId === service.id) + .map(({ emailId }) => idMap.get(emailId)) + ) + + record.orgService.push({ + where: { id: serviceId }, + create: { + id: serviceId, + legacyId, + serviceName: generateNestedFreeText(serviceNameArgs), + description: generateNestedFreeText(serviceDescArgs), + organization: { connect: { id: orgId } }, + published: true, + services: service['Tag(s)']?.length + ? { createMany: { data: service['Tag(s)']?.map(({ tag: tagId }) => ({ tagId })) } } + : undefined, + attributes: attributeRecords.create, + emails: { + connectOrCreate: serviceEmails.map((orgEmailId) => ({ + where: { orgEmailId_serviceId: { orgEmailId, serviceId } }, + create: { orgEmailId }, + })), + }, + phones: { + connectOrCreate: servicePhones.map((orgPhoneId) => ({ + where: { orgPhoneId_serviceId: { orgPhoneId, serviceId } }, + create: { orgPhoneId }, + })), + }, + }, + update: { + legacyId, + serviceName: generateNestedFreeTextUpsert(serviceNameArgs), + description: generateNestedFreeTextUpsert(serviceDescArgs), + organization: { connect: { id: orgId } }, + published: true, + services: service['Tag(s)']?.length + ? { + upsert: service['Tag(s)'].map(({ tag: tagId }) => ({ + where: { serviceId_tagId: { serviceId, tagId } }, + create: { tagId }, + update: { tagId }, + })), + } + : undefined, + emails: { + connectOrCreate: serviceEmails.map((orgEmailId) => ({ + where: { orgEmailId_serviceId: { orgEmailId, serviceId } }, + create: { orgEmailId }, + })), + }, + phones: { + connectOrCreate: servicePhones.map((orgPhoneId) => ({ + where: { orgPhoneId_serviceId: { orgPhoneId, serviceId } }, + create: { orgPhoneId }, + })), + }, + attributes: attributeRecords.update, + }, + }) + } + + for (const loc of locations) { + const existingLocationRecord = locationMap.get(loc.id) + const orgLocationId = isIdFor('orgLocation', loc.id) ? loc.id : generateId('orgLocation') + + const locationEmails = compact( + joins.orgLocationEmail + .filter(({ locationId }) => locationId === loc.id) + .map(({ emailId }) => idMap.get(emailId)) + ) + const locationPhones = compact( + joins.orgLocationPhone + .filter(({ locationId }) => locationId === loc.id) + .map(({ phoneId }) => idMap.get(phoneId)) + ) + const locationServices = compact( + joins.orgLocationService + .filter(({ locationId }) => locationId === loc.id) + .map(({ serviceId }) => idMap.get(serviceId)) + ) + + const locDataCreate: Prisma.OrgLocationUpsertArgs['create'] = { + id: orgLocationId, + orgId, + name: loc['Location Name'].trim(), + street1: loc.Street, + city: loc.City?.trim() ?? '', + countryId: loc.Country, + govDistId: loc.State, + postCode: loc.PostalCode, + notVisitable: loc['Hide Location?'], + mapCityOnly: loc['Hide Location?'], + phones: { + connectOrCreate: locationPhones.map((phoneId) => ({ + where: { orgLocationId_phoneId: { orgLocationId, phoneId } }, + create: { phoneId }, + })), + }, + emails: { + connectOrCreate: locationEmails.map((orgEmailId) => ({ + where: { orgEmailId_orgLocationId: { orgEmailId, orgLocationId } }, + create: { orgEmailId }, + })), + }, + services: { + connectOrCreate: locationServices.map((serviceId) => ({ + where: { orgLocationId_serviceId: { orgLocationId, serviceId } }, + create: { serviceId }, + })), + }, + } + const locDataUpdate: Prisma.OrgLocationUpsertArgs['update'] = { + ...locDataCreate, + id: undefined, + } + + const cca2 = countryMap.get(loc.Country) + const govDistAbbrev = govDistMap.get(loc.State ?? '') + if (loc.City && cca2) { + const searchString = compact([ + locDataCreate.street1, + locDataCreate.city, + govDistAbbrev, + locDataCreate.postCode, + cca2, + ]).join(', ') + + const searchParams = new URLSearchParams({ + text: searchString, + format: 'json', + apiKey: process.env.GEOAPIFY_API_KEY as string, + filter: `countrycode:${activeCountries.join(',').toLowerCase()}`, + }).toString() + const cachedResult = geoCache.get(searchParams) + if (cachedResult?.lat && cachedResult?.lon) { + locDataCreate.latitude = cachedResult.lat + locDataCreate.longitude = cachedResult.lon + locDataUpdate.latitude = cachedResult.lat + locDataUpdate.longitude = cachedResult.lon + } else { + const geoURL = `https://api.geoapify.com/v1/geocode/search?${searchParams}` + const geoResponse = await throttleApiCalls(async () => await fetch(geoURL))() + const geoData = await geoResponse.json() + const geoResult = geoData.results.length ? geoData.results[0] : null + locDataCreate.latitude = geoResult?.lat + locDataCreate.longitude = geoResult?.lon + locDataUpdate.latitude = geoResult?.lat + locDataUpdate.longitude = geoResult?.lon + if (geoResult?.lat && geoResult?.lon) { + geoCache.set(searchParams, { lat: geoResult.lat, lon: geoResult.lon }) + } + } + + if (loc['Service Area Coverage - State(s)'] || loc['Service Area Coverage - USA National']) { + const serviceAreaId = existingLocationRecord?.serviceAreas?.id ?? generateId('serviceArea') + + const countriesToAttach = (loc['Service Area Coverage - USA National'] ?? []).map((country) => ({ + countryId: country.trim(), + })) + const govDistsToAttach = (loc['Service Area Coverage - State(s)'] ?? []).map((govDist) => ({ + govDistId: govDist.trim(), + })) + const serviceAreaCreate = { + create: { + id: serviceAreaId, + countries: { + createMany: { + data: countriesToAttach, + skipDuplicates: true, + }, + }, + districts: { + createMany: { + data: govDistsToAttach, + skipDuplicates: true, + }, + }, + }, + } as const + locDataCreate.serviceAreas = serviceAreaCreate + locDataUpdate.serviceAreas = { + upsert: { + create: serviceAreaCreate.create, + update: { + countries: { + set: countriesToAttach.map(({ countryId }) => ({ + serviceAreaId_countryId: { countryId, serviceAreaId }, + })), + }, + districts: { + set: govDistsToAttach.map(({ govDistId }) => ({ + serviceAreaId_govDistId: { govDistId, serviceAreaId }, + })), + }, + }, + }, + } + } + } + + const orgLocationRecord: Prisma.OrgLocationUpsertArgs = { + where: { id: orgLocationId }, + create: locDataCreate, + update: locDataUpdate, + } + record.orgLocation.push(orgLocationRecord) + } + output.records.push(record) + } + console.log(geoCache.size) + fs.writeFileSync(path.resolve(__dirname, '!geocache.json'), JSON.stringify([...geoCache.entries()])) + fs.writeFileSync(path.resolve(__dirname, '!data.json'), JSON.stringify(output)) +} + +run() + +type OrganizationAttributes = { + connectOrCreate: Prisma.AttributeSupplementCreateOrConnectWithoutOrganizationInput[] + upsert: Prisma.AttributeSupplementUpsertWithWhereUniqueWithoutOrganizationInput[] +} diff --git a/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!schemas.ts b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!schemas.ts new file mode 100644 index 00000000000..0467be15f52 --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/!schemas.ts @@ -0,0 +1,169 @@ +import { string, z } from 'zod' + +const stripEmptyString = (val?: string) => (typeof val === 'string' && val === '' ? undefined : val) +const boolOrBlank = z.enum(['FALSE', 'TRUE', '']).transform((val) => (val === 'TRUE' ? true : false)) + +const stringToArray = (val?: string) => + (typeof val === 'string' && val === '') || val === undefined + ? undefined + : val.split(',').map((x) => x.trim()) + +const separateServiceTags = (val?: string) => { + const arr = stringToArray(val) + if (!arr) return undefined + const output: { category: string; tag: string }[] = [] + for (const item of arr) { + const [category, tag] = item.split(':') + if (typeof category === 'string' && typeof tag === 'string') { + output.push({ category, tag }) + } + } + return output +} +const coerceNumber = (val?: string) => { + const stripped = stripEmptyString(val) + if (stripped) { + return parseInt(stripped) + } + return undefined +} +export const DataSchema = { + Organization: z.object({ + id: z.string(), + Name: z.string(), + URL: z.string().optional().transform(stripEmptyString), + Description: z.string(), + 'Alert Message': z.string().optional().transform(stripEmptyString), + 'bipoc-led': boolOrBlank, + 'black-led': boolOrBlank, + 'bipoc-comm': boolOrBlank, + 'immigrant-led': boolOrBlank, + 'immigrant-comm': boolOrBlank, + 'asylum-seekers': boolOrBlank, + 'resettled-refugees': boolOrBlank, + 'trans-led': boolOrBlank, + 'trans-comm': boolOrBlank, + 'trans-youth-focus': boolOrBlank, + 'trans-masc': boolOrBlank, + 'trans-fem': boolOrBlank, + 'gender-nc': boolOrBlank, + 'lgbtq-youth-focus': boolOrBlank, + 'spanish-speakers': boolOrBlank, + 'hiv-comm': boolOrBlank, + 'Additional Notes': z.string().optional().transform(stripEmptyString), + 'reviewed?': boolOrBlank, + }), + + OrgEmail: z.object({ + id: z.string(), + firstName: z.string().optional().transform(stripEmptyString), + lastName: z.string().optional().transform(stripEmptyString), + primary: boolOrBlank, + email: z.string().email(), + description: z.string().optional().transform(stripEmptyString), + organizationId: z.string(), + locationOnly: boolOrBlank, + serviceOnly: boolOrBlank, + }), + + SvcAccess: z.object({ + id: z.string(), + serviceId: z.string(), + type: z.enum(['email', 'phone', 'file', 'link', '']), + value: z.string(), + }), + OrgPhone: z.object({ + id: z.string(), + number: z.string(), + ext: z.string().optional().transform(stripEmptyString), + primary: boolOrBlank, + countryId: z.string().optional().transform(stripEmptyString), + description: z.string(), + organizationId: z.string(), + }), + OrgLocation: z.object({ + id: z.string(), + organizationId: z.string(), + 'Location Name': z.string(), + Country: z.string(), + Street: z.string().optional().transform(stripEmptyString), + City: z.string().optional().transform(stripEmptyString), + State: z.string().optional().transform(stripEmptyString), + PostalCode: z.string().optional().transform(stripEmptyString), + 'Hide Location?': boolOrBlank, + 'Service Area Coverage - USA National': z + .string() + .optional() + .transform((val) => stringToArray(val)), + 'Service Area Coverage - State(s)': z + .string() + .optional() + .transform((val) => stringToArray(val)), + }), + OrgSocial: z.object({ + id: z.string(), + organizationId: z.string(), + service: z.string(), + url: z.string().url(), + }), + OrgService: z.object({ + id: z.string(), + organizationId: z.string(), + Title: z.string(), + Description: z.string(), + 'Tag(s)': z.string().optional().transform(separateServiceTags), + 'other-describe': z.string().optional().transform(stripEmptyString), + 'elig-age-min': z.string().optional().transform(coerceNumber), + 'elig-age-max': z.string().optional().transform(coerceNumber), + 'cost-free': boolOrBlank, + 'cost-fees': z.string().optional().transform(coerceNumber), + 'lang-offered': z.string().optional().transform(stringToArray), + 'has-confidentiality-policy': boolOrBlank, + 'offers-remote-services': boolOrBlank, + 'req-medical-insurance': boolOrBlank, + 'req-photo-id': boolOrBlank, + 'req-proof-of-age': boolOrBlank, + 'req-proof-of-income': boolOrBlank, + 'req-referral': boolOrBlank, + 'at-capacity': boolOrBlank, + }), +} +export const DataFile = z.object({ + organization: DataSchema.Organization.array(), + orgEmail: DataSchema.OrgEmail.array(), + svcAccess: DataSchema.SvcAccess.array(), + orgPhone: DataSchema.OrgPhone.array(), + orgLocation: DataSchema.OrgLocation.array(), + orgSocial: DataSchema.OrgSocial.array(), + orgService: DataSchema.OrgService.array(), +}) + +export const JoinSchema = { + OrgServicePhone: z.object({ + serviceId: z.string(), + phoneId: z.string(), + }), + OrgServiceEmail: z.object({ + serviceId: z.string(), + emailId: z.string(), + }), + OrgLocationEmail: z.object({ + locationId: z.string(), + emailId: z.string(), + }), + OrgLocationService: z.object({ + locationId: z.string(), + serviceId: z.string(), + }), + OrgLocationPhone: z.object({ + locationId: z.string(), + phoneId: z.string(), + }), +} +export const JoinFile = z.object({ + orgServicePhone: JoinSchema.OrgServicePhone.array(), + orgServiceEmail: JoinSchema.OrgServiceEmail.array(), + orgLocationEmail: JoinSchema.OrgLocationEmail.array(), + orgLocationService: JoinSchema.OrgLocationService.array(), + orgLocationPhone: JoinSchema.OrgLocationPhone.array(), +}) diff --git a/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/index.ts b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/index.ts new file mode 100644 index 00000000000..85bc856e00e --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-20_appsheet-load/index.ts @@ -0,0 +1,98 @@ +import { prisma } from '~db/client' +import { downloadFromDatastore, formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' + +import { type Output } from './!prep-single' +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-02-20_appsheet-load', + title: 'appsheet load', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240220_appsheet_load = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + + log(`Downloading data from datastore`) + const data = (await downloadFromDatastore('migrations/2024-02-20_appsheet-load/data.json', log)) as Output + + await prisma.$transaction( + async (tx) => { + let i = 1 + const total = data.records.length + for (const record of data.records) { + log(`[${i}/${total}] Upserting records for ${record.organization.create.name}`, 'info') + const counts = { + emails: 0, + phones: 0, + services: 0, + locations: 0, + organizations: 0, + } + const org = await tx.organization.upsert(record.organization) + if (org) counts.organizations++ + for (const email of record.orgEmail) { + log(`Upserting email ${email.create.email}`, 'update', true) + const result = await tx.orgEmail.upsert(email) + if (result) counts.emails++ + } + for (const phone of record.orgPhone) { + log(`Upserting phone ${phone.create.number}`, 'update', true) + const result = await tx.orgPhone.upsert(phone) + if (result) counts.phones++ + } + for (const service of record.orgService) { + log( + `Upserting service ${service.create.serviceName?.create?.tsKey?.create?.text}`, + 'update', + true + ) + const result = await tx.orgService.upsert(service) + if (result) counts.services++ + } + for (const location of record.orgLocation) { + log(`Upserting location ${location.create.name}`, 'update', true) + const result = await tx.orgLocation.upsert(location) + if (result) counts.locations++ + } + log( + `Processed -> Organizations: ${counts.organizations} Emails: ${counts.emails}/${record.orgEmail.length} Phones: ${counts.phones}/${record.orgPhone.length} Services: ${counts.services}/${record.orgService.length} Locations: ${counts.locations}/${record.orgLocation.length}`, + 'info', + true + ) + i++ + } + }, + { timeout: 600_000 } + ) + + const handledSuggestions = await prisma.suggestion.updateMany(data.handledSuggestions) + log(`Marked ${handledSuggestions.count} suggestions as 'handled'`) + + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob diff --git a/packages/db/prisma/data-migrations/2024-02-23_add-missing-website.ts b/packages/db/prisma/data-migrations/2024-02-23_add-missing-website.ts new file mode 100644 index 00000000000..a04a52d27f8 --- /dev/null +++ b/packages/db/prisma/data-migrations/2024-02-23_add-missing-website.ts @@ -0,0 +1,64 @@ +import { prisma } from '~db/client' +import { formatMessage } from '~db/prisma/common' +import { type MigrationJob } from '~db/prisma/dataMigrationRunner' +import { createLogger, type JobDef, jobPostRunner } from '~db/prisma/jobPreRun' + +/** Define the job metadata here. */ +const jobDef: JobDef = { + jobId: '2024-02-23_add-missing-website', + title: 'add missing website', + createdBy: 'Joe Karow', + /** Optional: Longer description for the job */ + description: undefined, +} +/** + * Job export - this variable MUST be UNIQUE + */ +export const job20240223_add_missing_website = { + title: `[${jobDef.jobId}] ${jobDef.title}`, + task: async (_ctx, task) => { + /** Create logging instance */ + createLogger(task, jobDef.jobId) + const log = (...args: Parameters) => (task.output = formatMessage(...args)) + /** + * Start defining your data migration from here. + * + * To log output, use `task.output = 'Message to log'` + * + * This will be written to `stdout` and to a log file in `/prisma/migration-logs/` + */ + + // Do stuff + const brsWebsite = await prisma.orgWebsite.createMany({ + data: [ + { + id: 'oweb_01HQBST3HJP9A2ETHEXA30CMMP', + url: 'https://www.blackremoteshe.com', + organizationId: 'orgn_01HQBG00A6K7XC9XFABDAA698T', + published: true, + }, + ], + skipDuplicates: true, + }) + log(`Added ${brsWebsite.count} website(s)`) + const ldBadge = await prisma.attributeSupplement.createMany({ + data: [ + { + id: 'atts_01HQBSW9GN0Z01Q6RADYTBK5SW', + attributeId: 'attr_01GW2HHFVN3JX2J7REFFT5NAMS', + active: true, + organizationId: 'orgn_01GVH3V4D6Q35GK0T6F3GADX2E', + }, + ], + }) + log(`Added ${ldBadge.count} badge(s)`) + + /** + * DO NOT REMOVE BELOW + * + * This writes a record to the DB to register that this migration has run successfully. + */ + await jobPostRunner(jobDef) + }, + def: jobDef, +} satisfies MigrationJob diff --git a/packages/db/prisma/data-migrations/index.ts b/packages/db/prisma/data-migrations/index.ts index 31638c79d78..ea5203ea2fe 100644 --- a/packages/db/prisma/data-migrations/index.ts +++ b/packages/db/prisma/data-migrations/index.ts @@ -3,4 +3,7 @@ export * from './2024-01-31_fix-attr-supp-json/index' export * from './2024-01-31_target-population-attrib' export * from './2024-02-01_add-missing-attributes/index' export * from './2024-02-02_deactivate-incompatible-attribs' +export * from './2024-02-19_attach-orphan-text' +export * from './2024-02-20_appsheet-load/index' +export * from './2024-02-23_add-missing-website' // codegen:end diff --git a/packages/util/package.json b/packages/util/package.json index 052416a3501..badadb5d660 100644 --- a/packages/util/package.json +++ b/packages/util/package.json @@ -15,6 +15,7 @@ "dependencies": { "@vercel/edge-config": "1.0.2", "luxon": "3.4.4", + "social-links": "1.14.0", "superjson": "2.2.1", "tslog": "4.9.2" }, diff --git a/packages/util/social-parser/index.ts b/packages/util/social-parser/index.ts new file mode 100644 index 00000000000..3438eba1c0f --- /dev/null +++ b/packages/util/social-parser/index.ts @@ -0,0 +1,21 @@ +import { PREDEFINED_PROFILES, SocialLinks } from 'social-links' + +const socialParser = new SocialLinks() + +for (const profile of PREDEFINED_PROFILES) { + if (profile.name === 'youtube') { + profile.matches.push( + { + match: '(https?://)?(www.)?youtube.com/channel/({PROFILE_ID})(?:/|/.*)?', + group: 3, + }, + { + match: '(https?://)?(www.)?youtube.com/user/({PROFILE_ID})(?:/|/.*)?', + group: 3, + } + ) + } + socialParser.addProfile(profile.name, profile.matches) +} + +export { socialParser } diff --git a/patches/social-links@1.14.0.patch b/patches/social-links@1.14.0.patch new file mode 100644 index 00000000000..16783a528fb --- /dev/null +++ b/patches/social-links@1.14.0.patch @@ -0,0 +1,117 @@ +diff --git a/lib/main.js b/lib/main.js +index 014539859aafd7103da98b65e31c60bcaf74750f..59a64dfd7c7b4061600ab1f0aea10207a60a5487 100644 +--- a/lib/main.js ++++ b/lib/main.js +@@ -55,7 +55,7 @@ var profiles_1 = require("./profiles/"); + Object.defineProperty(exports, "PREDEFINED_PROFILES", { enumerable: true, get: function () { return profiles_1.PREDEFINED_PROFILES; } }); + var types_1 = require("./types"); + __exportStar(require("./types"), exports); +-var PROFILE_ID = '[A-Za-z0-9_\\-\\.]+'; ++var PROFILE_ID = '[A-Za-z0-9_\\-\\.%]+'; + var QUERY_PARAM = '(\\?.*)?'; + var createRegexp = function (profileMatch, config) { + var str = profileMatch.match.replace('{PROFILE_ID}', "".concat(PROFILE_ID)); +diff --git a/lib/main.js.map b/lib/main.js.map +index aed8ca09ab0e714984e3d9ec667617a40b848d57..f797ba5814c0053ec01910a758ec919411adcc5f 100644 +--- a/lib/main.js.map ++++ b/lib/main.js.map +@@ -1 +1 @@ +-{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"} +\ No newline at end of file +diff --git a/lib/profiles/linkedin.d.ts.map b/lib/profiles/linkedin.d.ts.map +index 45ba22c07e1ecd3ba333cf9bc20e6a41eb8e9747..c0bd793b4bf61f472113af6fcf4347a292666065 100644 +--- a/lib/profiles/linkedin.d.ts.map ++++ b/lib/profiles/linkedin.d.ts.map +@@ -1 +1 @@ +-{"version":3,"file":"linkedin.d.ts","sourceRoot":"","sources":["../../src/profiles/linkedin.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;CAapB,CAAC"} +\ No newline at end of file ++{"version":3,"file":"linkedin.d.ts","sourceRoot":"","sources":["../../src/profiles/linkedin.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,QAAQ;;;;;;;;;;;;;CAmBpB,CAAC"} +\ No newline at end of file +diff --git a/lib/profiles/linkedin.js b/lib/profiles/linkedin.js +index 17bda9bb9fad300231293a84410d524158a10dd3..62684a88128715bd1f20ee8c2120f199dc5fd8d6 100644 +--- a/lib/profiles/linkedin.js ++++ b/lib/profiles/linkedin.js +@@ -14,6 +14,12 @@ exports.linkedin = { + pattern: 'https://linkedin.com/mwlite/in/{PROFILE_ID}' + }, + { match: '({PROFILE_ID})', group: 1 }, ++ { ++ match: '(https?://)?([a-z]{2,3}.)?linkedin.com/company/({PROFILE_ID})/?', ++ group: 3, ++ type: types_1.TYPE_DESKTOP, ++ pattern: 'https://linkedin.com/company/{PROFILE_ID}', ++ } + ] + }; + //# sourceMappingURL=linkedin.js.map +\ No newline at end of file +diff --git a/lib/profiles/linkedin.js.map b/lib/profiles/linkedin.js.map +index 19b1c0f38d3a56fded626be46d665160ba917e65..80871b4b2a6cc5234dddb6686f24236b48ded908 100644 +--- a/lib/profiles/linkedin.js.map ++++ b/lib/profiles/linkedin.js.map +@@ -1 +1 @@ +-{"version":3,"file":"linkedin.js","sourceRoot":"","sources":["../../src/profiles/linkedin.ts"],"names":[],"mappings":";;;AACA,kCAAqD;AAExC,QAAA,QAAQ,GAAG;IACtB,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE;QACP;YACE,KAAK,EAAE,4DAA4D,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,oBAAY;YACjG,OAAO,EAAE,sCAAsC;SAChD;QACD;YACE,KAAK,EAAE,mEAAmE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,mBAAW;YACvG,OAAO,EAAE,6CAA6C;SACvD;QACD,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE;KACtC;CACF,CAAC"} +\ No newline at end of file ++{"version":3,"file":"linkedin.js","sourceRoot":"","sources":["../../src/profiles/linkedin.ts"],"names":[],"mappings":";;;AACA,kCAAqD;AAExC,QAAA,QAAQ,GAAG;IACtB,IAAI,EAAE,UAAU;IAChB,OAAO,EAAE;QACP;YACE,KAAK,EAAE,4DAA4D,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,oBAAY;YACjG,OAAO,EAAE,sCAAsC;SAChD;QACD;YACE,KAAK,EAAE,mEAAmE,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,mBAAW;YACvG,OAAO,EAAE,6CAA6C;SACvD;QACD,EAAE,KAAK,EAAE,gBAAgB,EAAE,KAAK,EAAE,CAAC,EAAE;QACrC;YACD,KAAK,EAAE,iEAAiE;YACxE,KAAK,EAAE,CAAC;YACR,IAAI,EAAE,oBAAY;YAClB,OAAO,EAAE,2CAA2C;SACpD;KACA;CACF,CAAC"} +\ No newline at end of file +diff --git a/lib/profiles/linkedin.spec.js b/lib/profiles/linkedin.spec.js +index 710f4c9274e81a9daba69abfe2198a30917c6b02..b90e456a47eeefd29ac0e1e972283b5769362072 100644 +--- a/lib/profiles/linkedin.spec.js ++++ b/lib/profiles/linkedin.spec.js +@@ -6,18 +6,21 @@ describe('PROFILE: linkedin', function () { + beforeEach(function () { + sl = new main_1.SocialLinks(); + }); +- var testProfile = function (profile, profileId, desktop, mobile) { ++ var testProfile = function (profile, profileId, desktop, mobile, company) { + expect(sl.hasProfile(profile)).toBeTruthy(); + expect(sl.isValid(profile, desktop)).toBeTruthy(); + expect(sl.isValid(profile, mobile)).toBeTruthy(); ++ expect(sl.isValid(profile, company)).toBeTruthy(); + expect(sl.getProfileId(profile, desktop)).toBe(profileId); + expect(sl.getProfileId(profile, mobile)).toBe(profileId); ++ expect(sl.getProfileId(profile, company)).toBe(profileId); + expect(sl.getLink(profile, profileId)).toBe(desktop); + expect(sl.getLink(profile, profileId, main_1.TYPE_DESKTOP)).toBe(desktop); + expect(sl.getLink(profile, profileId, main_1.TYPE_MOBILE)).toBe(mobile); + expect(sl.sanitize(profile, desktop)).toBe(desktop); + expect(sl.sanitize(profile, desktop, main_1.TYPE_DESKTOP)).toBe(desktop); + expect(sl.sanitize(profile, mobile, main_1.TYPE_MOBILE)).toBe(mobile); ++ expect(sl.sanitize(profile, company, main_1.TYPE_DESKTOP)).toBe(desktop); + }; + var testProfileDesktop = function (profile, profileId, desktop) { + expect(sl.hasProfile(profile)).toBeTruthy(); +@@ -33,7 +36,8 @@ describe('PROFILE: linkedin', function () { + var profileId = 'gkucmierz'; + var desktop = "https://linkedin.com/in/".concat(profileId); + var mobile = "https://linkedin.com/mwlite/in/".concat(profileId); +- testProfile(profile, profileId, desktop, mobile); ++ var company = "https://linkedin.com/company/".concat(profileId); ++ testProfile(profile, profileId, desktop, mobile, company); + }); + it('should accept localized urls', function () { + var profile = 'linkedin'; +@@ -42,5 +46,12 @@ describe('PROFILE: linkedin', function () { + expect(sl.sanitize(profile, 'https://de.linkedin.com/in/anton-begehr/')).toBe('https://linkedin.com/in/anton-begehr'); + expect(sl.sanitize(profile, 'https://de.linkedin.com/mwlite/in/anton-begehr/', main_1.TYPE_MOBILE)).toBe('https://linkedin.com/mwlite/in/anton-begehr'); + }); ++ it('should capture a profile id that contains percent encoded characters', function () { ++ var profile = 'linkedin'; ++ var profileId = 'gk%5Fucmierz'; ++ var companyUrlBase = "https://linkedin.com/company/".concat(profileId); ++ expect(sl.isValid(profile, companyUrlBase)).toBeTruthy(); ++ expect(sl.getProfileId(profile, companyUrlBase)).toBe(profileId); ++ }); + }); + //# sourceMappingURL=linkedin.spec.js.map +\ No newline at end of file +diff --git a/lib/profiles/linkedin.spec.js.map b/lib/profiles/linkedin.spec.js.map +index 3b11e74e75646420b13920aec6a79278f4ac34cc..15065a4fe8eb142c3a88c5123025f1cdf91ac96a 100644 +--- a/lib/profiles/linkedin.spec.js.map ++++ b/lib/profiles/linkedin.spec.js.map +@@ -1 +1 @@ +-{"version":3,"file":"linkedin.spec.js","sourceRoot":"","sources":["../../src/profiles/linkedin.spec.ts"],"names":[],"mappings":"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"} +\ No newline at end of file ++{"version":3,"file":"linkedin.spec.js","sourceRoot":"","sources":["../../src/profiles/linkedin.spec.ts"],"names":[],"mappings":"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"} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de0e52b5a7b..195c5a41c72 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -33,6 +33,9 @@ patchedDependencies: '@crowdin/ota-client@1.0.0': hash: refrge56ym5gomc3tkglzjdymy path: patches/@crowdin__ota-client@1.0.0.patch + social-links@1.14.0: + hash: vsl4v34ksjh5tzibzra6h65ytm + path: patches/social-links@1.14.0.patch trpc-panel@1.3.4: hash: 3z2tx2cn67fyw5s2xdx73dxaji path: patches/trpc-panel@1.3.4.patch @@ -1094,6 +1097,9 @@ importers: slugify: specifier: 1.6.6 version: 1.6.6 + social-links: + specifier: 1.14.0 + version: 1.14.0(patch_hash=vsl4v34ksjh5tzibzra6h65ytm) sql-bricks-postgres: specifier: 0.6.0 version: 0.6.0 @@ -1681,6 +1687,9 @@ importers: luxon: specifier: 3.4.4 version: 3.4.4 + social-links: + specifier: 1.14.0 + version: 1.14.0(patch_hash=vsl4v34ksjh5tzibzra6h65ytm) superjson: specifier: 2.2.1 version: 2.2.1 @@ -23157,6 +23166,10 @@ packages: no-case: 2.3.2 dev: true + /social-links@1.14.0(patch_hash=vsl4v34ksjh5tzibzra6h65ytm): + resolution: {integrity: sha512-98FpRSrHilAcD/p4Aro2J5rzKnpFJ5QF5M9YEWns6gXwosgBqWKV+AtgYJFRIsvXMyd9UTD3SzPcSU8IJMW7cw==} + patched: true + /socket.io-adapter@2.5.2: resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} dependencies: