@basictech/react 0.7.0-beta.1 → 0.7.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.ts CHANGED
@@ -1,38 +1,13 @@
1
1
  import { useState } from "react";
2
2
  import { useBasic, BasicProvider, BasicStorage, LocalStorageAdapter } from "./AuthContext";
3
3
  import { useLiveQuery as useQuery } from "dexie-react-hooks";
4
-
5
- // const useQuery = (queryable: any) => {
6
- // const [loading, setLoading] = useState(true)
7
- // const [error, setError] = useState<Error | null>(null)
8
-
9
- // const result = useLiveQuery(async () => {
10
- // try {
11
- // setLoading(true)
12
- // setError(null)
13
-
14
- // // if (typeof queryable === 'function') {
15
- // // return await queryable()
16
- // // }
17
- // return queryable
18
-
19
- // } catch (err) {
20
- // setError(err instanceof Error ? err : new Error('Unknown error'))
21
- // return undefined
22
- // } finally {
23
- // setLoading(false)
24
- // }
25
- // }, [queryable])
26
-
27
- // return {
28
- // data: result,
29
- // loading,
30
- // error
31
- // }
32
- // }
33
-
4
+ // import { createVersionUpdater, VersionUpdater, Migration } from "./versionUpdater";
34
5
 
35
6
 
36
7
  export {
37
- useBasic, BasicProvider, useQuery, BasicStorage, LocalStorageAdapter
8
+ useBasic, BasicProvider, useQuery
38
9
  }
10
+
11
+ // export type {
12
+ // VersionUpdater, Migration
13
+ // }
package/src/sync/index.ts CHANGED
@@ -8,7 +8,7 @@ import 'dexie-observable';
8
8
  import { syncProtocol } from './syncProtocol'
9
9
  import { log } from '../config'
10
10
 
11
- import { validateSchema, validateData } from '../schema'
11
+ import { validateSchema, validateData } from '@basictech/schema'
12
12
  syncProtocol()
13
13
 
14
14
 
