Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: introduce entity versioning (DAP-4797) #40

Open
wants to merge 13 commits into
base: develop
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ export const Dropdown: FC<DropdownProps> = ({
) : selectedMutation.source === EntitySourceType.Local ? (
<Badge margin="0 4px 0 0" text={selectedMutation.source} theme="white" />
) : null}
{selectedMutation.metadata.name}
{selectedMutation.metadata.name} (v{selectedMutation.version})
</SelectedMutationDescription>
{selectedMutation.authorId ? (
<SelectedMutationId>by {selectedMutation.authorId}</SelectedMutationId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { AppInMutation } from '@mweb/backend'
import { Image } from './image'
import { useSaveMutation, useMutableWeb } from '@mweb/engine'
import { ButtonsGroup } from './buttons-group'
import { useMutationVersions } from '@mweb/engine'
import { MutationVersionDropdown } from './mutation-version-dropdown'

const SelectedMutationEditorWrapper = styled.div`
display: flex;
Expand Down Expand Up @@ -197,6 +199,7 @@ const EMPTY_MUTATION_ID = '/mutation/NewMutation'
const createEmptyMutation = (): MutationDto => ({
authorId: null,
blockNumber: 0,
version: '0',
id: EMPTY_MUTATION_ID,
localId: 'NewMutation',
timestamp: 0,
Expand Down Expand Up @@ -254,7 +257,7 @@ const alerts: { [name: string]: IAlert } = {
}

export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutations, onClose }) => {
const { switchMutation, switchPreferredSource } = useMutableWeb()
const { switchMutation, switchPreferredSource, isLoading } = useMutableWeb()
const loggedInAccountId = useAccountId()
const [isModified, setIsModified] = useState(true)
const [appIdToOpenDocsModal, setAppIdToOpenDocsModal] = useState<string | null>(null)
Expand All @@ -274,9 +277,13 @@ export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutati

const [editingMutation, setEditingMutation] = useState<MutationDto>(chooseEditingMutation())
const [isConfirmModalOpen, setIsConfirmModalOpen] = useState(false)

const [alert, setAlert] = useState<IAlert | null>(null)

// Reload the base mutation if it changed (e.g. if a mutation version was updated)
useEffect(() => {
setEditingMutation(chooseEditingMutation())
}, [isLoading])

useEffect(() => {
const doChecksForAlerts = (): IAlert | null => {
if (!loggedInAccountId) return alerts.noWallet
Expand Down Expand Up @@ -378,7 +385,10 @@ export const MutationEditorModal: FC<Props> = ({ apps, baseMutation, localMutati
/>
</ImgWrapper>
<TextWrapper>
<p>{baseMutation.metadata.name}</p>
<p>
{baseMutation.metadata.name}{' '}
<MutationVersionDropdown mutationId={baseMutation?.id ?? null} />
</p>
<span>
by{' '}
{!baseMutation.authorId && !loggedInAccountId
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { useMutableWeb, useMutationVersions } from '@mweb/engine'
import React from 'react'
import { FC } from 'react'

const LatestKey = 'latest'

export const MutationVersionDropdown: FC<{ mutationId: string | null }> = ({ mutationId }) => {
const {
switchMutationVersion,
selectedMutation,
mutationVersions: currentMutationVersions,
} = useMutableWeb()
const { mutationVersions, areMutationVersionsLoading } = useMutationVersions(mutationId)

if (!mutationId) {
return null
}

if (!selectedMutation || areMutationVersionsLoading) {
return <span>Loading...</span>
}

const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (mutationId) {
switchMutationVersion(
mutationId,
e.target.value === LatestKey ? null : e.target.value?.toString()
)
}
}

return (
<select onChange={handleChange} value={currentMutationVersions[mutationId] ?? LatestKey}>
{mutationVersions.map((version) => (
<option key={version.version} value={version.version}>
v{version.version}
</option>
))}
<option value={LatestKey}>latest</option>
</select>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class ApplicationService {
}

public async getApplication(appId: AppId): Promise<ApplicationDto | null> {
const app = await this.applicationRepository.getItem(appId)
const app = await this.applicationRepository.getItem({ id: appId })
return app?.toDto() ?? null
}

Expand Down
64 changes: 58 additions & 6 deletions libs/backend/src/services/base/base-agg.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,24 @@ export class BaseAggRepository<T extends Base> implements IRepository<T> {
private local: IRepository<T>
) {}

async getItem(id: EntityId, source?: EntitySourceType): Promise<T | null> {
async getItem({
id,
source,
version,
}: {
id: EntityId
source?: EntitySourceType
version?: string
}): Promise<T | null> {
if (source === EntitySourceType.Local) {
return this.local.getItem(id)
return this.local.getItem({ id, version })
} else if (source === EntitySourceType.Origin) {
return this.remote.getItem(id)
return this.remote.getItem({ id, version })
} else {
// ToDo: why local is preferred?
const localItem = await this.local.getItem(id)
const localItem = await this.local.getItem({ id, version })
if (localItem) return localItem
return this.remote.getItem(id)
return this.remote.getItem({ id, version })
}
}

Expand Down Expand Up @@ -88,8 +96,52 @@ export class BaseAggRepository<T extends Base> implements IRepository<T> {
}
}

async getTagValue({
id,
source,
tag,
}: {
id: EntityId
source?: EntitySourceType
tag: string
}): Promise<string | null> {
if (source === EntitySourceType.Local) {
return this.local.getTagValue({ id, tag })
} else if (source === EntitySourceType.Origin) {
return this.remote.getTagValue({ id, tag })
} else {
throw new Error('Invalid source')
}
}

async getTags({ id, source }: { id: EntityId; source?: EntitySourceType }): Promise<string[]> {
if (source === EntitySourceType.Local) {
return this.local.getTags({ id })
} else if (source === EntitySourceType.Origin) {
return this.remote.getTags({ id })
} else {
throw new Error('Invalid source')
}
}

async getVersions({
id,
source,
}: {
id: EntityId
source?: EntitySourceType
}): Promise<string[]> {
if (source === EntitySourceType.Local) {
return this.local.getVersions({ id })
} else if (source === EntitySourceType.Origin) {
return this.remote.getVersions({ id })
} else {
throw new Error('Invalid source')
}
}

private async _deleteLocalItemIfExist(id: EntityId) {
if (await this.local.getItem(id)) {
if (await this.local.getItem({ id })) {
await this.local.deleteItem(id)
}
}
Expand Down
22 changes: 17 additions & 5 deletions libs/backend/src/services/base/base-local.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
this._entityKey = getEntity(EntityType).name
}

async getItem(id: EntityId): Promise<T | null> {
async getItem({ id, version }: { id: EntityId; version?: string }): Promise<T | null> {
const parsedId = BaseLocalRepository._parseGlobalId(id)
if (!parsedId) return null

Expand Down Expand Up @@ -48,7 +48,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
return true
})

const items = await Promise.all(filteredKeys.map((id) => this.getItem(id)))
const items = await Promise.all(filteredKeys.map((id) => this.getItem({ id })))

return items.filter((x) => x !== null)
}
Expand All @@ -62,12 +62,12 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
}
return true
})

return filteredItems
}

async createItem(item: T): Promise<T> {
if (await this.getItem(item.id)) {
if (await this.getItem({ id: item.id })) {
throw new Error('Item with that ID already exists')
}

Expand All @@ -79,7 +79,7 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
}

async editItem(item: T): Promise<T> {
if (!(await this.getItem(item.id))) {
if (!(await this.getItem({ id: item.id }))) {
throw new Error('Item with that ID does not exist')
}

Expand Down Expand Up @@ -129,6 +129,18 @@ export class BaseLocalRepository<T extends Base> implements IRepository<T> {
return entity
}

async getVersions(options: { id: EntityId }): Promise<string[]> {
throw new Error('Method not implemented.')
}

async getTagValue(options: { id: EntityId; tag: string }): Promise<string | null> {
throw new Error('Method not implemented.')
}

async getTags(options: { id: EntityId }): Promise<string[]> {
throw new Error('Method not implemented.')
}

private static _parseGlobalId(globalId: EntityId): {
authorId: string
type: string
Expand Down
1 change: 1 addition & 0 deletions libs/backend/src/services/base/base.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export type BaseDto = {
authorId: string | null
blockNumber: number
timestamp: number
version: string
}
2 changes: 2 additions & 0 deletions libs/backend/src/services/base/base.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class Base {
source: EntitySourceType = EntitySourceType.Local
blockNumber: number = 0 // ToDo: fake block number
timestamp: number = 0 // ToDo: fake timestamp
version: string = '0' // ToDo: fake version?

get authorId(): string | null {
return this.id.split(KeyDelimiter)[0] ?? null
Expand Down Expand Up @@ -59,6 +60,7 @@ export class Base {
source: this.source,
blockNumber: this.blockNumber,
timestamp: this.timestamp,
version: this.version,
}
}
}
Loading
Loading