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: compatibility endpoint #1212

Merged
merged 40 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
aa57b45
feat: compatibility endpoint
eemmiillyy Oct 6, 2023
f026cbb
clean
eemmiillyy Oct 6, 2023
73ba214
name
eemmiillyy Oct 6, 2023
3baed08
clean
eemmiillyy Oct 6, 2023
04daef8
hook
eemmiillyy Oct 9, 2023
8e4db20
change location of compatibility.json
eemmiillyy Oct 9, 2023
8369405
Merge branch 'main' into feat/compatibility-endpoint
eemmiillyy Oct 9, 2023
dc09a70
correct branch
eemmiillyy Oct 9, 2023
9e7ce3a
clean
eemmiillyy Oct 9, 2023
1732a7f
pr changes
eemmiillyy Oct 10, 2023
dc85c30
Merge branch 'main' into feat/compatibility-endpoint
eemmiillyy Oct 11, 2023
e7a60cf
Merge branch 'main' into feat/compatibility-endpoint
eemmiillyy Oct 16, 2023
ec08a66
Merge branch 'main' into feat/compatibility-endpoint
eemmiillyy Oct 23, 2023
a9158ef
pr changes
eemmiillyy Oct 24, 2023
d625612
release
eemmiillyy Oct 24, 2023
cad3fdb
script
eemmiillyy Oct 24, 2023
aba9747
tests
eemmiillyy Oct 27, 2023
b34de0c
fix tests
eemmiillyy Oct 27, 2023
16558a0
clean
eemmiillyy Oct 27, 2023
05c9fe4
clean
eemmiillyy Oct 27, 2023
d1d5b6e
simplify
eemmiillyy Oct 27, 2023
d54e0ef
Merge branch 'main' into feat/compatibility-endpoint
eemmiillyy Oct 27, 2023
de55186
alpha versions
eemmiillyy Oct 30, 2023
2d8e561
ensure alpha versions work
eemmiillyy Oct 30, 2023
faa0c82
clean
eemmiillyy Oct 30, 2023
d99ba2e
correct url
eemmiillyy Oct 30, 2023
b2d0820
fix
eemmiillyy Oct 30, 2023
878bfdf
feat: add total count to search responses (#1232)
eemmiillyy Nov 1, 2023
4a7b9ca
fix: excessive depth on summarize (#1235)
eemmiillyy Nov 2, 2023
b12b9eb
fix: column renaming errors (#1231)
eemmiillyy Nov 2, 2023
5ef6047
Partial update
SferaDev Nov 6, 2023
c81dfed
Merge branch 'main' into feat/compatibility-endpoint
SferaDev Nov 6, 2023
b253347
Apply suggestions from code review
SferaDev Nov 6, 2023
d862ebd
Merge remote-tracking branch 'origin/main' into feat/compatibility-en…
SferaDev Nov 27, 2023
27f8007
Update action
SferaDev Nov 27, 2023
7ee6c38
Update script
SferaDev Nov 27, 2023
fa503e1
Update tests
SferaDev Nov 27, 2023
8bda935
Create hot-bulldogs-dream.md
SferaDev Nov 27, 2023
6b9a2b6
Remove uppercase
SferaDev Nov 27, 2023
91d1e30
Update message
SferaDev Nov 27, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,12 @@ jobs:
run: pnpm build

- name: Create Release Pull Request or Publish to npm
id: release
SferaDev marked this conversation as resolved.
Show resolved Hide resolved
uses: changesets/action@v1
with:
title: Release tracking
publish: npx changeset publish
SferaDev marked this conversation as resolved.
Show resolved Hide resolved
version: node ./scripts/changeset-version.mjs
env:
GITHUB_TOKEN: ${{ secrets.GIT_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
5 changes: 5 additions & 0 deletions cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@rollup/plugin-virtual": "^3.0.2",
"@types/ini": "^1.3.32",
"@types/prompts": "^2.4.7",
"@types/semver": "^7.5.3",
"@xata.io/client": "workspace:*",
"@xata.io/codegen": "workspace:*",
"@xata.io/importer": "workspace:*",
Expand Down Expand Up @@ -60,6 +61,7 @@
"rollup-plugin-hypothetical": "^2.1.1",
"rollup-plugin-import-cdn": "^0.2.3",
"rollup-plugin-virtual-fs": "^4.0.1-alpha.0",
"semver": "^7.5.4",
"text-table": "^0.2.0",
"tmp": "^0.2.1",
"which": "^4.0.0",
Expand All @@ -86,6 +88,9 @@
},
"oclif": {
"bin": "xata",
"hooks": {
"init": "./dist/hooks/init/compatibility"
},
"dirname": "xata",
"commands": "./dist/commands",
"plugins": [
Expand Down
193 changes: 193 additions & 0 deletions cli/src/hooks/init/compatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import { writeFile, readFile, stat } from 'fs/promises';
import fetch from 'node-fetch';
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
import { ONE_DAY, check, fetchInfo, getSdkVersion } from './compatibility.js';
import semver from 'semver';

vi.mock('node-fetch');
vi.mock('fs/promises');

afterEach(() => {
vi.clearAllMocks();
});

const fetchMock = fetch as unknown as ReturnType<typeof vi.fn>;
const writeFileMock = writeFile as unknown as ReturnType<typeof vi.fn>;
const readFileMock = readFile as unknown as ReturnType<typeof vi.fn>;
const statMock = stat as unknown as ReturnType<typeof vi.fn>;

const latestAvailableVersionCLI = '1.0.0';
const latestAvailableVersionSDK = '2.0.0';
const specificVersionCLI = '0.0.8';

const userVersionCLI = '~0.0.1';
const userVersionSDK = '^0.0.2';
const userVersionAlpha = `${latestAvailableVersionCLI}-alpha.v927d47c`;

const cliUpdateAvailable = `"✨ A newer version of the Xata CLI is now available: ${latestAvailableVersionCLI}. You are currently using version: ${semver.coerce(
userVersionCLI
)}"`;
const sdkUpdateAvailable = `"✨ A newer version of the Xata SDK is now available: ${latestAvailableVersionSDK}. You are currently using version: ${semver.coerce(
userVersionSDK
)}"`;

const cliError = `"Incompatible version of CLI: ${semver.coerce(
userVersionCLI
)}. Please upgrade to a version that satisfies: >=${latestAvailableVersionCLI}||${specificVersionCLI}."`;
const sdkError = `"Incompatible version of SDK: ${semver.coerce(
userVersionSDK
)}. Please upgrade to a version that satisfies: ${latestAvailableVersionSDK}."`;

const compatibilityFile = './compatibility.json';

const compatibilityObj = {
cli: {
latest: latestAvailableVersionCLI,
compatibility: [
{
range: `>=${latestAvailableVersionCLI}`
},
{
range: `${specificVersionCLI}`
}
]
},
sdk: {
latest: latestAvailableVersionSDK,
compatibility: [
{
range: latestAvailableVersionSDK
}
]
}
};

const packageJsonObj = (withPackage: boolean) => {
return {
name: 'client-ts',
dependencies: withPackage
? {
'@xata.io/client': userVersionSDK
}
: {}
};
};

fetchMock.mockReturnValue({
ok: true,
json: async () => compatibilityObj
});

describe('getSdkVersion', () => {
test('returns version when @xata package', async () => {
readFileMock.mockReturnValue(JSON.stringify(packageJsonObj(true)));
expect(await getSdkVersion()).toEqual(userVersionSDK);
});
test('returns null when no @xata package', async () => {
readFileMock.mockReturnValue(JSON.stringify(packageJsonObj(false)));
expect(await getSdkVersion()).toEqual(null);
});
});

describe('fetchInfo', () => {
const fetchInfoParams = {
compatibilityFile,
compatibilityUri: ''
};
describe('refreshes', () => {
beforeEach(() => {
readFileMock.mockReturnValue(JSON.stringify(compatibilityObj));
writeFileMock.mockReturnValue(undefined);
});
test('when no files exist', async () => {
statMock.mockRejectedValue(undefined);
await fetchInfo(fetchInfoParams);
expect(writeFileMock).toHaveBeenCalledTimes(1);
});
test('when file is stale', async () => {
const yesterday = new Date().getDate() - ONE_DAY + 1000;
statMock.mockReturnValue({ mtime: new Date(yesterday) });
await fetchInfo(fetchInfoParams);
expect(writeFileMock).toHaveBeenCalledTimes(1);
});
test('when problem fetching, no file writes', async () => {
fetchMock.mockReturnValue({
ok: false
});
const yesterday = new Date().getDate() - ONE_DAY + 1000;
statMock.mockReturnValue({ mtime: new Date(yesterday) });
await fetchInfo(fetchInfoParams);
expect(writeFileMock).not.toHaveBeenCalled();
});
});
describe('does not refresh', () => {
test('if file is not stale', async () => {
statMock.mockReturnValue({ mtime: new Date() });
await fetchInfo(fetchInfoParams);
expect(fetchMock).not.toHaveBeenCalled();
expect(writeFileMock).not.toHaveBeenCalled();
});
});
});

describe('checks', () => {
const defaultParams = { compatibilityObj };
describe('latest', () => {
beforeEach(() => {
readFileMock.mockReturnValue(JSON.stringify(compatibilityObj));
});
test('returns warn if newer package available', async () => {
const cliResponse = await check({ ...defaultParams, pkg: 'cli', currentVersion: userVersionCLI });
expect(cliResponse.warn).toMatchInlineSnapshot(cliUpdateAvailable);
const sdkResponse = await check({ ...defaultParams, pkg: 'sdk', currentVersion: userVersionSDK });
expect(sdkResponse.warn).toMatchInlineSnapshot(sdkUpdateAvailable);
});
test('returns null if no newer package available', async () => {
const cliResponse = await check({ ...defaultParams, pkg: 'cli', currentVersion: latestAvailableVersionCLI });
expect(cliResponse.warn).toBeNull();
const sdkResponse = await check({ ...defaultParams, pkg: 'sdk', currentVersion: latestAvailableVersionSDK });
expect(sdkResponse.warn).toBeNull();
});
});
describe('compatibility', () => {
beforeEach(() => {
readFileMock.mockReturnValue(JSON.stringify(compatibilityObj));
});
test('returns error if not compatible', async () => {
const cliResponse = await check({
...defaultParams,
pkg: 'cli',
currentVersion: userVersionCLI
});
expect(cliResponse.error).toMatchInlineSnapshot(cliError);
const sdkResponse = await check({
...defaultParams,
pkg: 'sdk',
currentVersion: userVersionSDK
});
expect(sdkResponse.error).toMatchInlineSnapshot(sdkError);
});
test('returns null if compatible', async () => {
const cliResponse = await check({
...defaultParams,
pkg: 'cli',
currentVersion: latestAvailableVersionCLI
});
expect(cliResponse.error).toBeNull();
const sdkResponse = await check({
...defaultParams,
pkg: 'sdk',
currentVersion: latestAvailableVersionSDK
});
expect(sdkResponse.error).toBeNull();
// Alpha versions
const cliResponseAlpha = await check({
...defaultParams,
pkg: 'cli',
currentVersion: userVersionAlpha
});
expect(cliResponseAlpha.error).toBeNull();
expect(cliResponseAlpha.warn).toBeNull();
});
});
});
109 changes: 109 additions & 0 deletions cli/src/hooks/init/compatibility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { Hook } from '@oclif/core';
import { readFile, stat, writeFile, mkdir } from 'fs/promises';
import semver from 'semver';
import path from 'path';
import fetch from 'node-fetch';

export const ONE_DAY = 1000 * 60 * 60 * 24 * 1;

export type Packages = 'cli' | 'sdk';
export type Compatibility = {
[key in Packages]: {
latest: string;
compatibility: { range: string }[];
};
};
export type PackageJson = { dependencies: Record<string, string> };

export const check = async (params: {
pkg: 'cli' | 'sdk';
currentVersion: string;
compatibilityObj: Compatibility;
}) => {
const { pkg, compatibilityObj } = params;
const currentVersion = semver.coerce(params.currentVersion)?.version as string;
const updateAvailable = semver.lt(currentVersion, compatibilityObj[pkg].latest);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the user is using version ^1.1.1 and the newest version is 1.1.2 they will still see an update available log.

const compatibleRange = compatibilityObj[pkg].compatibility.map((v) => v.range).join('||');
const semverCompatible = semver.satisfies(currentVersion, compatibleRange);

return {
warn: updateAvailable
? `✨ A newer version of the Xata ${pkg.toUpperCase()} is now available: ${
compatibilityObj[pkg].latest
}. You are currently using version: ${currentVersion}`
: null,
error: !semverCompatible
? `Incompatible version of ${pkg.toUpperCase()}: ${currentVersion}. Please upgrade to a version that satisfies: ${compatibleRange}.`
: null
};
};

export const getSdkVersion = async (): Promise<null | string> => {
const packageJson: PackageJson = JSON.parse(await readFile(`${path.join(process.cwd())}/package.json`, 'utf-8'));
return packageJson && packageJson.dependencies && packageJson.dependencies['@xata.io/client']
? packageJson.dependencies['@xata.io/client']
: null;
};

export const fetchInfo = async (params: { compatibilityUri: string; compatibilityFile: string }) => {
const { compatibilityUri, compatibilityFile } = params;
let shouldRefresh = true;
try {
// Latest time of one of the files should be enough
const statResult = await stat(compatibilityFile);
const lastModified = new Date(statResult.mtime);
// Last param is the number of days - we fetch new package info if the file is older than 1 day
const staleAt = new Date(lastModified.valueOf() + ONE_DAY);
shouldRefresh = new Date() > staleAt;
} catch (_e) {
// Do nothing
}
if (shouldRefresh) {
try {
const latestCompatibilityResponse = await fetch(compatibilityUri);
if (!latestCompatibilityResponse.ok) return;
const body = await latestCompatibilityResponse.json();
if (!(body as Compatibility).cli) return;
try {
await writeFile(compatibilityFile, JSON.stringify(body));
} catch (e) {
if ((e as any).code === 'ENOENT') {
await mkdir(path.dirname(compatibilityFile), { recursive: true });
await writeFile(compatibilityFile, JSON.stringify(body));
}
}
} catch (_e) {
// Do nothing
}
}
};

const hook: Hook<'init'> = async function (_options) {
const dir = path.join(process.cwd(), '.xata', 'version');
const compatibilityFile = `${dir}/compatibility.json`;
const compatibilityUri = 'https://raw.githubusercontent.com/xataio/client-ts/main/compatibility.json';

const displayWarning = async () => {
const compatibilityObj: Compatibility = JSON.parse(await readFile(compatibilityFile, 'utf-8'));
const defaultParams = { compatibilityObj };

const cliPkg = 'cli';
const cliVersion = this.config.version;
const { warn, error } = await check({ ...defaultParams, pkg: cliPkg, currentVersion: cliVersion });
if (warn) this.log(warn);
if (error) this.error(error);

const sdkVersion = await getSdkVersion();
if (sdkVersion) {
const sdkPkg = 'sdk';
const { warn, error } = await check({ ...defaultParams, pkg: sdkPkg, currentVersion: sdkVersion });
if (warn) this.log(warn);
if (error) this.error(error);
}
};

await fetchInfo({ compatibilityFile, compatibilityUri });
await displayWarning();
};

export default hook;
18 changes: 18 additions & 0 deletions compatibility.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"cli": {
"latest": "0.14.2",
"compatibility": [
{
"range": ">=0.0.0"
}
]
},
"sdk": {
"latest": "0.26.9",
"compatibility": [
{
"range": ">=0.0.0"
}
]
}
}
Loading