@@ -0,0 +1,22 @@
1
+ import { BasicStorage } from '../utils/storage'
2
+ import { Migration } from './versionUpdater'
3
+
4
+
5
+ export const addMigrationTimestamp: Migration = {
6
+ fromVersion: '0.6.0',
7
+ toVersion: '0.7.0',
8
+ async migrate(storage: BasicStorage) {
9
+ console.log('Running test migration')
10
+ storage.set('test_migration', 'true')
11
+ }
12
+ }
13
+
14
+
15
+ /**
16
+ * Get all available migrations
17
+ */
18
+ export function getMigrations(): Migration[] {
19
+ return [
20
+ addMigrationTimestamp
21
+ ]
22
+ }
@@ -0,0 +1,160 @@
1
+ import { BasicStorage } from '../utils/storage'
2
+
3
+ export interface VersionInfo {
4
+ version: string
5
+ lastUpdated: number
6
+ }
7
+
8
+ export interface Migration {
9
+ fromVersion: string
10
+ toVersion: string
11
+ migrate: (storage: BasicStorage) => Promise<void>
12
+ }
13
+
14
+ export class VersionUpdater {
15
+ private storage: BasicStorage
16
+ private currentVersion: string
17
+ private migrations: Migration[]
18
+ private versionKey = 'basic_app_version'
19
+
20
+ constructor(storage: BasicStorage, currentVersion: string, migrations: Migration[] = []) {
21
+ this.storage = storage
22
+ this.currentVersion = currentVersion
23
+ this.migrations = migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion))
24
+ }
25
+
26
+ /**
27
+ * Check current stored version and run migrations if needed
28
+ * Only compares major.minor versions, ignoring beta/prerelease parts
29
+ * Example: "0.7.0-beta.1" and "0.7.0" are treated as the same version
30
+ */
31
+ async checkAndUpdate(): Promise<{ updated: boolean; fromVersion?: string; toVersion: string }> {
32
+ const storedVersion = await this.getStoredVersion()
33
+
34
+ if (!storedVersion) {
35
+ // First time setup
36
+ await this.setStoredVersion(this.currentVersion)
37
+ return { updated: false, toVersion: this.currentVersion }
38
+ }
39
+
40
+ if (storedVersion === this.currentVersion) {
41
+ return { updated: false, toVersion: this.currentVersion }
42
+ }
43
+
44
+ // Need to run migrations
45
+ const migrationsToRun = this.getMigrationsToRun(storedVersion, this.currentVersion)
46
+
47
+ if (migrationsToRun.length === 0) {
48
+ // No migrations needed, just update version
49
+ await this.setStoredVersion(this.currentVersion)
50
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion }
51
+ }
52
+
53
+ // Run migrations
54
+ for (const migration of migrationsToRun) {
55
+ try {
56
+ console.log(`Running migration from ${migration.fromVersion} to ${migration.toVersion}`)
57
+ await migration.migrate(this.storage)
58
+ } catch (error) {
59
+ console.error(`Migration failed from ${migration.fromVersion} to ${migration.toVersion}:`, error)
60
+ throw new Error(`Migration failed: ${error}`)
61
+ }
62
+ }
63
+
64
+ // Update to current version
65
+ await this.setStoredVersion(this.currentVersion)
66
+ return { updated: true, fromVersion: storedVersion, toVersion: this.currentVersion }
67
+ }
68
+
69
+ private async getStoredVersion(): Promise<string | null> {
70
+ try {
71
+ const versionData = await this.storage.get(this.versionKey)
72
+ if (!versionData) return null
73
+
74
+ const versionInfo: VersionInfo = JSON.parse(versionData)
75
+ return versionInfo.version
76
+ } catch (error) {
77
+ console.warn('Failed to get stored version:', error)
78
+ return null
79
+ }
80
+ }
81
+
82
+ private async setStoredVersion(version: string): Promise<void> {
83
+ const versionInfo: VersionInfo = {
84
+ version,
85
+ lastUpdated: Date.now()
86
+ }
87
+ await this.storage.set(this.versionKey, JSON.stringify(versionInfo))
88
+ }
89
+
90
+ private getMigrationsToRun(fromVersion: string, toVersion: string): Migration[] {
91
+ return this.migrations.filter(migration => {
92
+ // Migration should run if we're crossing the version boundary
93
+ // i.e., stored version is less than migration.toVersion AND current version is >= migration.toVersion
94
+ const storedLessThanMigrationTo = this.compareVersions(fromVersion, migration.toVersion) < 0
95
+ const currentGreaterThanOrEqualMigrationTo = this.compareVersions(toVersion, migration.toVersion) >= 0
96
+
97
+ console.log(`Checking migration ${migration.fromVersion} → ${migration.toVersion}:`)
98
+ console.log(` stored ${fromVersion} < migration.to ${migration.toVersion}: ${storedLessThanMigrationTo}`)
99
+ console.log(` current ${toVersion} >= migration.to ${migration.toVersion}: ${currentGreaterThanOrEqualMigrationTo}`)
100
+
101
+ const shouldRun = storedLessThanMigrationTo && currentGreaterThanOrEqualMigrationTo
102
+ console.log(` Should run: ${shouldRun}`)
103
+
104
+ return shouldRun
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Simple semantic version comparison (major.minor only, ignoring beta/prerelease)
110
+ * Returns: -1 if a < b, 0 if a === b, 1 if a > b
111
+ */
112
+ private compareVersions(a: string, b: string): number {
113
+ // Extract major.minor from version strings, ignoring beta/prerelease parts
114
+ const aMajorMinor = this.extractMajorMinor(a)
115
+ const bMajorMinor = this.extractMajorMinor(b)
116
+
117
+ // Compare major version first
118
+ if (aMajorMinor.major !== bMajorMinor.major) {
119
+ return aMajorMinor.major - bMajorMinor.major
120
+ }
121
+
122
+ // Then compare minor version
123
+ return aMajorMinor.minor - bMajorMinor.minor
124
+ }
125
+
126
+ /**
127
+ * Extract major.minor from version string, ignoring beta/prerelease
128
+ * Examples: "0.7.0-beta.1" -> {major: 0, minor: 7}
129
+ * "1.2.3" -> {major: 1, minor: 2}
130
+ */
131
+ private extractMajorMinor(version: string): { major: number, minor: number } {
132
+ // Remove beta/prerelease parts and split by dots
133
+ const cleanVersion = version.split('-')[0]?.split('+')[0] || version
134
+ const parts = cleanVersion.split('.').map(Number)
135
+
136
+ return {
137
+ major: parts[0] || 0,
138
+ minor: parts[1] || 0
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Add a migration to the updater
144
+ */
145
+ addMigration(migration: Migration): void {
146
+ this.migrations.push(migration)
147
+ this.migrations.sort((a, b) => this.compareVersions(a.fromVersion, b.fromVersion))
148
+ }
149
+ }
150
+
151
+ /**
152
+ * Create a simple version updater instance
153
+ */
154
+ export function createVersionUpdater(
155
+ storage: BasicStorage,
156
+ currentVersion: string,
157
+ migrations: Migration[] = []
158
+ ): VersionUpdater {
159
+ return new VersionUpdater(storage, currentVersion, migrations)
160
+ }
@@ -0,0 +1,82 @@
1
+ // Network utilities for Basic React package
2
+ import { log } from '../config'
3
+ import { version as currentVersion } from '../../package.json'
4
+
5
+ export function isDevelopment(debug?: boolean): boolean {
6
+ return (
7
+ window.location.hostname === 'localhost' ||
8
+ window.location.hostname === '127.0.0.1' ||
9
+ window.location.hostname.includes('localhost') ||
10
+ window.location.hostname.includes('127.0.0.1') ||
11
+ window.location.hostname.includes('.local') ||
12
+ process.env.NODE_ENV === 'development' ||
13
+ debug === true
14
+ )
15
+ }
16
+
17
+ export async function checkForNewVersion(): Promise<{
18
+ hasNewVersion: boolean,
19
+ latestVersion: string | null,
20
+ currentVersion: string | null
21
+ }> {
22
+ try {
23
+ const isBeta = currentVersion.includes('beta')
24
+
25
+ const response = await fetch(`https://registry.npmjs.org/@basictech/react/${isBeta ? 'beta' : 'latest'}`);
26
+ if (!response.ok) {
27
+ throw new Error('Failed to fetch version from npm');
28
+ }
29
+
30
+ const data = await response.json();
31
+ const latestVersion = data.version;
32
+
33
+ if (latestVersion !== currentVersion) {
34
+ console.warn('[basic] New version available:', latestVersion, `\nrun "npm install @basictech/react@${latestVersion}" to update`);
35
+ }
36
+ if (isBeta) {
37
+ log('thank you for being on basictech/react beta :)')
38
+ }
39
+
40
+ return {
41
+ hasNewVersion: currentVersion !== latestVersion,
42
+ latestVersion,
43
+ currentVersion
44
+ };
45
+ } catch (error) {
46
+ log('Error checking for new version:', error);
47
+ return {
48
+ hasNewVersion: false,
49
+ latestVersion: null,
50
+ currentVersion: null
51
+ };
52
+ }
53
+ }
54
+
55
+ export function cleanOAuthParamsFromUrl(): void {
56
+ if (window.location.search.includes('code') || window.location.search.includes('state')) {
57
+ const url = new URL(window.location.href)
58
+ url.searchParams.delete('code')
59
+ url.searchParams.delete('state')
60
+ window.history.pushState({}, document.title, url.pathname + url.search)
61
+ log('Cleaned OAuth parameters from URL')
62
+ }
63
+ }
64
+
65
+ export function getSyncStatus(statusCode: number): string {
66
+ switch (statusCode) {
67
+ case -1:
68
+ return "ERROR";
69
+ case 0:
70
+ return "OFFLINE";
71
+ case 1:
72
+ return "CONNECTING";
73
+ case 2:
74
+ return "ONLINE";
75
+ case 3:
76
+ return "SYNCING";
77
+ case 4:
78
+ return "ERROR_WILL_RETRY";
79
+ default:
80
+ return "UNKNOWN";
81
+ }
82
+ }
@@ -0,0 +1,120 @@
1
+ // Schema utilities for Basic React package
2
+ import { validateSchema, compareSchemas } from '@basictech/schema'
3
+ import { log } from '../config'
4
+
5
+ export async function getSchemaStatus(schema: any): Promise<{
6
+ valid: boolean,
7
+ status: string,
8
+ latest: any
9
+ }> {
10
+ const projectId = schema.project_id
11
+ const valid = validateSchema(schema)
12
+
13
+ if (!valid.valid) {
14
+ console.warn('BasicDB Error: your local schema is invalid. Please fix errors and try again - sync is disabled')
15
+ return {
16
+ valid: false,
17
+ status: 'invalid',
18
+ latest: null
19
+ }
20
+ }
21
+
22
+ const latestSchema = await fetch(`https://api.basic.tech/project/${projectId}/schema`)
23
+ .then(res => res.json())
24
+ .then(data => data.data[0].schema)
25
+ .catch(err => {
26
+ return {
27
+ valid: false,
28
+ status: 'error',
29
+ latest: null
30
+ }
31
+ })
32
+
33
+ console.log('latestSchema', latestSchema)
34
+
35
+ if (!latestSchema.version) {
36
+ return {
37
+ valid: false,
38
+ status: 'error',
39
+ latest: null
40
+ }
41
+ }
42
+
43
+ if (latestSchema.version > schema.version) {
44
+ // error_code: schema_behind
45
+ console.warn('BasicDB Error: your local schema version is behind the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
46
+ return {
47
+ valid: false,
48
+ status: 'behind',
49
+ latest: latestSchema
50
+ }
51
+ } else if (latestSchema.version < schema.version) {
52
+ // error_code: schema_ahead
53
+ console.warn('BasicDB Error: your local schema version is ahead of the latest. Found version:', schema.version, 'but expected', latestSchema.version, " - sync is disabled")
54
+ return {
55
+ valid: false,
56
+ status: 'ahead',
57
+ latest: latestSchema
58
+ }
59
+ } else if (latestSchema.version === schema.version) {
60
+ const changes = compareSchemas(schema, latestSchema)
61
+ if (changes.valid) {
62
+ return {
63
+ valid: true,
64
+ status: 'current',
65
+ latest: latestSchema
66
+ }
67
+ } else {
68
+ // error_code: schema_conflict
69
+ console.warn('BasicDB Error: your local schema is conflicting with the latest. Your version:', schema.version, 'does not match origin version', latestSchema.version, " - sync is disabled")
70
+ return {
71
+ valid: false,
72
+ status: 'conflict',
73
+ latest: latestSchema
74
+ }
75
+ }
76
+ } else {
77
+ return {
78
+ valid: false,
79
+ status: 'error',
80
+ latest: null
81
+ }
82
+ }
83
+ }
84
+
85
+ export async function validateAndCheckSchema(schema: any): Promise<{
86
+ isValid: boolean,
87
+ schemaStatus: { valid: boolean, status?: string, latest?: any },
88
+ errors?: any[]
89
+ }> {
90
+ const valid = validateSchema(schema)
91
+ if (!valid.valid) {
92
+ log('Basic Schema is invalid!', valid.errors)
93
+ console.group('Schema Errors')
94
+ let errorMessage = ''
95
+ valid.errors.forEach((error, index) => {
96
+ log(`${index + 1}:`, error.message, ` - at ${error.instancePath}`)
97
+ errorMessage += `${index + 1}: ${error.message} - at ${error.instancePath}\n`
98
+ })
99
+ console.groupEnd()
100
+
101
+ return {
102
+ isValid: false,
103
+ schemaStatus: { valid: false },
104
+ errors: valid.errors
105
+ }
106
+ }
107
+
108
+ let schemaStatus = { valid: false }
109
+ if (schema.version !== 0) {
110
+ schemaStatus = await getSchemaStatus(schema)
111
+ log('schemaStatus', schemaStatus)
112
+ } else {
113
+ log("schema not published - at version 0")
114
+ }
115
+
116
+ return {
117
+ isValid: true,
118
+ schemaStatus
119
+ }
120
+ }
@@ -0,0 +1,62 @@
1
+ // Storage utilities for Basic React package
2
+ export interface BasicStorage {
3
+ get(key: string): Promise<string | null>
4
+ set(key: string, value: string): Promise<void>
5
+ remove(key: string): Promise<void>
6
+ }
7
+
8
+ export class LocalStorageAdapter implements BasicStorage {
9
+ async get(key: string): Promise<string | null> {
10
+ return localStorage.getItem(key)
11
+ }
12
+
13
+ async set(key: string, value: string): Promise<void> {
14
+ localStorage.setItem(key, value)
15
+ }
16
+
17
+ async remove(key: string): Promise<void> {
18
+ localStorage.removeItem(key)
19
+ }
20
+ }
21
+
22
+ export const STORAGE_KEYS = {
23
+ REFRESH_TOKEN: 'basic_refresh_token',
24
+ USER_INFO: 'basic_user_info',
25
+ AUTH_STATE: 'basic_auth_state',
26
+ DEBUG: 'basic_debug'
27
+ } as const
28
+
29
+ export function getCookie(name: string): string {
30
+ let cookieValue = '';
31
+ if (document.cookie && document.cookie !== '') {
32
+ const cookies = document.cookie.split(';');
33
+ for (let i = 0; i < cookies.length; i++) {
34
+ const cookie = cookies[i]?.trim();
35
+ if (cookie && cookie.substring(0, name.length + 1) === (name + '=')) {
36
+ cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
37
+ break;
38
+ }
39
+ }
40
+ }
41
+ return cookieValue;
42
+ }
43
+
44
+ export function setCookie(name: string, value: string, options?: { secure?: boolean, sameSite?: string, httpOnly?: boolean }): void {
45
+ const opts = {
46
+ secure: true,
47
+ sameSite: 'Strict',
48
+ httpOnly: false,
49
+ ...options
50
+ };
51
+
52
+ let cookieString = `${name}=${value}`;
53
+ if (opts.secure) cookieString += '; Secure';
54
+ if (opts.sameSite) cookieString += `; SameSite=${opts.sameSite}`;
55
+ if (opts.httpOnly) cookieString += '; HttpOnly';
56
+
57
+ document.cookie = cookieString;
58
+ }
59
+
60
+ export function clearCookie(name: string): void {
61
+ document.cookie = `${name}=; Secure; SameSite=Strict`;
62
+ }
package/src/schema.ts DELETED
@@ -1,159 +0,0 @@
1
- // Basic Schema Library
2
- // utils for validating and interacting with Basic schemas
3
- import Ajv, { ErrorObject } from 'ajv'
4
-
5
- const basicJsonSchema = {
6
- "$schema": "http://json-schema.org/draft-07/schema#",
7
- "type": "object",
8
- "properties": {
9
- "project_id": {
10
- "type": "string"
11
- },
12
- "namespace": {
13
- "type": "string",
14
- },
15
- "version": {
16
- "type": "integer",
17
- "minimum": 0
18
- },
19
- "tables": {
20
- "type": "object",
21
- "patternProperties": {
22
- "^[a-zA-Z0-9_]+$": {
23
- "type": "object",
24
- "properties": {
25
- "name": {
26
- "type": "string"
27
- },
28
- "type": {
29
- "type": "string",
30
- "enum": ["collection"]
31
- },
32
- "fields": {
33
- "type": "object",
34
- "patternProperties": {
35
- "^[a-zA-Z0-9_]+$": {
36
- "type": "object",
37
- "properties": {
38
- "type": {
39
- "type": "string",
40
- "enum": ["string", "boolean", "number", "json"]
41
- },
42
- "indexed": {
43
- "type": "boolean"
44
- },
45
- "required": {
46
- "type": "boolean"
47
- }
48
- },
49
- "required": ["type"]
50
- }
51
- },
52
- "additionalProperties": true
53
- }
54
- },
55
- "required": ["fields"]
56
- }
57
- },
58
- "additionalProperties": true
59
- }
60
- },
61
- "required": ["project_id", "version", "tables"]
62
- }
63
- const ajv = new Ajv()
64
- const validator = ajv.compile(basicJsonSchema)
65
-
66
- type Schema = typeof basicJsonSchema
67
-
68
- function generateEmptySchema() {
69
-
70
- }
71
-
72
-
73
- /**
74
- * Validate a schema
75
- * only checks if the schema is formatted correctly, not if can be published
76
- * @param schema - The schema to validate
77
- * @returns {valid: boolean, errors: any[]} - The validation result
78
- */
79
- function validateSchema(schema: Schema) : {valid: boolean, errors: ErrorObject[]} {
80
- const v = validator(schema)
81
- return {
82
- valid: v,
83
- errors: validator.errors || []
84
- }
85
- }
86
-
87
- // type ErrorObject = {
88
- // keyword: string;
89
- // instancePath: string;
90
- // schemaPath: string;
91
- // params: Record<string, any>;
92
- // propertyName?: string;
93
- // message?: string;
94
- // schema?: any;
95
- // parentSchema?: any;
96
- // data?: any;
97
- // }
98
-
99
-
100
- function validateData(schema: any, table: string, data: Record<string, any>, checkRequired: boolean = true) {
101
- const valid = validateSchema(schema)
102
- if (!valid.valid) {
103
- return { valid: false, errors: valid.errors, message: "Schema is invalid" }
104
- }
105
-
106
- const tableSchema = schema.tables[table]
107
-
108
- if (!tableSchema) {
109
- return { valid: false, errors: [{ message: `Table ${table} not found in schema` }], message: "Table not found" }
110
- }
111
-
112
- for (const [fieldName, fieldValue] of Object.entries(data)) {
113
- const fieldSchema = tableSchema.fields[fieldName]
114
-
115
- if (!fieldSchema) {
116
- return {
117
- valid: false,
118
- errors: [{ message: `Field ${fieldName} not found in schema` }],
119
- message: "Invalid field"
120
- }
121
- }
122
-
123
- const schemaType = fieldSchema.type
124
- const valueType = typeof fieldValue
125
-
126
- if (
127
- (schemaType === 'string' && valueType !== 'string') ||
128
- (schemaType === 'number' && valueType !== 'number') ||
129
- (schemaType === 'boolean' && valueType !== 'boolean') ||
130
- (schemaType === 'json' && valueType !== 'object')
131
- ) {
132
- return {
133
- valid: false,
134
- errors: [{
135
- message: `Field ${fieldName} should be type ${schemaType}, got ${valueType}`
136
- }],
137
- message: "invalid type"
138
- }
139
- }
140
- }
141
-
142
- if (checkRequired) {
143
- for (const [fieldName, fieldSchema] of Object.entries(tableSchema.fields)) {
144
- if ((fieldSchema as { required?: boolean }).required && !data[fieldName]) {
145
- return { valid: false, errors: [{ message: `Field ${fieldName} is required` }], message: "Required field missing" }
146
- }
147
- }
148
- }
149
-
150
- return { valid: true, errors: [] }
151
- }
152
-
153
-
154
- export {
155
- validateSchema,
156
- validateData,
157
- generateEmptySchema
158
- }
159
-