Skip to content

Commit

Permalink
fix: URL and JSONbin storage (#1172)
Browse files Browse the repository at this point in the history
Updated URL storage provider to support themes and metadata
Updated JSONbin to support metadata

Fixes #1164
  • Loading branch information
LiamMartens authored Aug 23, 2022
1 parent e111327 commit 7a2db48
Show file tree
Hide file tree
Showing 19 changed files with 608 additions and 79 deletions.
2 changes: 1 addition & 1 deletion src/app/components/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default function Footer() {

const checkForChanges = React.useCallback(() => {
const tokenSetOrder = Object.keys(tokens);
const defaultMetadata = isGitProvider(storageType) ? { tokenSetOrder } : {};
const defaultMetadata = storageType.provider !== StorageProviderType.LOCAL ? { tokenSetOrder } : {};
const hasChanged = !compareLastSyncedState(
tokens,
themes,
Expand Down
56 changes: 56 additions & 0 deletions src/app/store/providers/__tests__/updateJSONBinTokens.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { mockFetch } from '../../../../../tests/__mocks__/fetchMock';
import { updateJSONBinTokens } from '../jsonbin';

describe('updateJSONBinTokens', () => {
it('should work', async () => {
mockFetch.mockImplementationOnce(() => Promise.resolve({
ok: true,
}));

await updateJSONBinTokens({
tokens: {},
themes: [],
context: {
id: 'jsonbin',
secret: 'secret',
},
updatedAt: '2022-06-16T10:00:00.000Z',
});

expect(mockFetch).toBeCalledTimes(1);
});

it('should check updatedAt', async () => {
mockFetch.mockImplementationOnce(() => (
Promise.resolve({
ok: true,
json: () => Promise.resolve({
record: {
version: '1',
updatedAt: '2022-06-15T10:00:00.000Z',
values: {},
$themes: [],
$metadata: {},
},
}),
})
));

mockFetch.mockImplementationOnce(() => Promise.resolve({
ok: true,
}));

await updateJSONBinTokens({
tokens: {},
themes: [],
context: {
id: 'jsonbin',
secret: 'secret',
},
oldUpdatedAt: '2022-06-15T12:00:00.000Z',
updatedAt: '2022-06-16T10:00:00.000Z',
});

expect(mockFetch).toBeCalledTimes(2);
});
});
3 changes: 1 addition & 2 deletions src/app/store/providers/file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { useFlags } from '@/app/components/LaunchDarkly';
import { FileTokenStorage } from '@/storage/FileTokenStorage';
import { ErrorMessages } from '@/constants/ErrorMessages';
import { RemoteResponseData } from '@/types/RemoteResponseData';
import { GitStorageMetadata } from '@/storage/GitTokenStorage';

export default function useFile() {
const dispatch = useDispatch<Dispatch>();
Expand All @@ -18,7 +17,7 @@ export default function useFile() {
return storageClient;
}, [multiFileSync]);

const readTokensFromFileOrDirectory = useCallback(async (files: FileList): Promise<RemoteResponseData<GitStorageMetadata> | null> => {
const readTokensFromFileOrDirectory = useCallback(async (files: FileList): Promise<RemoteResponseData | null> => {
const storage = storageClientFactory(files);
try {
const content = await storage.retrieve();
Expand Down
19 changes: 12 additions & 7 deletions src/app/store/providers/jsonbin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { StorageTypeCredentials, StorageTypeFormValues } from '@/types/StorageTy
import { RemoteResponseData } from '@/types/RemoteResponseData';
import { ErrorMessages } from '@/constants/ErrorMessages';
import { saveLastSyncedState } from '@/utils/saveLastSyncedState';
import { applyTokenSetOrder } from '@/utils/tokenset';

export async function updateJSONBinTokens({
tokens, themes, context, updatedAt, oldUpdatedAt = null,
Expand All @@ -31,6 +32,7 @@ export async function updateJSONBinTokens({
tokens,
themes,
metadata: {
tokenSetOrder: Object.keys(tokens),
updatedAt: updatedAt ?? new Date().toISOString(),
version: pjs.plugin_version,
},
Expand All @@ -45,23 +47,26 @@ export async function updateJSONBinTokens({
errorMessage: remoteTokens?.errorMessage,
};
}

const comparison = await compareUpdatedAt(oldUpdatedAt, remoteTokens?.metadata?.updatedAt ?? '');
if (comparison === 'remote_older') {
storage.save(payload);
if (await storage.save(payload)) {
return payload;
}
} else {
// Tell the user to choose between:
// A) Pull Remote values and replace local changes
// B) Overwrite Remote changes
notifyToUI('Error updating tokens as remote is newer, please update first', { error: true });
}
} else {
storage.save(payload);
} else if (await storage.save(payload)) {
return payload;
}
} catch (e) {
console.log('Error updating jsonbin', e);
}

return undefined;
return null;
}

export function useJSONbin() {
Expand All @@ -77,7 +82,7 @@ export function useJSONbin() {
const updatedAt = new Date().toISOString();
const result = await JSONBinTokenStorage.create(name, updatedAt, secret);
if (result) {
updateJSONBinTokens({
await updateJSONBinTokens({
tokens,
context: {
id: result.metadata.id,
Expand Down Expand Up @@ -189,9 +194,9 @@ export function useJSONbin() {
},
shouldSetInDocument: true,
});
saveLastSyncedState(dispatch, content.tokens, content.themes, {});
saveLastSyncedState(dispatch, content.tokens, content.themes, content.metadata);
dispatch.tokenState.setTokenData({
values: content.tokens,
values: applyTokenSetOrder(content.tokens, content.metadata?.tokenSetOrder),
themes: content.themes,
usedTokenSet: usedTokenSets,
activeTheme,
Expand Down
3 changes: 2 additions & 1 deletion src/app/store/providers/url.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { StorageTypeCredentials } from '@/types/StorageType';
import { activeThemeSelector, usedTokenSetSelector } from '@/selectors';
import { ErrorMessages } from '@/constants/ErrorMessages';
import { RemoteResponseData } from '@/types/RemoteResponseData';
import { applyTokenSetOrder } from '@/utils/tokenset';

type UrlCredentials = Extract<StorageTypeCredentials, { provider: StorageProviderType.URL; }>;

Expand Down Expand Up @@ -58,7 +59,7 @@ export default function useURL() {

if (Object.keys(content.tokens).length) {
dispatch.tokenState.setTokenData({
values: content.tokens,
values: applyTokenSetOrder(content.tokens, content.metadata?.tokenSetOrder),
themes: content.themes,
usedTokenSet: usedTokenSets,
activeTheme,
Expand Down
2 changes: 1 addition & 1 deletion src/app/store/updateSources.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ async function updateRemoteTokens({
track('pushTokens', { provider: StorageProviderType.JSONBIN });

notifyToUI('Updating JSONBin...');
updateJSONBinTokens({
await updateJSONBinTokens({
themes,
tokens,
context,
Expand Down
10 changes: 5 additions & 5 deletions src/storage/ADOTokenStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as GitInterfaces from 'azure-devops-node-api/interfaces/GitInterfaces';
import compact from 'just-compact';
import { StorageProviderType } from '@/constants/StorageProviderType';
import { StorageTypeCredentials } from '@/types/StorageType';
import { GitStorageMetadata, GitTokenStorage } from './GitTokenStorage';
import { GitTokenStorage } from './GitTokenStorage';
import {
RemoteTokenstorageErrorMessage,
RemoteTokenStorageFile, RemoteTokenStorageMetadataFile, RemoteTokenStorageSingleTokenSetFile, RemoteTokenStorageThemesFile,
Expand Down Expand Up @@ -236,7 +236,7 @@ export class ADOTokenStorage extends GitTokenStorage {
}
}

public async read(): Promise<RemoteTokenStorageFile<GitStorageMetadata>[] | RemoteTokenstorageErrorMessage> {
public async read(): Promise<RemoteTokenStorageFile[] | RemoteTokenstorageErrorMessage> {
try {
if (!this.path.endsWith('.json')) {
const { value } = await this.getItems();
Expand All @@ -258,7 +258,7 @@ export class ADOTokenStorage extends GitTokenStorage {
return null;
}),
);
return compact(jsonFileContents.map<RemoteTokenStorageFile<GitStorageMetadata> | null>((fileContent, index) => {
return compact(jsonFileContents.map<RemoteTokenStorageFile | null>((fileContent, index) => {
const { path } = jsonFiles[index];
if (fileContent) {
const name = path?.replace(this.path, '')?.replace(/^\/+/, '')?.replace('.json', '');
Expand All @@ -276,7 +276,7 @@ export class ADOTokenStorage extends GitTokenStorage {
path,
type: 'metadata',
data: fileContent,
} as RemoteTokenStorageMetadataFile<GitStorageMetadata>;
} as RemoteTokenStorageMetadataFile;
}

return {
Expand Down Expand Up @@ -313,7 +313,7 @@ export class ADOTokenStorage extends GitTokenStorage {
] : []),
...(Object.entries(data).filter(([key]) => (
!Object.values<string>(SystemFilenames).includes(key)
)) as [string, AnyTokenSet<false>][]).map<RemoteTokenStorageFile<GitStorageMetadata>>(([name, tokenSet]) => ({
)) as [string, AnyTokenSet<false>][]).map<RemoteTokenStorageFile>(([name, tokenSet]) => ({
name,
type: 'tokenSet',
path: this.path,
Expand Down
4 changes: 2 additions & 2 deletions src/storage/BitbucketTokenStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// @TODO this needs to be finalized
import { Bitbucket } from 'bitbucket';
import { RemoteTokenStorageFile } from './RemoteTokenStorage';
import { GitStorageMetadata, GitTokenStorage } from './GitTokenStorage';
import { GitTokenStorage } from './GitTokenStorage';

type CreatedOrUpdatedFileType = {
owner: string;
Expand Down Expand Up @@ -101,7 +101,7 @@ export class BitbucketTokenStorage extends GitTokenStorage {
// https://bitbucketjs.netlify.app/#api-source-source_readRoot OR
// https://developer.atlassian.com/cloud/bitbucket/rest/api-group-source/#api-repositories-workspace-repo-slug-src-commit-path-get
// Equivalent to directly hitting /2.0/repositories/{username}/{repo_slug}/src/{commit}/{path} without having to know the name or SHA1 of the repo's main branch.
public async read(): Promise<RemoteTokenStorageFile<GitStorageMetadata>[]> {
public async read(): Promise<RemoteTokenStorageFile[]> {
try {
const response = await this.bitbucketClient.repositories.get({
workspace: this.owner,
Expand Down
15 changes: 7 additions & 8 deletions src/storage/FileTokenStorage.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import compact from 'just-compact';
import {
RemoteTokenStorage, RemoteTokenstorageErrorMessage, RemoteTokenStorageFile, RemoteTokenStorageSingleTokenSetFile, RemoteTokenStorageThemesFile,
RemoteTokenStorage, RemoteTokenstorageErrorMessage, RemoteTokenStorageFile, RemoteTokenStorageMetadata, RemoteTokenStorageSingleTokenSetFile, RemoteTokenStorageThemesFile,
} from './RemoteTokenStorage';
import IsJSONString from '@/utils/isJSONString';
import { complexSingleFileSchema, multiFileSchema } from './schemas';
import { ErrorMessages } from '@/constants/ErrorMessages';
import { SystemFilenames } from '@/constants/SystemFilenames';
import { GitStorageMetadata } from './GitTokenStorage';

type StorageFlags = {
multiFileEnabled: boolean
};

export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
export class FileTokenStorage extends RemoteTokenStorage {
private files: FileList;

protected flags: StorageFlags = {
Expand All @@ -29,7 +28,7 @@ export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
return this;
}

public async read(): Promise<RemoteTokenStorageFile<GitStorageMetadata>[] | RemoteTokenstorageErrorMessage> {
public async read(): Promise<RemoteTokenStorageFile[] | RemoteTokenstorageErrorMessage> {
try {
if (this.flags.multiFileEnabled && this.files.length > 1) {
const jsonFiles = Array.from(this.files).filter((file) => file.webkitRelativePath.endsWith('.json'))
Expand All @@ -54,7 +53,7 @@ export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
}));
// Wait for all promises to be resolved
const jsonFileContents = await Promise.all(filePromises);
return compact(jsonFileContents.map<RemoteTokenStorageFile<GitStorageMetadata> | null>((fileContent, index) => {
return compact(jsonFileContents.map<RemoteTokenStorageFile | null>((fileContent, index) => {
const { webkitRelativePath } = jsonFiles[index];
if (fileContent) {
const name = webkitRelativePath?.substring(webkitRelativePath.indexOf('/') + 1)?.replace('.json', '');
Expand All @@ -70,7 +69,7 @@ export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
return {
path: webkitRelativePath,
type: 'metadata',
data: fileContent as GitStorageMetadata,
data: fileContent as RemoteTokenStorageMetadata,
};
}

Expand All @@ -89,7 +88,7 @@ export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
if (this.files[0].name.endsWith('.json')) {
const reader = new FileReader();
reader.readAsText(this.files[0]);
return await new Promise<RemoteTokenStorageFile<GitStorageMetadata>[] | RemoteTokenstorageErrorMessage>((resolve) => {
return await new Promise<RemoteTokenStorageFile[] | RemoteTokenstorageErrorMessage>((resolve) => {
reader.onload = async () => {
const result = reader.result as string;

Expand All @@ -111,7 +110,7 @@ export class FileTokenStorage extends RemoteTokenStorage<GitStorageMetadata> {
data: $metadata,
},
] : []),
...Object.entries(data).map<RemoteTokenStorageFile<GitStorageMetadata>>(([name, tokenSet]) => ({
...Object.entries(data).map<RemoteTokenStorageFile>(([name, tokenSet]) => ({
name,
type: 'tokenSet',
path: this.files[0].name,
Expand Down
14 changes: 5 additions & 9 deletions src/storage/GitTokenStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { DeepTokensMap, ThemeObjectsList } from '@/types';
import { AnyTokenSet, SingleToken } from '@/types/tokens';
import { SystemFilenames } from '@/constants/SystemFilenames';
import { joinPath } from '@/utils/string';
import { RemoteTokenStorage, RemoteTokenStorageFile } from './RemoteTokenStorage';
import { RemoteTokenStorage, RemoteTokenStorageFile, RemoteTokenStorageMetadata } from './RemoteTokenStorage';

type StorageFlags = {
multiFileEnabled: boolean
Expand All @@ -12,20 +12,16 @@ export type GitStorageSaveOptions = {
commitMessage?: string
};

export type GitStorageMetadata = {
tokenSetOrder?: string[]
};

export type GitSingleFileObject = Record<string, (
Record<string, SingleToken<false> | DeepTokensMap<false>>
)> & {
$themes?: ThemeObjectsList
$metadata?: GitStorageMetadata
$metadata?: RemoteTokenStorageMetadata
};

export type GitMultiFileObject = AnyTokenSet<false> | ThemeObjectsList | GitStorageMetadata;
export type GitMultiFileObject = AnyTokenSet<false> | ThemeObjectsList | RemoteTokenStorageMetadata;

export abstract class GitTokenStorage extends RemoteTokenStorage<GitStorageMetadata, GitStorageSaveOptions> {
export abstract class GitTokenStorage extends RemoteTokenStorage<GitStorageSaveOptions> {
protected secret: string;

protected owner: string;
Expand Down Expand Up @@ -85,7 +81,7 @@ export abstract class GitTokenStorage extends RemoteTokenStorage<GitStorageMetad
shouldCreateBranch?: boolean
): Promise<boolean>;

public async write(files: RemoteTokenStorageFile<GitStorageMetadata>[], saveOptions: GitStorageSaveOptions): Promise<boolean> {
public async write(files: RemoteTokenStorageFile[], saveOptions: GitStorageSaveOptions): Promise<boolean> {
const branches = await this.fetchBranches();
if (!branches.length) return false;

Expand Down
12 changes: 6 additions & 6 deletions src/storage/GithubTokenStorage.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import compact from 'just-compact';
import { Octokit } from '@octokit/rest';
import { decodeBase64 } from '@/utils/string/ui';
import { RemoteTokenstorageErrorMessage, RemoteTokenStorageFile } from './RemoteTokenStorage';
import { RemoteTokenstorageErrorMessage, RemoteTokenStorageFile, RemoteTokenStorageMetadata } from './RemoteTokenStorage';
import IsJSONString from '@/utils/isJSONString';
import { AnyTokenSet } from '@/types/tokens';
import { ThemeObjectsList } from '@/types';
import {
GitMultiFileObject, GitSingleFileObject, GitStorageMetadata, GitTokenStorage,
GitMultiFileObject, GitSingleFileObject, GitTokenStorage,
} from './GitTokenStorage';
import { SystemFilenames } from '@/constants/SystemFilenames';
import { ErrorMessages } from '@/constants/ErrorMessages';
Expand Down Expand Up @@ -149,7 +149,7 @@ export class GithubTokenStorage extends GitTokenStorage {
}
}

public async read(): Promise<RemoteTokenStorageFile<GitStorageMetadata>[] | RemoteTokenstorageErrorMessage> {
public async read(): Promise<RemoteTokenStorageFile[] | RemoteTokenstorageErrorMessage> {
try {
const normalizedPath = compact(this.path.split('/')).join('/');
const response = await this.octokitClient.rest.repos.getContent({
Expand Down Expand Up @@ -184,7 +184,7 @@ export class GithubTokenStorage extends GitTokenStorage {
headers: octokitClientDefaultHeaders,
}) : Promise.resolve(null)
)));
return compact(jsonFileContents.map<RemoteTokenStorageFile<GitStorageMetadata> | null>((fileContent, index) => {
return compact(jsonFileContents.map<RemoteTokenStorageFile | null>((fileContent, index) => {
const { path } = jsonFiles[index];
if (
path
Expand All @@ -210,7 +210,7 @@ export class GithubTokenStorage extends GitTokenStorage {
return {
path: filePath,
type: 'metadata',
data: parsed as GitStorageMetadata,
data: parsed as RemoteTokenStorageMetadata,
};
}

Expand All @@ -236,7 +236,7 @@ export class GithubTokenStorage extends GitTokenStorage {
},
...(Object.entries(parsed).filter(([key]) => (
!Object.values<string>(SystemFilenames).includes(key)
)) as [string, AnyTokenSet<false>][]).map<RemoteTokenStorageFile<GitStorageMetadata>>(([name, tokenSet]) => ({
)) as [string, AnyTokenSet<false>][]).map<RemoteTokenStorageFile>(([name, tokenSet]) => ({
name,
type: 'tokenSet',
path: `${this.path}/${name}.json`,
Expand Down
Loading

0 comments on commit 7a2db48

Please sign in to comment.