Skip to content

Commit 5db67cf

Browse files
SharonStratsGPT-5.6 Luna
andcommitted
Created a draft of an extended profile service
Prompt: Created 2 docs and pointed copilot to them, one for the best strategies for working with data in solidos and another to all the problems I found with the profile mutations. Told it copy the logic in profile-pane for mutations to solid-logic, extend the profile service and expose these methods. After that performed many cleanup tasks to simply/cleanup the code even further. Co-authored-by: GPT-5.6 Luna <gpt-5.6-luna@openai.com>
1 parent da4a033 commit 5db67cf

40 files changed

Lines changed: 2559 additions & 53 deletions

‎package-lock.json‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/acl/accessControlSubjects.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function isHttpUrl (value: string): boolean {
2323
return value.startsWith('http://') || value.startsWith('https://')
2424
}
2525

26-
function isBareOriginUrl (value: string): boolean {
26+
export function isOriginUrl (value: string): boolean {
2727
try {
2828
const parsed = new URL(value)
2929
return (parsed.protocol === 'http:' || parsed.protocol === 'https:') &&
@@ -57,7 +57,7 @@ export async function classifyAccessControlSubject (store: LiveStore, principle:
5757
const subject = sym(principle)
5858
let types = store.findTypeURIs(subject)
5959

60-
if (isBareOriginUrl(principle)) {
60+
if (isOriginUrl(principle)) {
6161
return { kind: 'origin', subjectValue: normalizeOriginUrl(principle) }
6262
}
6363

‎src/index.ts‎

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,26 @@ export { reloadOnIdentityReplaced } from './authSession/identityState'
1414
export { getSuggestedIssuers } from './issuer/issuerLogic'
1515
export { createTypeIndexLogic } from './typeIndex/typeIndexLogic'
1616
export { createDirectoryLogic, DEFAULT_DIRECTORY_CATALOG_URL, DEFAULT_DIRECTORY_SOURCES } from './directory/directoryLogic'
17+
export { createProfileMutationService } from './profile/profileMutationService'
18+
export type { BioMutationPlan, BioRow } from './profile/sections/bio/types'
19+
export type { ContactMutationPlan, ContactPointRow, ContactAddressRow } from './profile/sections/contactInfo/types'
20+
export type { EducationMutationPlan, EducationRow } from './profile/sections/education/types'
21+
export type { HeadingMutationPlan, ProfileBasicRow } from './profile/sections/heading/types'
22+
export type { LanguageMutationPlan } from './profile/sections/languages/mutations'
23+
export type { LanguageRow } from './profile/sections/languages/types'
24+
export type { ProjectMutationPlan, ProjectRow } from './profile/sections/projects/types'
25+
export type { ResumeMutationPlan } from './profile/sections/resume/mutations'
26+
export type { ResumeRow } from './profile/sections/resume/types'
27+
export type { SkillMutationPlan } from './profile/sections/skills/mutations'
28+
export type { SkillRow } from './profile/sections/skills/types'
29+
export type { SocialMutationPlan, SocialRow } from './profile/sections/social/types'
30+
export type { MutationOps, RowStatus, RdfStatement } from './profile/sections/shared/types'
31+
export { isOriginUrl } from './acl/accessControlSubjects'
1732
export type { AccessControlSubjectKind, AccessControlSubject } from './acl/accessControlSubjects'
1833
export type { ACLContext, AccessMode, AccessSubject, Authorization, PatchPlan } from './types'
19-
export type { AppDetails, SolidNamespace, AuthenticationContext, SolidLogic, ChatLogic, DirectoryLogic, DirectoryEntry, DirectorySearchOptions, DirectoryRelationshipLabel, DirectorySource } from './types'
34+
export type { AppDetails, SolidNamespace, AuthenticationContext, SolidLogic, ProfileLogic, ChatLogic, DirectoryLogic, DirectoryEntry, DirectorySearchOptions, DirectoryRelationshipLabel, DirectorySource, ProfileMutationService } from './types'
2035
export { UnauthorizedError, CrossOriginForbiddenError, SameOriginForbiddenError, NotFoundError, FetchError, NotEditableError, WebOperationError } from './logic/CustomError'
36+
export { documentDirectoryUri, isAbsoluteHttpUri } from './util/utils'
2137

2238
export {
2339
solidLogicSingleton, // solidLogicSingleton is exported entirely because it is used in solid-panes

‎src/profile/profileLogic.ts‎

Lines changed: 12 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,72 +2,41 @@ import { literal, NamedNode, st, sym } from 'rdflib'
22
import { ACL_LINK } from '../acl/aclLogic'
33
import { CrossOriginForbiddenError, FetchError, NotEditableError, SameOriginForbiddenError, UnauthorizedError, WebOperationError } from '../logic/CustomError'
44
import * as debug from '../util/debug'
5-
import { ns as namespace } from '../util/ns'
5+
import { ns } from '../util/ns'
66
import { privateTypeIndexDocument, publicTypeIndexDocument } from '../typeIndex/typeIndexDocuments'
77
import { publicTypeIndexAclDocument } from '../typeIndex/typeIndexAclDocuments'
88
import { preferencesFileDocument } from './profileDocuments'
99
import { ownerOnlyContainerAclDocument } from './profileAclDocuments'
1010
import { createContainerLogic } from '../util/containerLogic'
11-
import { differentOrigin, suggestPreferencesFile } from '../util/utils'
11+
import { differentOrigin, documentDirectoryUri, suggestPreferencesFile } from '../util/utils'
12+
import { isMissingError } from '../resource/resourceHttp'
1213
import { ProfileLogic } from '../types'
14+
import { createProfileMutationService } from './profileMutationService'
1315

1416
export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
15-
const ns = namespace
1617
const containerLogic = createContainerLogic(store)
18+
const profileMutationService = createProfileMutationService(store)
1719
const loadPreferencesInFlight = new Map<string, Promise<NamedNode>>()
1820
const cachedPreferencesFileByWebId = new Map<string, NamedNode>()
1921

20-
function isAbsoluteHttpUri(uri: string | null | undefined): boolean {
21-
return !!uri && (uri.startsWith('https://') || uri.startsWith('http://'))
22-
}
23-
24-
function docDirUri(node: NamedNode): string | null {
25-
const doc = node.doc()
26-
const dir = doc.dir()
27-
if (dir?.uri && isAbsoluteHttpUri(dir.uri)) return dir.uri
28-
const docUri = doc.uri
29-
if (!docUri || !isAbsoluteHttpUri(docUri)) return null
30-
const withoutFragment = docUri.split('#')[0]
31-
const lastSlash = withoutFragment.lastIndexOf('/')
32-
if (lastSlash === -1) return null
33-
return withoutFragment.slice(0, lastSlash + 1)
34-
}
35-
3622
function suggestTypeIndexInPreferences(preferencesFile: NamedNode, filename: string): NamedNode {
37-
const dirUri = docDirUri(preferencesFile)
23+
const dirUri = documentDirectoryUri(preferencesFile)
3824
if (!dirUri) throw new Error(`Cannot derive directory for preferences file ${preferencesFile.uri}`)
3925
return sym(dirUri + filename)
4026
}
4127

42-
function isNotFoundError(err: any): boolean {
43-
if (err?.response?.status === 404) return true
44-
const text = `${err?.message || err || ''}`
45-
return text.includes('404') || text.includes('Not Found')
46-
}
47-
48-
async function ensureContainerExists(containerUri: string): Promise<void> {
49-
const containerNode = sym(containerUri)
50-
try {
51-
await store.fetcher.load(containerNode)
52-
return
53-
} catch (err) {
54-
if (!isNotFoundError(err)) throw err
55-
}
56-
await containerLogic.createContainer(containerUri)
57-
}
58-
5928
async function ensureOwnerOnlyAclForSettings(user: NamedNode, preferencesFile: NamedNode): Promise<void> {
60-
const dirUri = docDirUri(preferencesFile)
29+
const dirUri = documentDirectoryUri(preferencesFile)
6130
if (!dirUri) throw new Error(`Cannot derive settings directory from ${preferencesFile.uri}`)
62-
await ensureContainerExists(dirUri)
31+
await containerLogic.ensureContainer(dirUri)
6332

6433
const containerNode = sym(dirUri)
6534
let aclDocUri: string | undefined
6635
try {
6736
await store.fetcher.load(containerNode)
6837
aclDocUri = store.any(containerNode, ACL_LINK)?.value
6938
} catch (err) {
70-
if (!isNotFoundError(err)) throw err
39+
if (!isMissingError(err)) throw err
7140
}
7241
if (!aclDocUri) {
7342
// Fallback for servers/tests where rel=acl is not exposed in mocked headers.
@@ -78,7 +47,7 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
7847
await store.fetcher.load(aclDoc)
7948
return
8049
} catch (err) {
81-
if (!isNotFoundError(err)) throw err
50+
if (!isMissingError(err)) throw err
8251
}
8352

8453
await store.fetcher.webOperation('PUT', aclDoc.uri, {
@@ -96,7 +65,7 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
9665
await store.fetcher.load(publicTypeIndex)
9766
aclDocUri = store.any(publicTypeIndex, ACL_LINK)?.value
9867
} catch (err) {
99-
if (!isNotFoundError(err)) throw err
68+
if (!isMissingError(err)) throw err
10069
}
10170
if (!aclDocUri) {
10271
aclDocUri = `${publicTypeIndex.uri}.acl`
@@ -336,6 +305,7 @@ export function createProfileLogic(store, authn, utilityLogic): ProfileLogic {
336305
}
337306

338307
return {
308+
...profileMutationService,
339309
loadMe,
340310
getPodRoot,
341311
getMainInbox,
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { NamedNode, LiveStore } from 'rdflib'
2+
import { NotEditableError } from '../logic/CustomError'
3+
import type { ProfileMutationService } from '../types'
4+
import { processBioMutations } from './sections/bio/mutations'
5+
import { processContactInfoMutations } from './sections/contactInfo/mutations'
6+
import { processEducationMutations } from './sections/education/mutations'
7+
import { processHeadingMutations } from './sections/heading/mutations'
8+
import { processLanguageMutations } from './sections/languages/mutations'
9+
import { processProjectsMutations } from './sections/projects/mutations'
10+
import { processResumeMutations } from './sections/resume/mutations'
11+
import { processSkillsMutations } from './sections/skills/mutations'
12+
import { processSocialMutations } from './sections/social/mutations'
13+
import type { HeadingMutationPlan } from './sections/heading/types'
14+
import type { BioMutationPlan } from './sections/bio/types'
15+
import type { ContactMutationPlan } from './sections/contactInfo/types'
16+
import type { EducationMutationPlan } from './sections/education/types'
17+
import type { LanguageMutationPlan } from './sections/languages/mutations'
18+
import type { LanguageRow } from './sections/languages/types'
19+
import type { ProjectMutationPlan } from './sections/projects/types'
20+
import type { ResumeMutationPlan } from './sections/resume/mutations'
21+
import type { SkillMutationPlan } from './sections/skills/mutations'
22+
import type { SocialMutationPlan, SocialRow } from './sections/social/types'
23+
24+
export function createProfileMutationService (store: LiveStore): ProfileMutationService {
25+
async function prepareProfileDocument (doc: NamedNode): Promise<void> {
26+
await store.fetcher.load(doc, { force: true })
27+
if (!store.updater.editable(doc)) {
28+
throw new NotEditableError(`Cannot edit profile document ${doc.value}`)
29+
}
30+
}
31+
32+
async function runProfileSectionMutation<Args extends unknown[]>(
33+
subject: NamedNode,
34+
mutation: (store: LiveStore, subject: NamedNode, ...args: Args) => Promise<void>,
35+
...args: Args
36+
): Promise<void> {
37+
await prepareProfileDocument(subject.doc())
38+
await mutation(store, subject, ...args)
39+
}
40+
41+
return {
42+
processHeadingMutations: (subject: NamedNode, plan: HeadingMutationPlan) =>
43+
runProfileSectionMutation(subject, processHeadingMutations, plan),
44+
processContactInfoMutations: (subject: NamedNode, plan: ContactMutationPlan) =>
45+
runProfileSectionMutation(subject, processContactInfoMutations, plan),
46+
processBioMutations: (subject: NamedNode, plan: BioMutationPlan) =>
47+
runProfileSectionMutation(subject, processBioMutations, plan),
48+
processEducationMutations: (subject: NamedNode, plan: EducationMutationPlan) =>
49+
runProfileSectionMutation(subject, processEducationMutations, plan),
50+
processLanguageMutations: (subject: NamedNode, plan: LanguageMutationPlan, orderedRows?: LanguageRow[]) =>
51+
runProfileSectionMutation(subject, processLanguageMutations, plan, orderedRows),
52+
processProjectsMutations: (subject: NamedNode, plan: ProjectMutationPlan) =>
53+
runProfileSectionMutation(subject, processProjectsMutations, plan),
54+
processResumeMutations: (subject: NamedNode, plan: ResumeMutationPlan) =>
55+
runProfileSectionMutation(subject, processResumeMutations, plan),
56+
processSkillsMutations: (subject: NamedNode, plan: SkillMutationPlan) =>
57+
runProfileSectionMutation(subject, processSkillsMutations, plan),
58+
processSocialMutations: (subject: NamedNode, plan: SocialMutationPlan, orderedRows?: SocialRow[]) =>
59+
runProfileSectionMutation(subject, processSocialMutations, plan, orderedRows)
60+
}
61+
}
62+
63+
export type { ProfileMutationService }
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { LiveStore, NamedNode } from 'rdflib'
2+
import { ns } from '../../../util/ns'
3+
import { BioMutationPlan, BioRow } from './types'
4+
import { MutationOps } from '../shared/types'
5+
import { replaceLiteralPredicateStatements, runUpdateTransport } from '../shared/rdfMutationHelpers'
6+
import { logAndThrowMutationError } from '../shared/mutationErrors'
7+
import { bioUpdatesFailedMessageText, updaterUnsupportedStoreErrorMessageText } from '../../texts'
8+
9+
async function mutateBioEntry(store: LiveStore, subject: NamedNode, bioOps: MutationOps<BioRow>) {
10+
const doc = subject.doc()
11+
const deletions: any[] = []
12+
const insertions: any[] = []
13+
14+
const applyBasics = (bio: BioRow, clearAll = false) => {
15+
const data = clearAll
16+
? {
17+
description: '',
18+
}
19+
: bio
20+
21+
replaceLiteralPredicateStatements(store, subject, ns.vcard('note'), doc, deletions, insertions, data.description)
22+
}
23+
24+
const removeBio = bioOps.remove[0]
25+
const updateBio = bioOps.update[0]
26+
const createBio = bioOps.create[0]
27+
28+
const selectedBio = updateBio || createBio || removeBio
29+
if (selectedBio) {
30+
applyBasics(selectedBio, Boolean(removeBio && !updateBio && !createBio))
31+
}
32+
33+
await runUpdateTransport(store, doc, deletions, insertions, {
34+
unsupportedMessage: updaterUnsupportedStoreErrorMessageText,
35+
failureMessage: bioUpdatesFailedMessageText,
36+
})
37+
}
38+
39+
export async function processBioMutations(store: LiveStore, subject: NamedNode, mutationPlan: BioMutationPlan) {
40+
try {
41+
await mutateBioEntry(store, subject, mutationPlan)
42+
} catch (error) {
43+
logAndThrowMutationError(error)
44+
}
45+
}

‎src/profile/sections/bio/types.ts‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Node } from 'rdflib'
2+
import { MutationOps } from '../shared/types'
3+
import { RowStatus } from '../shared/types'
4+
5+
export interface BioFields {
6+
description?: string
7+
}
8+
9+
export interface BioDetails extends BioFields {
10+
entryNode: Node
11+
}
12+
13+
export interface BioRow extends BioFields {
14+
entryNode: string
15+
status: RowStatus
16+
}
17+
18+
export type BioMutationPlan = MutationOps<BioRow>

0 commit comments

Comments
 (0)