├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc ├── .releaserc.json ├── LICENSE ├── README.md ├── RELEASE.md ├── jest.config.ts ├── package-lock.json ├── package.json ├── src ├── SupabaseClient.ts ├── functions-js │ └── src │ │ ├── FunctionsClient.ts │ │ ├── helper.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── version.ts ├── gotrue-js │ └── src │ │ ├── GoTrueAdminApi.ts │ │ ├── GoTrueClient.ts │ │ ├── index.ts │ │ └── lib │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── fetch.ts │ │ ├── helpers.ts │ │ ├── local-storage.ts │ │ ├── polyfills.ts │ │ ├── types.ts │ │ └── version.ts ├── index.ts ├── lib │ ├── SupabaseAuthClient.ts │ ├── constants.ts │ ├── fetch.ts │ ├── helpers.ts │ ├── types.ts │ └── version.ts ├── postgrest-js │ └── src │ │ ├── PostgrestBuilder.ts │ │ ├── PostgrestClient.ts │ │ ├── PostgrestFilterBuilder.ts │ │ ├── PostgrestQueryBuilder.ts │ │ ├── PostgrestTransformBuilder.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ ├── select-query-parser.ts │ │ ├── types.ts │ │ └── version.ts ├── realtime-js │ └── src │ │ ├── RealtimeChannel.ts │ │ ├── RealtimeClient.ts │ │ ├── RealtimePresence.ts │ │ ├── index.ts │ │ └── lib │ │ ├── constants.ts │ │ ├── push.ts │ │ ├── serializer.ts │ │ ├── timer.ts │ │ ├── transformers.ts │ │ └── version.ts ├── storage-js │ └── src │ │ ├── StorageClient.ts │ │ ├── index.ts │ │ ├── lib │ │ ├── constants.ts │ │ ├── errors.ts │ │ ├── fetch.ts │ │ ├── formData.js │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── mimeMap.js │ │ ├── types.ts │ │ └── version.ts │ │ └── packages │ │ ├── StorageBucketApi.ts │ │ └── StorageFileApi.ts ├── wechaturl-parse │ ├── index.js │ └── lib │ │ └── bootstrap │ │ ├── browser.js │ │ ├── constants.js │ │ ├── errors.js │ │ ├── node.js │ │ ├── querystring.js │ │ ├── querystringify-wechat.js │ │ ├── url-parse-wechat.js │ │ ├── util.js │ │ └── validators.js └── wefetch.js ├── tsconfig.json ├── tsconfig.module.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .DS_Store 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | 76 | # parcel-bundler cache (https://parceljs.org/) 77 | .cache 78 | 79 | # Next.js build output 80 | .next 81 | 82 | # Nuxt.js build / generate output 83 | .nuxt 84 | dist 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port 106 | 107 | docs/v2 108 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.log 2 | npm-debug.log* 3 | 4 | # Coverage directory used by tools like istanbul 5 | coverage 6 | .nyc_output 7 | 8 | # Dependency directories 9 | node_modules 10 | 11 | # npm package lock 12 | package-lock.json 13 | yarn.lock 14 | 15 | # project files 16 | src 17 | test 18 | examples 19 | example-next-js 20 | umd_temp 21 | CHANGELOG.md 22 | .travis.yml 23 | .editorconfig 24 | .eslintignore 25 | .eslintrc 26 | .babelrc 27 | .gitignore 28 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .expo 2 | .next 3 | node_modules 4 | package-lock.json 5 | docker* -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": [ 3 | { "name": "master" }, 4 | { "name": "next", "channel": "next", "prerelease": true }, 5 | { "name": "rc", "channel": "rc", "prerelease": true } 6 | ], 7 | "plugins": [ 8 | [ 9 | "semantic-release-plugin-update-version-in-files", 10 | { 11 | "files": [ 12 | "src/lib/version.ts", 13 | "dist/main/lib/version.js", 14 | "dist/main/lib/version.d.ts", 15 | "dist/module/lib/version.js", 16 | "dist/module/lib/version.d.ts", 17 | "dist/umd/supabase.js" 18 | ], 19 | "placeholder": "0.0.0-automated" 20 | } 21 | ], 22 | "@semantic-release/commit-analyzer", 23 | "@semantic-release/release-notes-generator", 24 | [ 25 | "@semantic-release/github", 26 | { 27 | "successComment": false, 28 | "releasedLabels": false, 29 | "failTitle": false, 30 | "addReleases": false 31 | } 32 | ], 33 | "@semantic-release/npm" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Supabase 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `supabase-wechat-stable-v2` 2 | 3 | 4 | 一个用于Supabase的同构的微信客户端。 5 | 6 | 7 | ## Usage 8 | 9 | 首先,你需要安装这个库。 10 | 11 | ```sh 12 | npm install supabase-wechat-stable-v2 13 | ``` 14 | 15 | 然后你就可以导入库并与数据库建立连接。 16 | 17 | ```js 18 | import { createClient } from 'supabase-wechat-stable-v2' 19 | 20 | // Create a single supabase client for interacting with your database 21 | const supabase = createClient('https://xyzcompany.supabase.co', 'public-anon-key') 22 | ``` 23 | 查询数据(举例) 24 | 25 | ```js 26 | 27 | const { data, error } = await supabase 28 | .from('countries') 29 | .select() 30 | 31 | ``` 32 | 33 | 34 | ## Sponsors 35 | 36 | We are building the features of Firebase using enterprise-grade, open source products. We support existing communities wherever possible, and if the products don’t exist we build them and open source them ourselves. Thanks to these sponsors who are making the OSS ecosystem better for everyone. 37 | 38 | [![New Sponsor](https://user-images.githubusercontent.com/10214025/90518111-e74bbb00-e198-11ea-8f88-c9e3c1aa4b5b.png)](https://github.com/sponsors/supabase) 39 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Releases 2 | 3 | Releases are handled by Semantic release. This document is for forcing and documenting any non-code changes. 4 | 5 | ## 1.11.8 6 | 7 | - Implement @supabase/storage-js 8 | 9 | ## 1.4.0 10 | 11 | - Ability to redirect a user to a specified location after confirmation 12 | 13 | ## 1.1.4 14 | 15 | - bump @supabase/gotrue-js from 1.9.3.2 to [1.10.1](https://github.com/supabase/gotrue-js/releases/tag/v1.10.1) 16 | - Includes Next.js/Express helpers and Password reset helpers 17 | 18 | ## 1.1.3 19 | 20 | - bump @supabase/postgrest-js from 0.21.2 to 0.22.0 21 | - Fix: Added 'AuthUser' to the exports 22 | 23 | ## 1.0.6 24 | 25 | - Bumps gotrue-js so that it works with React Native: https://github.com/supabase/gotrue-js/pull/26 26 | 27 | ## 1.0.5 28 | 29 | - Adds local storage options for Auth. 30 | 31 | ## 1.0.4 32 | 33 | - Upgrades postgrest-js dependency to fix ordering for JSON columns. (https://github.com/supabase/postgrest-js/pull/132) 34 | 35 | ## 1.0.2 36 | 37 | - Fixes link in readme for NPM. 38 | 39 | ## 1.0.1 - Improved DX 40 | 41 | - Upgraded the `supabase.auth` to [gotrue-js](https://github.com/supabase/gotrue-js) - supports Oath logins & more 42 | - We always return errors, not throwing errors. 43 | - We only generate one socket connection per supabase client. 44 | - Native typescript 45 | - Fixes #32 Major DX change: response and error handling 46 | - Fixes #49 When no `supabaseKey` is passed in it throws an error 47 | - Fixes #31 chore: set up semantic releases 48 | - Fixes #15 `supabase.auth.logout()` throws "Invalid user" error. 49 | - Fixes #20 Auth: Change DX of user management 50 | - Fixes #30 Supabase auth interface missing informiation 51 | - Fixes https://github.com/supabase/supabase/issues/147 https://github.com/supabase/supabase/issues/147 52 | - Partial fix for https://github.com/supabase/realtime-js/issues/53 - if there is no token provided. The error needs to be caught at a socket level. 53 | 54 | #### Breaking changes 55 | 56 | **`body` is now `data`** 57 | 58 | Previously: 59 | 60 | ```jsx 61 | const { body } = supabase.from('todos').select('*') 62 | ``` 63 | 64 | Now: 65 | 66 | ```jsx 67 | const { data } = supabase.from('todos').select('*') 68 | ``` 69 | 70 | **Errors are returned not thrown** 71 | 72 | Previously: 73 | 74 | ```jsx 75 | try { 76 | const { body } = supabase.from('todos').select('*') 77 | } catch (error) { 78 | console.log(error) 79 | } 80 | ``` 81 | 82 | Now: 83 | 84 | ```jsx 85 | const { data, error } = supabase.from('todos').select('*') 86 | if (error) console.log(error) 87 | ``` 88 | 89 | **`ova()` and `ovr()` are now just `ov()`** 90 | 91 | Previously: 92 | 93 | ```jsx 94 | try { 95 | const { body } = supabase.from('todos').select('*').ovr('population_range_millions', [150, 250]) 96 | } catch (error) { 97 | console.log(error) 98 | } 99 | ``` 100 | 101 | Now: 102 | 103 | ```jsx 104 | const { data, error } = supabase 105 | .from('todos') 106 | .select('*') 107 | .ov('population_range_millions', [150, 250]) 108 | if (error) console.log(error) 109 | ``` 110 | 111 | **`offset()` is removed** 112 | 113 | You can now use range() instead of `limit()` + `offset()` 114 | 115 | **`ova()` and `ovr()` are now just `ov()`** 116 | 117 | Previously: 118 | 119 | ```js 120 | let countries = await supabase.from('cities').select('name').offset(10).limit(10) 121 | ``` 122 | 123 | Now: 124 | 125 | ```js 126 | let countries = await supabase.from('cities').select('name').range(10, 20) 127 | ``` 128 | 129 | **`signup()` is now `signUp()` and `email` / `password` is passed as an object** 130 | 131 | Previously: 132 | 133 | ```jsx 134 | const { 135 | body: { user }, 136 | } = await supabase.auth.signup('someone@email.com', 'password') 137 | ``` 138 | 139 | Now: 140 | 141 | ```jsx 142 | const { user, error } = await supabase.auth.signUp({ 143 | email: 'someone@email.com', 144 | password: 'password', 145 | }) 146 | ``` 147 | 148 | **`login()` is now `signIn()` and `email` / `password` is passed as an object** 149 | 150 | Previously: 151 | 152 | ```jsx 153 | const { 154 | body: { user }, 155 | } = await supabase.auth.signup('someone@email.com', 'password') 156 | ``` 157 | 158 | Now: 159 | 160 | ```jsx 161 | const { user, error } = await supabase.auth.signIn({ 162 | email: 'someone@email.com', 163 | password: 'password', 164 | }) 165 | ``` 166 | 167 | **`logout()` is now `signOut()`** 168 | 169 | Previously: 170 | 171 | ```jsx 172 | await supabase.auth.logout() 173 | ``` 174 | 175 | Now: 176 | 177 | ```jsx 178 | await supabase.auth.signOut() 179 | ``` 180 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from '@jest/types' 2 | 3 | const config: Config.InitialOptions = { 4 | preset: 'ts-jest', 5 | testEnvironment: 'node', 6 | clearMocks: true, 7 | collectCoverage: false, 8 | coverageDirectory: './test/coverage', 9 | coverageReporters: ['json', 'html', 'lcov'], 10 | collectCoverageFrom: [ 11 | './src/**/*.{js,ts}', 12 | './src/**/*.unit.test.ts', 13 | '!**/node_modules/**', 14 | '!**/vendor/**', 15 | '!**/vendor/**', 16 | ], 17 | } 18 | export default config 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "supabase-wechat-stable-v2", 3 | "version": "2.1.4", 4 | "description": "wechat Javascript client for Supabase", 5 | "keywords": [ 6 | "javascript", 7 | "typescript", 8 | "supabase" 9 | ], 10 | "homepage": "https://github.com/supabase/supabase-js", 11 | "bugs": "https://github.com/supabase/supabase-js/issues", 12 | "license": "MIT", 13 | "author": "Supabase", 14 | "files": [ 15 | "dist", 16 | "src" 17 | ], 18 | "main": "dist/main/index.js", 19 | "module": "dist/module/index.js", 20 | "miniprogram": "./dist/main", 21 | "types": "dist/module/index.d.ts", 22 | "sideEffects": false, 23 | "repository": "supabase/supabase-js", 24 | "scripts": { 25 | "clean": "rimraf dist docs", 26 | "format": "prettier --write \"{src,test}/**/*.ts\"", 27 | "build": "run-s clean format build:*", 28 | "build:main": "tsc -p tsconfig.json", 29 | "build:module": "tsc -p tsconfig.module.json", 30 | "build:umd": "webpack", 31 | "types-generate": "dts-gen -m '@supabase/supabase-js' -s", 32 | "test": "jest --runInBand", 33 | "test:coverage": "jest --runInBand --coverage", 34 | "test:db": "cd infra/db && docker-compose down && docker-compose up -d && sleep 5", 35 | "test:watch": "jest --watch --verbose false --silent false", 36 | "test:clean": "cd infra/db && docker-compose down", 37 | "docs": "typedoc --entryPoints src/index.ts --out docs/v2 --includes src/**/*.ts", 38 | "docs:json": "typedoc --entryPoints src/index.ts --includes src/**/*.ts --json docs/v2/spec.json --excludeExternals" 39 | }, 40 | "dependencies": { 41 | "phoenix": "^1.6.16" 42 | }, 43 | "devDependencies": { 44 | "@types/jest": "^29.2.5", 45 | "husky": "^4.3.0", 46 | "jest": "^29.3.1", 47 | "npm-run-all": "^4.1.5", 48 | "prettier": "^2.5.1", 49 | "pretty-quick": "^3.1.3", 50 | "rimraf": "^3.0.2", 51 | "semantic-release-plugin-update-version-in-files": "^1.1.0", 52 | "ts-jest": "^29.0.5", 53 | "ts-loader": "^8.0.11", 54 | "ts-node": "^10.9.1", 55 | "typedoc": "^0.22.16", 56 | "typescript": "^4.5.5", 57 | "webpack": "^5.69.1", 58 | "webpack-cli": "^4.9.2" 59 | }, 60 | "husky": { 61 | "hooks": { 62 | "pre-commit": "pretty-quick --staged" 63 | } 64 | }, 65 | "jsdelivr": "dist/umd/supabase.js", 66 | "unpkg": "dist/umd/supabase.js" 67 | } 68 | -------------------------------------------------------------------------------- /src/SupabaseClient.ts: -------------------------------------------------------------------------------- 1 | import { FunctionsClient } from './functions-js/src/index' 2 | import { AuthChangeEvent } from './gotrue-js/src/index' 3 | import { 4 | PostgrestClient, 5 | PostgrestFilterBuilder, 6 | PostgrestQueryBuilder, 7 | } from './postgrest-js/src/index' 8 | import { 9 | RealtimeChannel, 10 | RealtimeChannelOptions, 11 | RealtimeClient, 12 | RealtimeClientOptions, 13 | } from './realtime-js/src/index' 14 | import { StorageClient as SupabaseStorageClient } from './storage-js/src/index' 15 | import { DEFAULT_HEADERS } from './lib/constants' 16 | import { fetchWithAuth } from './lib/fetch' 17 | import { stripTrailingSlash, applySettingDefaults } from './lib/helpers' 18 | import { SupabaseAuthClient } from './lib/SupabaseAuthClient' 19 | import { Fetch, GenericSchema, SupabaseClientOptions, SupabaseAuthClientOptions } from './lib/types' 20 | let { URL } = require('./wechaturl-parse/index') 21 | 22 | const DEFAULT_GLOBAL_OPTIONS = { 23 | headers: DEFAULT_HEADERS, 24 | } 25 | 26 | const DEFAULT_DB_OPTIONS = { 27 | schema: 'public', 28 | } 29 | 30 | const DEFAULT_AUTH_OPTIONS: SupabaseAuthClientOptions = { 31 | autoRefreshToken: true, 32 | persistSession: true, 33 | detectSessionInUrl: true, 34 | } 35 | 36 | const DEFAULT_REALTIME_OPTIONS: RealtimeClientOptions = {} 37 | 38 | /** 39 | * Supabase Client. 40 | * 41 | * An isomorphic Javascript client for interacting with Postgres. 42 | */ 43 | export default class SupabaseClient< 44 | Database = any, 45 | SchemaName extends string & keyof Database = 'public' extends keyof Database 46 | ? 'public' 47 | : string & keyof Database, 48 | Schema extends GenericSchema = Database[SchemaName] extends GenericSchema 49 | ? Database[SchemaName] 50 | : any 51 | > { 52 | /** 53 | * Supabase Auth allows you to create and manage user sessions for access to data that is secured by access policies. 54 | */ 55 | auth: SupabaseAuthClient 56 | 57 | protected realtimeUrl: string 58 | protected authUrl: string 59 | protected storageUrl: string 60 | protected functionsUrl: string 61 | protected realtime: RealtimeClient 62 | protected rest: PostgrestClient 63 | protected storageKey: string 64 | protected fetch?: Fetch 65 | protected changedAccessToken: string | undefined 66 | 67 | protected headers: { 68 | [key: string]: string 69 | } 70 | 71 | /** 72 | * Create a new client for use in the browser. 73 | * @param supabaseUrl The unique Supabase URL which is supplied when you create a new project in your project dashboard. 74 | * @param supabaseKey The unique Supabase Key which is supplied when you create a new project in your project dashboard. 75 | * @param options.db.schema You can switch in between schemas. The schema needs to be on the list of exposed schemas inside Supabase. 76 | * @param options.auth.autoRefreshToken Set to "true" if you want to automatically refresh the token before expiring. 77 | * @param options.auth.persistSession Set to "true" if you want to automatically save the user session into local storage. 78 | * @param options.auth.detectSessionInUrl Set to "true" if you want to automatically detects OAuth grants in the URL and signs in the user. 79 | * @param options.realtime Options passed along to realtime-js constructor. 80 | * @param options.global.fetch A custom fetch implementation. 81 | * @param options.global.headers Any additional headers to send with each network request. 82 | */ 83 | constructor( 84 | protected supabaseUrl: string, 85 | protected supabaseKey: string, 86 | options?: SupabaseClientOptions 87 | ) { 88 | if (!supabaseUrl) throw new Error('supabaseUrl is required.') 89 | if (!supabaseKey) throw new Error('supabaseKey is required.') 90 | 91 | const _supabaseUrl = stripTrailingSlash(supabaseUrl) 92 | 93 | this.realtimeUrl = `${_supabaseUrl}/realtime/v1`.replace(/^http/i, 'ws') 94 | this.authUrl = `${_supabaseUrl}/auth/v1` 95 | this.storageUrl = `${_supabaseUrl}/storage/v1` 96 | 97 | const isPlatform = _supabaseUrl.match(/(supabase\.co)|(supabase\.in)/) 98 | if (isPlatform) { 99 | const urlParts = _supabaseUrl.split('.') 100 | this.functionsUrl = `${urlParts[0]}.functions.${urlParts[1]}.${urlParts[2]}` 101 | } else { 102 | this.functionsUrl = `${_supabaseUrl}/functions/v1` 103 | } 104 | // default storage key uses the supabase project ref as a namespace 105 | const defaultStorageKey = `sb-${new URL(this.authUrl).hostname.split('.')[0]}-auth-token` 106 | const DEFAULTS = { 107 | db: DEFAULT_DB_OPTIONS, 108 | realtime: DEFAULT_REALTIME_OPTIONS, 109 | auth: { ...DEFAULT_AUTH_OPTIONS, storageKey: defaultStorageKey }, 110 | global: DEFAULT_GLOBAL_OPTIONS, 111 | } 112 | 113 | const settings = applySettingDefaults(options ?? {}, DEFAULTS) 114 | 115 | this.storageKey = settings.auth?.storageKey ?? '' 116 | this.headers = settings.global?.headers ?? {} 117 | 118 | this.auth = this._initSupabaseAuthClient( 119 | settings.auth ?? {}, 120 | this.headers, 121 | settings.global?.fetch 122 | ) 123 | this.fetch = fetchWithAuth(supabaseKey, this._getAccessToken.bind(this), settings.global?.fetch) 124 | 125 | this.realtime = this._initRealtimeClient({ headers: this.headers, ...settings.realtime }) 126 | this.rest = new PostgrestClient(`${_supabaseUrl}/rest/v1`, { 127 | headers: this.headers, 128 | schema: settings.db?.schema, 129 | fetch: this.fetch, 130 | }) 131 | 132 | this._listenForAuthEvents() 133 | } 134 | 135 | /** 136 | * Supabase Functions allows you to deploy and invoke edge functions. 137 | */ 138 | get functions() { 139 | return new FunctionsClient(this.functionsUrl, { 140 | headers: this.headers, 141 | customFetch: this.fetch, 142 | }) 143 | } 144 | 145 | /** 146 | * Supabase Storage allows you to manage user-generated content, such as photos or videos. 147 | */ 148 | get storage() { 149 | return new SupabaseStorageClient(this.supabaseKey, this.storageUrl, this.headers, this.fetch) 150 | } 151 | 152 | /** 153 | * Perform a table operation. 154 | * 155 | * @param table The table name to operate on. 156 | */ 157 | from< 158 | TableName extends string & keyof Schema['Tables'], 159 | Table extends Schema['Tables'][TableName] 160 | >(relation: TableName): PostgrestQueryBuilder 161 | from( 162 | relation: ViewName 163 | ): PostgrestQueryBuilder 164 | from(relation: string): PostgrestQueryBuilder 165 | from(relation: string): PostgrestQueryBuilder { 166 | return this.rest.from(relation) 167 | } 168 | 169 | /** 170 | * Perform a function call. 171 | * 172 | * @param fn The function name to call. 173 | * @param args The parameters to pass to the function call. 174 | * @param options.head When set to true, no data will be returned. 175 | * @param options.count Count algorithm to use to count rows in a table. 176 | * 177 | */ 178 | rpc< 179 | FunctionName extends string & keyof Schema['Functions'], 180 | Function_ extends Schema['Functions'][FunctionName] 181 | >( 182 | fn: FunctionName, 183 | args: Function_['Args'] = {}, 184 | options?: { 185 | head?: boolean 186 | count?: 'exact' | 'planned' | 'estimated' 187 | } 188 | ): PostgrestFilterBuilder< 189 | Schema, 190 | Function_['Returns'] extends any[] 191 | ? Function_['Returns'][number] extends Record 192 | ? Function_['Returns'][number] 193 | : never 194 | : never, 195 | Function_['Returns'] 196 | > { 197 | return this.rest.rpc(fn, args, options) 198 | } 199 | 200 | /** 201 | * Creates a Realtime channel with Broadcast, Presence, and Postgres Changes. 202 | * 203 | * @param {string} name - The name of the Realtime channel. 204 | * @param {Object} opts - The options to pass to the Realtime channel. 205 | * 206 | */ 207 | channel(name: string, opts: RealtimeChannelOptions = { config: {} }): RealtimeChannel { 208 | return this.realtime.channel(name, opts) 209 | } 210 | 211 | /** 212 | * Returns all Realtime channels. 213 | */ 214 | getChannels(): RealtimeChannel[] { 215 | return this.realtime.getChannels() 216 | } 217 | 218 | /** 219 | * Unsubscribes and removes Realtime channel from Realtime client. 220 | * 221 | * @param {RealtimeChannel} channel - The name of the Realtime channel. 222 | * 223 | */ 224 | removeChannel(channel: RealtimeChannel): Promise<'ok' | 'timed out' | 'error'> { 225 | return this.realtime.removeChannel(channel) 226 | } 227 | 228 | /** 229 | * Unsubscribes and removes all Realtime channels from Realtime client. 230 | */ 231 | removeAllChannels(): Promise<('ok' | 'timed out' | 'error')[]> { 232 | return this.realtime.removeAllChannels() 233 | } 234 | 235 | private async _getAccessToken() { 236 | const { data } = await this.auth.getSession() 237 | 238 | return data.session?.access_token ?? null 239 | } 240 | 241 | private _initSupabaseAuthClient( 242 | { 243 | autoRefreshToken, 244 | persistSession, 245 | detectSessionInUrl, 246 | storage, 247 | storageKey, 248 | }: SupabaseAuthClientOptions, 249 | headers?: Record, 250 | fetch?: Fetch 251 | ) { 252 | const authHeaders = { 253 | Authorization: `Bearer ${this.supabaseKey}`, 254 | apikey: `${this.supabaseKey}`, 255 | } 256 | return new SupabaseAuthClient({ 257 | url: this.authUrl, 258 | headers: { ...authHeaders, ...headers }, 259 | storageKey: storageKey, 260 | autoRefreshToken, 261 | persistSession, 262 | detectSessionInUrl, 263 | storage, 264 | fetch, 265 | }) 266 | } 267 | 268 | private _initRealtimeClient(options: RealtimeClientOptions) { 269 | return new RealtimeClient(this.realtimeUrl, { 270 | ...options, 271 | params: { ...{ apikey: this.supabaseKey }, ...options?.params }, 272 | }) 273 | } 274 | 275 | private _listenForAuthEvents() { 276 | let data = this.auth.onAuthStateChange((event, session) => { 277 | this._handleTokenChanged(event, session?.access_token, 'CLIENT') 278 | }) 279 | return data 280 | } 281 | 282 | private _handleTokenChanged( 283 | event: AuthChangeEvent, 284 | token: string | undefined, 285 | source: 'CLIENT' | 'STORAGE' 286 | ) { 287 | if ( 288 | (event === 'TOKEN_REFRESHED' || event === 'SIGNED_IN') && 289 | this.changedAccessToken !== token 290 | ) { 291 | // Token has changed 292 | this.realtime.setAuth(token ?? null) 293 | 294 | this.changedAccessToken = token 295 | } else if (event === 'SIGNED_OUT' || event === 'USER_DELETED') { 296 | // Token is removed 297 | this.realtime.setAuth(this.supabaseKey) 298 | if (source == 'STORAGE') this.auth.signOut() 299 | this.changedAccessToken = undefined 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /src/functions-js/src/FunctionsClient.ts: -------------------------------------------------------------------------------- 1 | import { resolveFetch } from './helper' 2 | import { 3 | Fetch, 4 | FunctionsFetchError, 5 | FunctionsHttpError, 6 | FunctionsRelayError, 7 | FunctionsResponse, 8 | FunctionInvokeOptions, 9 | } from './types' 10 | 11 | export class FunctionsClient { 12 | protected url: string 13 | protected headers: Record 14 | protected fetch: Fetch 15 | 16 | constructor( 17 | url: string, 18 | { 19 | headers = {}, 20 | customFetch, 21 | }: { 22 | headers?: Record 23 | customFetch?: Fetch 24 | } = {} 25 | ) { 26 | this.url = url 27 | this.headers = headers 28 | this.fetch = resolveFetch(customFetch) 29 | } 30 | 31 | /** 32 | * Updates the authorization header 33 | * @param token - the new jwt token sent in the authorisation header 34 | */ 35 | setAuth(token: string) { 36 | this.headers.Authorization = `Bearer ${token}` 37 | } 38 | 39 | /** 40 | * Invokes a function 41 | * @param functionName - the name of the function to invoke 42 | */ 43 | async invoke( 44 | functionName: string, 45 | invokeOptions: FunctionInvokeOptions = {} 46 | ): Promise> { 47 | try { 48 | const { headers, method, body: functionArgs } = invokeOptions 49 | 50 | let _headers: Record = {} 51 | let body: any 52 | if ( 53 | functionArgs && 54 | ((headers && !Object.prototype.hasOwnProperty.call(headers, 'Content-Type')) || !headers) 55 | ) { 56 | if ( 57 | (typeof Blob !== 'undefined' && functionArgs instanceof Blob) || 58 | functionArgs instanceof ArrayBuffer 59 | ) { 60 | // will work for File as File inherits Blob 61 | // also works for ArrayBuffer as it is the same underlying structure as a Blob 62 | _headers['Content-Type'] = 'application/octet-stream' 63 | body = functionArgs 64 | } else if (typeof functionArgs === 'string') { 65 | // plain string 66 | _headers['Content-Type'] = 'text/plain' 67 | body = functionArgs 68 | } else if (typeof FormData !== 'undefined' && functionArgs instanceof FormData) { 69 | // don't set content-type headers 70 | // Request will automatically add the right boundary value 71 | body = functionArgs 72 | } else { 73 | // default, assume this is JSON 74 | _headers['Content-Type'] = 'application/json' 75 | body = JSON.stringify(functionArgs) 76 | } 77 | } 78 | 79 | const response = await this.fetch(`${functionName}`, { 80 | method: method || 'POST', 81 | // headers priority is (high to low): 82 | // 1. invoke-level headers 83 | // 2. client-level headers 84 | // 3. default Content-Type header 85 | headers: { ..._headers, ...this.headers, ...headers }, 86 | body, 87 | }).catch((fetchError) => { 88 | throw new FunctionsFetchError(fetchError) 89 | }) 90 | 91 | const isRelayError = response.headers.get('x-relay-error') 92 | if (isRelayError && isRelayError === 'true') { 93 | throw new FunctionsRelayError(response) 94 | } 95 | 96 | if (!response.ok) { 97 | throw new FunctionsHttpError(response) 98 | } 99 | 100 | let responseType = (response.headers.get('Content-Type') ?? 'text/plain').split(';')[0].trim() 101 | let data: any 102 | if (responseType === 'application/json') { 103 | data = await response.json() 104 | } else if (responseType === 'application/octet-stream') { 105 | data = await response.blob() 106 | } else if (responseType === 'multipart/form-data') { 107 | data = await response.formData() 108 | } else { 109 | // default to text 110 | data = await response.text() 111 | } 112 | 113 | return { data, error: null } 114 | } catch (error) { 115 | return { data: null, error } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/functions-js/src/helper.ts: -------------------------------------------------------------------------------- 1 | import { Fetch } from './types' 2 | 3 | export const resolveFetch = (customFetch?: Fetch): Fetch => { 4 | let _fetch: Fetch 5 | if (customFetch) { 6 | _fetch = customFetch 7 | } else if (typeof fetch === 'undefined') { 8 | // _fetch = async (...args) => await (await import('cross-fetch')).fetch(...args) 9 | } else { 10 | _fetch = fetch 11 | } 12 | return (...args) => _fetch(...args) 13 | } 14 | -------------------------------------------------------------------------------- /src/functions-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { FunctionsClient } from './FunctionsClient' 2 | export { 3 | FunctionsError, 4 | FunctionsFetchError, 5 | FunctionsHttpError, 6 | FunctionsRelayError, 7 | FunctionsResponse, 8 | } from './types' 9 | -------------------------------------------------------------------------------- /src/functions-js/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Fetch = typeof fetch 2 | 3 | /** 4 | * Response format 5 | * 6 | */ 7 | export interface FunctionsResponseSuccess { 8 | data: T 9 | error: null 10 | } 11 | export interface FunctionsResponseFailure { 12 | data: null 13 | error: any 14 | } 15 | export type FunctionsResponse = FunctionsResponseSuccess | FunctionsResponseFailure 16 | 17 | export class FunctionsError extends Error { 18 | context: any 19 | constructor(message: string, name = 'FunctionsError', context?: any) { 20 | super(message) 21 | super.name = name 22 | this.context = context 23 | } 24 | } 25 | 26 | export class FunctionsFetchError extends FunctionsError { 27 | constructor(context: any) { 28 | super('Failed to send a request to the Edge Function', 'FunctionsFetchError', context) 29 | } 30 | } 31 | 32 | export class FunctionsRelayError extends FunctionsError { 33 | constructor(context: any) { 34 | super('Relay Error invoking the Edge Function', 'FunctionsRelayError', context) 35 | } 36 | } 37 | 38 | export class FunctionsHttpError extends FunctionsError { 39 | constructor(context: any) { 40 | super('Edge Function returned a non-2xx status code', 'FunctionsHttpError', context) 41 | } 42 | } 43 | 44 | export type FunctionInvokeOptions = { 45 | /** 46 | * object representing the headers to send with the request 47 | * */ 48 | headers?: { [key: string]: string } 49 | /** 50 | * The HTTP verb of the request 51 | */ 52 | method?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE' 53 | /** 54 | * the body of the request 55 | */ 56 | body?: 57 | | File 58 | | Blob 59 | | ArrayBuffer 60 | | FormData 61 | | ReadableStream 62 | | Record 63 | | string 64 | } 65 | -------------------------------------------------------------------------------- /src/functions-js/src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = '2.0.0' 2 | -------------------------------------------------------------------------------- /src/gotrue-js/src/GoTrueAdminApi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Fetch, 3 | _generateLinkResponse, 4 | _noResolveJsonResponse, 5 | _request, 6 | _userResponse, 7 | } from './lib/fetch' 8 | import { resolveFetch } from './lib/helpers' 9 | import { 10 | AdminUserAttributes, 11 | GenerateLinkParams, 12 | GenerateLinkResponse, 13 | Pagination, 14 | User, 15 | UserResponse, 16 | GoTrueAdminMFAApi, 17 | AuthMFAAdminDeleteFactorParams, 18 | AuthMFAAdminDeleteFactorResponse, 19 | AuthMFAAdminListFactorsParams, 20 | AuthMFAAdminListFactorsResponse, 21 | PageParams, 22 | } from './lib/types' 23 | import { AuthError, isAuthError } from './lib/errors' 24 | 25 | export default class GoTrueAdminApi { 26 | /** Contains all MFA administration methods. */ 27 | mfa: GoTrueAdminMFAApi 28 | 29 | protected url: string 30 | protected headers: { 31 | [key: string]: string 32 | } 33 | protected fetch: Fetch 34 | 35 | constructor({ 36 | url = '', 37 | headers = {}, 38 | fetch, 39 | }: { 40 | url: string 41 | headers?: { 42 | [key: string]: string 43 | } 44 | fetch?: Fetch 45 | }) { 46 | this.url = url 47 | this.headers = headers 48 | this.fetch = resolveFetch(fetch) 49 | this.mfa = { 50 | listFactors: this._listFactors.bind(this), 51 | deleteFactor: this._deleteFactor.bind(this), 52 | } 53 | } 54 | 55 | /** 56 | * Removes a logged-in session. 57 | * @param jwt A valid, logged-in JWT. 58 | */ 59 | async signOut(jwt: string): Promise<{ data: null; error: AuthError | null }> { 60 | try { 61 | await _request(this.fetch, 'POST', `${this.url}/logout`, { 62 | headers: this.headers, 63 | jwt, 64 | noResolveJson: true, 65 | }) 66 | return { data: null, error: null } 67 | } catch (error) { 68 | if (isAuthError(error)) { 69 | return { data: null, error } 70 | } 71 | 72 | throw error 73 | } 74 | } 75 | 76 | /** 77 | * Sends an invite link to an email address. 78 | * @param email The email address of the user. 79 | * @param options.redirectTo A URL or mobile deeplink to send the user to after they are confirmed. 80 | * @param options.data Optional user metadata 81 | */ 82 | async inviteUserByEmail( 83 | email: string, 84 | options: { 85 | redirectTo?: string 86 | data?: object 87 | } = {} 88 | ): Promise { 89 | try { 90 | return await _request(this.fetch, 'POST', `${this.url}/invite`, { 91 | body: { email, data: options.data }, 92 | headers: this.headers, 93 | redirectTo: options.redirectTo, 94 | xform: _userResponse, 95 | }) 96 | } catch (error) { 97 | if (isAuthError(error)) { 98 | return { data: { user: null }, error } 99 | } 100 | 101 | throw error 102 | } 103 | } 104 | 105 | /** 106 | * Generates email links and OTPs to be sent via a custom email provider. 107 | * @param email The user's email. 108 | * @param options.password User password. For signup only. 109 | * @param options.data Optional user metadata. For signup only. 110 | * @param options.redirectTo The redirect url which should be appended to the generated link 111 | */ 112 | async generateLink(params: GenerateLinkParams): Promise { 113 | try { 114 | const { options, ...rest } = params 115 | const body: any = { ...rest, ...options } 116 | if ('newEmail' in rest) { 117 | // replace newEmail with new_email in request body 118 | body.new_email = rest?.newEmail 119 | delete body['newEmail'] 120 | } 121 | return await _request(this.fetch, 'POST', `${this.url}/admin/generate_link`, { 122 | body: body, 123 | headers: this.headers, 124 | xform: _generateLinkResponse, 125 | redirectTo: options?.redirectTo, 126 | }) 127 | } catch (error) { 128 | if (isAuthError(error)) { 129 | return { 130 | data: { 131 | properties: null, 132 | user: null, 133 | }, 134 | error, 135 | } 136 | } 137 | throw error 138 | } 139 | } 140 | 141 | // User Admin API 142 | /** 143 | * Creates a new user. 144 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 145 | */ 146 | async createUser(attributes: AdminUserAttributes): Promise { 147 | try { 148 | return await _request(this.fetch, 'POST', `${this.url}/admin/users`, { 149 | body: attributes, 150 | headers: this.headers, 151 | xform: _userResponse, 152 | }) 153 | } catch (error) { 154 | if (isAuthError(error)) { 155 | return { data: { user: null }, error } 156 | } 157 | 158 | throw error 159 | } 160 | } 161 | 162 | /** 163 | * Get a list of users. 164 | * 165 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 166 | * @param params An object which supports `page` and `perPage` as numbers, to alter the paginated results. 167 | */ 168 | async listUsers( 169 | params?: PageParams 170 | ): Promise< 171 | | { data: { users: User[]; aud: string } & Pagination; error: null } 172 | | { data: { users: [] }; error: AuthError } 173 | > { 174 | try { 175 | const pagination: Pagination = { nextPage: null, lastPage: 0, total: 0 } 176 | const response = await _request(this.fetch, 'GET', `${this.url}/admin/users`, { 177 | headers: this.headers, 178 | noResolveJson: true, 179 | query: { 180 | page: params?.page?.toString() ?? '', 181 | per_page: params?.perPage?.toString() ?? '', 182 | }, 183 | xform: _noResolveJsonResponse, 184 | }) 185 | if (response.error) throw response.error 186 | 187 | const users = response 188 | const total = response.headers.get('x-total-count') ?? 0 189 | const links = response.headers.get('link')?.split(',') ?? [] 190 | if (links.length > 0) { 191 | links.forEach((link: string) => { 192 | const page = parseInt(link.split(';')[0].split('=')[1].substring(0, 1)) 193 | const rel = JSON.parse(link.split(';')[1].split('=')[1]) 194 | pagination[`${rel}Page`] = page 195 | }) 196 | 197 | pagination.total = parseInt(total) 198 | } 199 | return { data: { ...users, ...pagination }, error: null } 200 | } catch (error) { 201 | if (isAuthError(error)) { 202 | return { data: { users: [] }, error } 203 | } 204 | throw error 205 | } 206 | } 207 | 208 | /** 209 | * Get user by id. 210 | * 211 | * @param uid The user's unique identifier 212 | * 213 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 214 | */ 215 | async getUserById(uid: string): Promise { 216 | try { 217 | return await _request(this.fetch, 'GET', `${this.url}/admin/users/${uid}`, { 218 | headers: this.headers, 219 | xform: _userResponse, 220 | }) 221 | } catch (error) { 222 | if (isAuthError(error)) { 223 | return { data: { user: null }, error } 224 | } 225 | 226 | throw error 227 | } 228 | } 229 | 230 | /** 231 | * Updates the user data. 232 | * 233 | * @param attributes The data you want to update. 234 | * 235 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 236 | */ 237 | async updateUserById(uid: string, attributes: AdminUserAttributes): Promise { 238 | try { 239 | return await _request(this.fetch, 'PUT', `${this.url}/admin/users/${uid}`, { 240 | body: attributes, 241 | headers: this.headers, 242 | xform: _userResponse, 243 | }) 244 | } catch (error) { 245 | if (isAuthError(error)) { 246 | return { data: { user: null }, error } 247 | } 248 | 249 | throw error 250 | } 251 | } 252 | 253 | /** 254 | * Delete a user. Requires a `service_role` key. 255 | * 256 | * @param id The user id you want to remove. 257 | * @param shouldSoftDelete If true, then the user will be soft-deleted from the auth schema. 258 | * Defaults to false for backward compatibility. 259 | * 260 | * This function should only be called on a server. Never expose your `service_role` key in the browser. 261 | */ 262 | async deleteUser(id: string, shouldSoftDelete = false): Promise { 263 | try { 264 | return await _request(this.fetch, 'DELETE', `${this.url}/admin/users/${id}`, { 265 | headers: this.headers, 266 | body: { 267 | should_soft_delete: shouldSoftDelete, 268 | }, 269 | xform: _userResponse, 270 | }) 271 | } catch (error) { 272 | if (isAuthError(error)) { 273 | return { data: { user: null }, error } 274 | } 275 | 276 | throw error 277 | } 278 | } 279 | 280 | private async _listFactors( 281 | params: AuthMFAAdminListFactorsParams 282 | ): Promise { 283 | try { 284 | const { data, error } = await _request( 285 | this.fetch, 286 | 'GET', 287 | `${this.url}/admin/users/${params.userId}/factors`, 288 | { 289 | headers: this.headers, 290 | xform: (factors: any) => { 291 | return { data: { factors }, error: null } 292 | }, 293 | } 294 | ) 295 | return { data, error } 296 | } catch (error) { 297 | if (isAuthError(error)) { 298 | return { data: null, error } 299 | } 300 | 301 | throw error 302 | } 303 | } 304 | 305 | private async _deleteFactor( 306 | params: AuthMFAAdminDeleteFactorParams 307 | ): Promise { 308 | try { 309 | const data = await _request( 310 | this.fetch, 311 | 'DELETE', 312 | `${this.url}/admin/users/${params.userId}/factors/${params.id}`, 313 | { 314 | headers: this.headers, 315 | } 316 | ) 317 | 318 | return { data, error: null } 319 | } catch (error) { 320 | if (isAuthError(error)) { 321 | return { data: null, error } 322 | } 323 | 324 | throw error 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /src/gotrue-js/src/index.ts: -------------------------------------------------------------------------------- 1 | import GoTrueAdminApi from './GoTrueAdminApi' 2 | import GoTrueClient from './GoTrueClient' 3 | export { GoTrueAdminApi, GoTrueClient } 4 | export * from './lib/types' 5 | export * from './lib/errors' 6 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version' 2 | export const GOTRUE_URL = 'http://localhost:9999' 3 | export const STORAGE_KEY = 'supabase.auth.token' 4 | export const AUDIENCE = '' 5 | export const DEFAULT_HEADERS = { 'X-Client-Info': `gotrue-js/${version}` } 6 | export const EXPIRY_MARGIN = 10 // in seconds 7 | export const NETWORK_FAILURE = { 8 | MAX_RETRIES: 10, 9 | RETRY_INTERVAL: 2, // in deciseconds 10 | } 11 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | export class AuthError extends Error { 2 | status: number | undefined 3 | protected __isAuthError = true 4 | 5 | constructor(message: string, status?: number) { 6 | super(message) 7 | this.name = 'AuthError' 8 | this.status = status 9 | } 10 | } 11 | 12 | export function isAuthError(error: unknown): error is AuthError { 13 | return typeof error === 'object' && error !== null && '__isAuthError' in error 14 | } 15 | 16 | export class AuthApiError extends AuthError { 17 | status: number 18 | 19 | constructor(message: string, status: number) { 20 | super(message, status) 21 | this.name = 'AuthApiError' 22 | this.status = status 23 | } 24 | 25 | toJSON() { 26 | return { 27 | name: this.name, 28 | message: this.message, 29 | status: this.status, 30 | } 31 | } 32 | } 33 | 34 | export function isAuthApiError(error: unknown): error is AuthApiError { 35 | return isAuthError(error) && error.name === 'AuthApiError' 36 | } 37 | 38 | export class AuthUnknownError extends AuthError { 39 | originalError: unknown 40 | 41 | constructor(message: string, originalError: unknown) { 42 | super(message) 43 | this.name = 'AuthUnknownError' 44 | this.originalError = originalError 45 | } 46 | } 47 | 48 | export class CustomAuthError extends AuthError { 49 | name: string 50 | status: number 51 | constructor(message: string, name: string, status: number) { 52 | super(message) 53 | this.name = name 54 | this.status = status 55 | } 56 | 57 | toJSON() { 58 | return { 59 | name: this.name, 60 | message: this.message, 61 | status: this.status, 62 | } 63 | } 64 | } 65 | 66 | export class AuthSessionMissingError extends CustomAuthError { 67 | constructor() { 68 | super('Auth session missing!', 'AuthSessionMissingError', 400) 69 | } 70 | } 71 | 72 | export class AuthInvalidCredentialsError extends CustomAuthError { 73 | constructor(message: string) { 74 | super(message, 'AuthInvalidCredentialsError', 400) 75 | } 76 | } 77 | 78 | export class AuthImplicitGrantRedirectError extends CustomAuthError { 79 | details: { error: string; code: string } | null = null 80 | constructor(message: string, details: { error: string; code: string } | null = null) { 81 | super(message, 'AuthImplicitGrantRedirectError', 500) 82 | this.details = details 83 | } 84 | 85 | toJSON() { 86 | return { 87 | name: this.name, 88 | message: this.message, 89 | status: this.status, 90 | details: this.details, 91 | } 92 | } 93 | } 94 | 95 | export class AuthRetryableFetchError extends CustomAuthError { 96 | constructor(message: string, status: number) { 97 | super(message, 'AuthRetryableFetchError', status) 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { expiresAt, looksLikeFetchResponse } from './helpers' 2 | let { URLSearchParams } = require('../../../wechaturl-parse/index') 3 | import { 4 | AuthResponse, 5 | SSOResponse, 6 | GenerateLinkProperties, 7 | GenerateLinkResponse, 8 | User, 9 | UserResponse, 10 | } from './types' 11 | import { AuthApiError, AuthRetryableFetchError, AuthUnknownError } from './errors' 12 | 13 | export type Fetch = typeof fetch 14 | 15 | export interface FetchOptions { 16 | headers?: { 17 | [key: string]: string 18 | } 19 | noResolveJson?: boolean 20 | } 21 | 22 | export interface FetchParameters { 23 | signal?: AbortSignal 24 | } 25 | 26 | export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'DELETE' 27 | 28 | const _getErrorMessage = (err: any): string => 29 | err.msg || err.message || err.error_description || err.error || JSON.stringify(err) 30 | 31 | const handleError = async (error: unknown, reject: (reason?: any) => void) => { 32 | const NETWORK_ERROR_CODES = [502, 503, 504] 33 | if (!looksLikeFetchResponse(error)) { 34 | reject(new AuthRetryableFetchError(_getErrorMessage(error), 0)) 35 | } else if (NETWORK_ERROR_CODES.includes(error.status)) { 36 | // status in 500...599 range - server had an error, request might be retryed. 37 | reject(new AuthRetryableFetchError(_getErrorMessage(error), error.status)) 38 | } else { 39 | // got a response from server that is not in the 500...599 range - should not retry 40 | error 41 | .json() 42 | .then((err) => { 43 | reject(new AuthApiError(_getErrorMessage(err), error.status || 500)) 44 | }) 45 | .catch((e) => { 46 | // not a valid json response 47 | reject(new AuthUnknownError(_getErrorMessage(e), e)) 48 | }) 49 | } 50 | } 51 | 52 | const _getRequestParams = ( 53 | method: RequestMethodType, 54 | options?: FetchOptions, 55 | parameters?: FetchParameters, 56 | body?: object 57 | ) => { 58 | const params: { [k: string]: any } = { method, headers: options?.headers || {} } 59 | 60 | if (method === 'GET') { 61 | return params 62 | } 63 | 64 | params.headers = { 'Content-Type': 'application/json;charset=UTF-8', ...options?.headers } 65 | params.body = JSON.stringify(body) 66 | return { ...params, ...parameters } 67 | } 68 | 69 | interface GotrueRequestOptions extends FetchOptions { 70 | jwt?: string 71 | redirectTo?: string 72 | body?: object 73 | query?: { [key: string]: string } 74 | /** 75 | * Function that transforms api response from gotrue into a desirable / standardised format 76 | */ 77 | xform?: (data: any) => any 78 | } 79 | 80 | export async function _request( 81 | fetcher: Fetch, 82 | method: RequestMethodType, 83 | url: string, 84 | options?: GotrueRequestOptions 85 | ) { 86 | const headers = { ...options?.headers } 87 | if (options?.jwt) { 88 | headers['Authorization'] = `Bearer ${options.jwt}` 89 | } 90 | const qs = options?.query ?? {} 91 | if (options?.redirectTo) { 92 | qs['redirect_to'] = options.redirectTo 93 | } 94 | const queryString = Object.keys(qs).length ? '?' + new URLSearchParams(qs).toString() : '' 95 | const data = await _handleRequest( 96 | fetcher, 97 | method, 98 | url + queryString, 99 | { headers, noResolveJson: options?.noResolveJson }, 100 | {}, 101 | options?.body 102 | ) 103 | return options?.xform ? options?.xform(data) : { data: { ...data }, error: null } 104 | } 105 | 106 | async function _handleRequest( 107 | fetcher: Fetch, 108 | method: RequestMethodType, 109 | url: string, 110 | options?: FetchOptions, 111 | parameters?: FetchParameters, 112 | body?: object 113 | ): Promise { 114 | return new Promise((resolve, reject) => { 115 | fetcher(url, _getRequestParams(method, options, parameters, body)) 116 | .then((result) => { 117 | if (!result.ok) throw result 118 | if (options?.noResolveJson) return result 119 | return result 120 | }) 121 | .then((data) => resolve(data)) 122 | .catch((error) => handleError(error, reject)) 123 | }) 124 | } 125 | 126 | export function _sessionResponse(data: any): AuthResponse { 127 | let session = null 128 | if (hasSession(data.data)) { 129 | session = { ...data.data } 130 | session.expires_at = expiresAt(data.data.expires_in) 131 | } 132 | const user: User = data.user ?? (data as User) 133 | return { data: { session, user }, error: null } 134 | } 135 | 136 | export function _userResponse(data: any): UserResponse { 137 | const user: User = data.user ?? (data as User) 138 | return { data: { user }, error: null } 139 | } 140 | 141 | export function _ssoResponse(data: any): SSOResponse { 142 | return { data, error: null } 143 | } 144 | 145 | export function _generateLinkResponse(data: any): GenerateLinkResponse { 146 | const { action_link, email_otp, hashed_token, redirect_to, verification_type, ...rest } = data 147 | 148 | const properties: GenerateLinkProperties = { 149 | action_link, 150 | email_otp, 151 | hashed_token, 152 | redirect_to, 153 | verification_type, 154 | } 155 | 156 | const user: User = { ...rest } 157 | return { 158 | data: { 159 | properties, 160 | user, 161 | }, 162 | error: null, 163 | } 164 | } 165 | 166 | export function _noResolveJsonResponse(data: any): Response { 167 | return data 168 | } 169 | 170 | /** 171 | * hasSession checks if the response object contains a valid session 172 | * @param data A response object 173 | * @returns true if a session is in the response 174 | */ 175 | function hasSession(data: any): boolean { 176 | return data.access_token && data.refresh_token && data.expires_in 177 | } 178 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SupportedStorage } from './types' 2 | 3 | export function expiresAt(expiresIn: number) { 4 | const timeNow = Math.round(Date.now() / 1000) 5 | return timeNow + expiresIn 6 | } 7 | 8 | export function uuid() { 9 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 10 | const r = (Math.random() * 16) | 0, 11 | v = c == 'x' ? r : (r & 0x3) | 0x8 12 | return v.toString(16) 13 | }) 14 | } 15 | 16 | export const isBrowser = () => typeof document !== 'undefined' 17 | 18 | export function getParameterByName(name: string, url?: string) { 19 | if (!url) url = window?.location?.href || '' 20 | // eslint-disable-next-line no-useless-escape 21 | name = name.replace(/[\[\]]/g, '\\$&') 22 | const regex = new RegExp('[?&#]' + name + '(=([^&#]*)|&|#|$)'), 23 | results = regex.exec(url) 24 | if (!results) return null 25 | if (!results[2]) return '' 26 | return decodeURIComponent(results[2].replace(/\+/g, ' ')) 27 | } 28 | 29 | type Fetch = typeof fetch 30 | 31 | export const resolveFetch = (customFetch?: Fetch): Fetch => { 32 | let _fetch: Fetch 33 | if (customFetch) { 34 | _fetch = customFetch 35 | } else if (typeof fetch === 'undefined') { 36 | // _fetch = async (...args) => await (await import('cross-fetch')).fetch(...args) 37 | } else { 38 | _fetch = fetch 39 | } 40 | return (...args) => _fetch(...args) 41 | } 42 | 43 | export const looksLikeFetchResponse = (maybeResponse: unknown): maybeResponse is Response => { 44 | return ( 45 | typeof maybeResponse === 'object' && 46 | maybeResponse !== null && 47 | 'status' in maybeResponse && 48 | 'ok' in maybeResponse && 49 | 'json' in maybeResponse && 50 | typeof (maybeResponse as any).json === 'function' 51 | ) 52 | } 53 | 54 | // Storage helpers 55 | export const setItemAsync = (storage: SupportedStorage, key: string, data: any) => { 56 | wx.setStorageSync(key, JSON.stringify(data)) 57 | } 58 | export const getItemAsync = (storage: SupportedStorage, key: string, data: any) => { 59 | const value = wx.getStorageSync(key) ? JSON.parse(wx.getStorageSync(key)) : '' 60 | if (!value) { 61 | return null 62 | } 63 | return value 64 | } 65 | 66 | export const removeItemAsync = async (storage: SupportedStorage, key: string): Promise => { 67 | wx.removeStorageSync(key) 68 | } 69 | 70 | export function decodeBase64URL(value: string): string { 71 | const key = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=' 72 | let base64 = '' 73 | let chr1, chr2, chr3 74 | let enc1, enc2, enc3, enc4 75 | let i = 0 76 | value = value.replace('-', '+').replace('_', '/') 77 | 78 | while (i < value.length) { 79 | enc1 = key.indexOf(value.charAt(i++)) 80 | enc2 = key.indexOf(value.charAt(i++)) 81 | enc3 = key.indexOf(value.charAt(i++)) 82 | enc4 = key.indexOf(value.charAt(i++)) 83 | chr1 = (enc1 << 2) | (enc2 >> 4) 84 | chr2 = ((enc2 & 15) << 4) | (enc3 >> 2) 85 | chr3 = ((enc3 & 3) << 6) | enc4 86 | base64 = base64 + String.fromCharCode(chr1) 87 | 88 | if (enc3 != 64 && chr2 != 0) { 89 | base64 = base64 + String.fromCharCode(chr2) 90 | } 91 | if (enc4 != 64 && chr3 != 0) { 92 | base64 = base64 + String.fromCharCode(chr3) 93 | } 94 | } 95 | return base64 96 | } 97 | 98 | /** 99 | * A deferred represents some asynchronous work that is not yet finished, which 100 | * may or may not culminate in a value. 101 | * Taken from: https://github.com/mike-north/types/blob/master/src/async.ts 102 | */ 103 | export class Deferred { 104 | public static promiseConstructor: PromiseConstructor = Promise 105 | 106 | public readonly promise!: PromiseLike 107 | 108 | public readonly resolve!: (value?: T | PromiseLike) => void 109 | 110 | public readonly reject!: (reason?: any) => any 111 | 112 | public constructor() { 113 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 114 | ;(this as any).promise = new Deferred.promiseConstructor((res, rej) => { 115 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 116 | ;(this as any).resolve = res 117 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 118 | ;(this as any).reject = rej 119 | }) 120 | } 121 | } 122 | 123 | // Taken from: https://stackoverflow.com/questions/38552003/how-to-decode-jwt-token-in-javascript-without-using-a-library 124 | export function decodeJWTPayload(token: string) { 125 | // Regex checks for base64url format 126 | const base64UrlRegex = /^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}=?$|[a-z0-9_-]{2}(==)?$)$/i 127 | 128 | const parts = token.split('.') 129 | 130 | if (parts.length !== 3) { 131 | throw new Error('JWT is not valid: not a JWT structure') 132 | } 133 | 134 | if (!base64UrlRegex.test(parts[1])) { 135 | throw new Error('JWT is not valid: payload is not in base64url format') 136 | } 137 | 138 | const base64Url = parts[1] 139 | return JSON.parse(decodeBase64URL(base64Url)) 140 | } 141 | 142 | /** 143 | * Creates a promise that resolves to null after some time. 144 | */ 145 | export function sleep(time: number): Promise { 146 | return new Promise((accept) => { 147 | setTimeout(() => accept(null), time) 148 | }) 149 | } 150 | 151 | /** 152 | * Converts the provided async function into a retryable function. Each result 153 | * or thrown error is sent to the isRetryable function which should return true 154 | * if the function should run again. 155 | */ 156 | export function retryable( 157 | fn: (attempt: number) => Promise, 158 | isRetryable: (attempt: number, error: any | null, result?: T) => boolean 159 | ): Promise { 160 | const promise = new Promise((accept, reject) => { 161 | // eslint-disable-next-line @typescript-eslint/no-extra-semi 162 | ;(async () => { 163 | for (let attempt = 0; attempt < Infinity; attempt++) { 164 | try { 165 | const result = await fn(attempt) 166 | 167 | if (!isRetryable(attempt, null, result)) { 168 | accept(result) 169 | return 170 | } 171 | } catch (e: any) { 172 | if (!isRetryable(attempt, e)) { 173 | reject(e) 174 | return 175 | } 176 | } 177 | } 178 | })() 179 | }) 180 | 181 | return promise 182 | } 183 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/local-storage.ts: -------------------------------------------------------------------------------- 1 | import { isBrowser } from './helpers' 2 | import { SupportedStorage } from './types' 3 | 4 | const localStorageAdapter: SupportedStorage = { 5 | getItem: (key) => { 6 | if (!isBrowser()) { 7 | return null 8 | } 9 | 10 | return globalThis.localStorage.getItem(key) 11 | }, 12 | setItem: (key, value) => { 13 | if (!isBrowser()) { 14 | return 15 | } 16 | 17 | globalThis.localStorage.setItem(key, value) 18 | }, 19 | removeItem: (key) => { 20 | if (!isBrowser()) { 21 | return 22 | } 23 | 24 | globalThis.localStorage.removeItem(key) 25 | }, 26 | } 27 | 28 | export default localStorageAdapter 29 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * https://mathiasbynens.be/notes/globalthis 3 | */ 4 | export function polyfillGlobalThis() { 5 | if (typeof globalThis === 'object') return 6 | try { 7 | Object.defineProperty(Object.prototype, '__magic__', { 8 | get: function () { 9 | return this 10 | }, 11 | configurable: true, 12 | }) 13 | // @ts-expect-error 'Allow access to magic' 14 | __magic__.globalThis = __magic__ 15 | // @ts-expect-error 'Allow access to magic' 16 | delete Object.prototype.__magic__ 17 | } catch (e) { 18 | if (typeof self !== 'undefined') { 19 | // @ts-expect-error 'Allow access to globals' 20 | self.globalThis = self 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/gotrue-js/src/lib/version.ts: -------------------------------------------------------------------------------- 1 | // Generated by genversion. 2 | export const version = '2.10.2' 3 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import SupabaseClient from './SupabaseClient' 2 | import type { GenericSchema, SupabaseClientOptions } from './lib/types' 3 | 4 | export * from './gotrue-js/src/index' 5 | export type { User as AuthUser, Session as AuthSession } from './gotrue-js/src/index' 6 | export type { 7 | PostgrestResponse, 8 | PostgrestSingleResponse, 9 | PostgrestMaybeSingleResponse, 10 | PostgrestError, 11 | } from './postgrest-js/src/index' 12 | export { 13 | FunctionsHttpError, 14 | FunctionsFetchError, 15 | FunctionsRelayError, 16 | FunctionsError, 17 | } from './functions-js/src/index' 18 | export * from './realtime-js/src/index' 19 | export { default as SupabaseClient } from './SupabaseClient' 20 | export type { SupabaseClientOptions } from './lib/types' 21 | import myfetch from './wefetch' 22 | 23 | /** 24 | * Creates a new Supabase Client. 25 | */ 26 | export const createClient = < 27 | Database = any, 28 | SchemaName extends string & keyof Database = 'public' extends keyof Database 29 | ? 'public' 30 | : string & keyof Database, 31 | Schema extends GenericSchema = Database[SchemaName] extends GenericSchema 32 | ? Database[SchemaName] 33 | : any 34 | >( 35 | supabaseUrl: string, 36 | supabaseKey: string, 37 | options?: SupabaseClientOptions 38 | ): SupabaseClient => { 39 | return new SupabaseClient(supabaseUrl, supabaseKey, { 40 | ...options, 41 | global: { 42 | fetch: (...args) => myfetch(...args), 43 | headers: options?.global?.headers || {}, 44 | }, 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /src/lib/SupabaseAuthClient.ts: -------------------------------------------------------------------------------- 1 | import { GoTrueClient } from '../gotrue-js/src/index' 2 | import { SupabaseAuthClientOptions } from './types' 3 | 4 | export class SupabaseAuthClient extends GoTrueClient { 5 | constructor(options: SupabaseAuthClientOptions) { 6 | super(options) 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | // constants.ts 2 | import { version } from './version' 3 | export const DEFAULT_HEADERS = { 'X-Client-Info': `supabase-js/${version}` } 4 | -------------------------------------------------------------------------------- /src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | // import crossFetch, { Headers as CrossFetchHeaders } from 'cross-fetch' 2 | 3 | type Fetch = typeof fetch 4 | 5 | export const resolveFetch = (customFetch?: Fetch): Fetch => { 6 | let _fetch: Fetch 7 | if (customFetch) { 8 | _fetch = customFetch 9 | } else if (typeof fetch === 'undefined') { 10 | // _fetch = crossFetch as unknown as Fetch 11 | } else { 12 | _fetch = fetch 13 | } 14 | return (...args) => _fetch(...args) 15 | } 16 | 17 | export const resolveHeaders = (init: any) => { 18 | return new Map(Object.entries(init.headers)) 19 | } 20 | 21 | export const fetchWithAuth = ( 22 | supabaseKey: string, 23 | getAccessToken: () => Promise, 24 | customFetch?: Fetch 25 | ): Fetch => { 26 | const fetch = resolveFetch(customFetch) 27 | 28 | return async (input, init) => { 29 | const accessToken = (await getAccessToken()) ?? supabaseKey 30 | let headers = resolveHeaders(init) 31 | if (!headers.has('apikey')) { 32 | headers.set('apikey', supabaseKey) 33 | } 34 | 35 | if (!headers.has('Authorization')) { 36 | headers.set('Authorization', `Bearer ${accessToken}`) 37 | } 38 | return fetch(input, { ...init, headers }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | // helpers.ts 2 | import { SupabaseClientOptions } from './types' 3 | 4 | export function uuid() { 5 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { 6 | var r = (Math.random() * 16) | 0, 7 | v = c == 'x' ? r : (r & 0x3) | 0x8 8 | return v.toString(16) 9 | }) 10 | } 11 | 12 | export function stripTrailingSlash(url: string): string { 13 | return url.replace(/\/$/, '') 14 | } 15 | 16 | export const isBrowser = () => typeof window !== 'undefined' 17 | 18 | export function applySettingDefaults< 19 | Database = any, 20 | SchemaName extends string & keyof Database = 'public' extends keyof Database 21 | ? 'public' 22 | : string & keyof Database 23 | >( 24 | options: SupabaseClientOptions, 25 | defaults: SupabaseClientOptions 26 | ): SupabaseClientOptions { 27 | const { 28 | db: dbOptions, 29 | auth: authOptions, 30 | realtime: realtimeOptions, 31 | global: globalOptions, 32 | } = options 33 | const { 34 | db: DEFAULT_DB_OPTIONS, 35 | auth: DEFAULT_AUTH_OPTIONS, 36 | realtime: DEFAULT_REALTIME_OPTIONS, 37 | global: DEFAULT_GLOBAL_OPTIONS, 38 | } = defaults 39 | 40 | return { 41 | db: { 42 | ...DEFAULT_DB_OPTIONS, 43 | ...dbOptions, 44 | }, 45 | auth: { 46 | ...DEFAULT_AUTH_OPTIONS, 47 | ...authOptions, 48 | }, 49 | realtime: { 50 | ...DEFAULT_REALTIME_OPTIONS, 51 | ...realtimeOptions, 52 | }, 53 | global: { 54 | ...DEFAULT_GLOBAL_OPTIONS, 55 | ...globalOptions, 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { GoTrueClient } from '../gotrue-js/src/index' 2 | import { RealtimeClientOptions } from '../realtime-js/src/index' 3 | 4 | type GoTrueClientOptions = ConstructorParameters[0] 5 | 6 | export interface SupabaseAuthClientOptions extends GoTrueClientOptions {} 7 | 8 | export type Fetch = typeof fetch 9 | 10 | export type SupabaseClientOptions = { 11 | /** 12 | * The Postgres schema which your tables belong to. Must be on the list of exposed schemas in Supabase. Defaults to `public`. 13 | */ 14 | db?: { 15 | schema?: SchemaName 16 | } 17 | 18 | auth?: { 19 | /** 20 | * Automatically refreshes the token for logged-in users. Defaults to true. 21 | */ 22 | autoRefreshToken?: boolean 23 | /** 24 | * Optional key name used for storing tokens in local storage. 25 | */ 26 | storageKey?: string 27 | /** 28 | * Whether to persist a logged-in session to storage. Defaults to true. 29 | */ 30 | persistSession?: boolean 31 | /** 32 | * Detect a session from the URL. Used for OAuth login callbacks. Defaults to true. 33 | */ 34 | detectSessionInUrl?: boolean 35 | /** 36 | * A storage provider. Used to store the logged-in session. 37 | */ 38 | storage?: SupabaseAuthClientOptions['storage'] 39 | } 40 | /** 41 | * Options passed to the realtime-js instance 42 | */ 43 | realtime?: RealtimeClientOptions 44 | global?: { 45 | /** 46 | * A custom `fetch` implementation. 47 | */ 48 | fetch?: Fetch 49 | /** 50 | * Optional headers for initializing the client. 51 | */ 52 | headers?: Record 53 | } 54 | } 55 | 56 | export type GenericTable = { 57 | Row: Record 58 | Insert: Record 59 | Update: Record 60 | } 61 | 62 | export type GenericUpdatableView = { 63 | Row: Record 64 | Insert: Record 65 | Update: Record 66 | } 67 | 68 | export type GenericNonUpdatableView = { 69 | Row: Record 70 | } 71 | 72 | export type GenericView = GenericUpdatableView | GenericNonUpdatableView 73 | 74 | export type GenericFunction = { 75 | Args: Record 76 | Returns: unknown 77 | } 78 | 79 | export type GenericSchema = { 80 | Tables: Record 81 | Views: Record 82 | Functions: Record 83 | } 84 | -------------------------------------------------------------------------------- /src/lib/version.ts: -------------------------------------------------------------------------------- 1 | export const version = '0.0.0-automated' 2 | -------------------------------------------------------------------------------- /src/postgrest-js/src/PostgrestBuilder.ts: -------------------------------------------------------------------------------- 1 | // import crossFetch from 'cross-fetch' 2 | 3 | import type { Fetch, PostgrestResponse } from './types' 4 | 5 | export default abstract class PostgrestBuilder 6 | implements PromiseLike> 7 | { 8 | protected method: 'GET' | 'HEAD' | 'POST' | 'PATCH' | 'DELETE' 9 | protected url: URL 10 | protected headers: Record 11 | protected schema?: string 12 | protected body?: unknown 13 | protected shouldThrowOnError = false 14 | protected signal?: AbortSignal 15 | protected fetch: Fetch 16 | protected allowEmpty: boolean 17 | 18 | constructor(builder: PostgrestBuilder) { 19 | this.method = builder.method 20 | this.url = builder.url 21 | this.headers = builder.headers 22 | this.schema = builder.schema 23 | this.body = builder.body 24 | this.shouldThrowOnError = builder.shouldThrowOnError 25 | this.signal = builder.signal 26 | this.allowEmpty = builder.allowEmpty 27 | 28 | if (builder.fetch) { 29 | this.fetch = builder.fetch 30 | } else if (typeof fetch === 'undefined') { 31 | // this.fetch = crossFetch 32 | } else { 33 | this.fetch = fetch 34 | } 35 | } 36 | 37 | /** 38 | * If there's an error with the query, throwOnError will reject the promise by 39 | * throwing the error instead of returning it as part of a successful response. 40 | * 41 | * {@link https://github.com/supabase/supabase-js/issues/92} 42 | */ 43 | throwOnError(): this { 44 | this.shouldThrowOnError = true 45 | return this 46 | } 47 | 48 | then, TResult2 = never>( 49 | onfulfilled?: 50 | | ((value: PostgrestResponse) => TResult1 | PromiseLike) 51 | | undefined 52 | | null, 53 | onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null 54 | ): PromiseLike { 55 | // https://postgrest.org/en/stable/api.html#switching-schemas 56 | if (this.schema === undefined) { 57 | // skip 58 | } else if (['GET', 'HEAD'].includes(this.method)) { 59 | this.headers['Accept-Profile'] = this.schema 60 | } else { 61 | this.headers['Content-Profile'] = this.schema 62 | } 63 | if (this.method !== 'GET' && this.method !== 'HEAD') { 64 | this.headers['Content-Type'] = 'application/json' 65 | } 66 | 67 | // NOTE: Invoke w/o `this` to avoid illegal invocation error. 68 | // https://github.com/supabase/postgrest-js/pull/247 69 | const _fetch = this.fetch 70 | let res = _fetch(this.url.toString(), { 71 | method: this.method, 72 | headers: this.headers, 73 | body: JSON.stringify(this.body), 74 | signal: this.signal, 75 | }).then(async (res) => { 76 | let error = null 77 | let data = null 78 | let count: number | null = null 79 | let status = res.status 80 | let statusText = res.statusText 81 | 82 | if (res.ok) { 83 | if (this.method !== 'HEAD') { 84 | const body = res 85 | if (body === '') { 86 | // Prefer: return=minimal 87 | } else if (this.headers['Accept'] === 'text/csv') { 88 | data = body 89 | } else if ( 90 | this.headers['Accept'] && 91 | this.headers['Accept'].includes('application/vnd.pgrst.plan+text') 92 | ) { 93 | data = body 94 | } else { 95 | data = body.data 96 | } 97 | } 98 | 99 | const countHeader = this.headers['Prefer']?.match(/count=(exact|planned|estimated)/) 100 | const contentRange = res.headers.get('content-range')?.split('/') 101 | if (countHeader && contentRange && contentRange.length > 1) { 102 | count = parseInt(contentRange[1]) 103 | } 104 | } else { 105 | const body = res 106 | 107 | try { 108 | error = body.data 109 | 110 | // Workaround for https://github.com/supabase/postgrest-js/issues/295 111 | if (Array.isArray(error) && res.status === 404) { 112 | data = [] 113 | error = null 114 | status = 200 115 | statusText = 'OK' 116 | } 117 | } catch { 118 | // Workaround for https://github.com/supabase/postgrest-js/issues/295 119 | if (res.status === 404 && body === '') { 120 | status = 204 121 | statusText = 'No Content' 122 | } else { 123 | error = { 124 | message: body, 125 | } 126 | } 127 | } 128 | 129 | if (error && this.allowEmpty && error?.details?.includes('Results contain 0 rows')) { 130 | error = null 131 | status = 200 132 | statusText = 'OK' 133 | } 134 | 135 | if (error && this.shouldThrowOnError) { 136 | throw error 137 | } 138 | } 139 | 140 | const postgrestResponse = { 141 | error, 142 | data, 143 | count, 144 | status, 145 | statusText, 146 | } 147 | 148 | return postgrestResponse 149 | }) 150 | if (!this.shouldThrowOnError) { 151 | res = res.catch((fetchError) => ({ 152 | error: { 153 | message: `FetchError: ${fetchError.message}`, 154 | details: '', 155 | hint: '', 156 | code: fetchError.code || '', 157 | }, 158 | data: null, 159 | count: null, 160 | status: 0, 161 | statusText: '', 162 | })) 163 | } 164 | 165 | return res.then(onfulfilled, onrejected) 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/postgrest-js/src/PostgrestClient.ts: -------------------------------------------------------------------------------- 1 | import PostgrestQueryBuilder from './PostgrestQueryBuilder' 2 | import PostgrestFilterBuilder from './PostgrestFilterBuilder' 3 | import PostgrestBuilder from './PostgrestBuilder' 4 | import { DEFAULT_HEADERS } from './constants' 5 | import { Fetch, GenericSchema } from './types' 6 | let { URL } = require('../../wechaturl-parse/index') 7 | 8 | /** 9 | * PostgREST client. 10 | * 11 | * @typeParam Database - Types for the schema from the [type 12 | * generator](https://supabase.com/docs/reference/javascript/next/typescript-support) 13 | * 14 | * @typeParam SchemaName - Postgres schema to switch to. Must be a string 15 | * literal, the same one passed to the constructor. If the schema is not 16 | * `"public"`, this must be supplied manually. 17 | */ 18 | export default class PostgrestClient< 19 | Database = any, 20 | SchemaName extends string & keyof Database = 'public' extends keyof Database 21 | ? 'public' 22 | : string & keyof Database, 23 | Schema extends GenericSchema = Database[SchemaName] extends GenericSchema 24 | ? Database[SchemaName] 25 | : any 26 | > { 27 | url: string 28 | headers: Record 29 | schema?: SchemaName 30 | fetch?: Fetch 31 | 32 | // TODO: Add back shouldThrowOnError once we figure out the typings 33 | /** 34 | * Creates a PostgREST client. 35 | * 36 | * @param url - URL of the PostgREST endpoint 37 | * @param options - Named parameters 38 | * @param options.headers - Custom headers 39 | * @param options.schema - Postgres schema to switch to 40 | * @param options.fetch - Custom fetch 41 | */ 42 | constructor( 43 | url: string, 44 | { 45 | headers = {}, 46 | schema, 47 | fetch, 48 | }: { 49 | headers?: Record 50 | schema?: SchemaName 51 | fetch?: Fetch 52 | } = {} 53 | ) { 54 | this.url = url 55 | this.headers = { ...DEFAULT_HEADERS, ...headers } 56 | this.schema = schema 57 | this.fetch = fetch 58 | } 59 | 60 | from< 61 | TableName extends string & keyof Schema['Tables'], 62 | Table extends Schema['Tables'][TableName] 63 | >(relation: TableName): PostgrestQueryBuilder 64 | from( 65 | relation: ViewName 66 | ): PostgrestQueryBuilder 67 | from(relation: string): PostgrestQueryBuilder 68 | /** 69 | * Perform a query on a table or a view. 70 | * 71 | * @param relation - The table or view name to query 72 | */ 73 | from(relation: string): PostgrestQueryBuilder { 74 | const url = new URL(`${this.url}/${relation}`) 75 | return new PostgrestQueryBuilder(url, { 76 | headers: { ...this.headers }, 77 | schema: this.schema, 78 | fetch: this.fetch, 79 | }) 80 | } 81 | 82 | /** 83 | * Perform a function call. 84 | * 85 | * @param fn - The function name to call 86 | * @param args - The arguments to pass to the function call 87 | * @param options - Named parameters 88 | * @param options.head - When set to `true`, `data` will not be returned. 89 | * Useful if you only need the count. 90 | * @param options.count - Count algorithm to use to count rows returned by the 91 | * function. Only applicable for [set-returning 92 | * functions](https://www.postgresql.org/docs/current/functions-srf.html). 93 | * 94 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 95 | * hood. 96 | * 97 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 98 | * statistics under the hood. 99 | * 100 | * `"estimated"`: Uses exact count for low numbers and planned count for high 101 | * numbers. 102 | */ 103 | rpc< 104 | FunctionName extends string & keyof Schema['Functions'], 105 | Function_ extends Schema['Functions'][FunctionName] 106 | >( 107 | fn: FunctionName, 108 | args: Function_['Args'] = {}, 109 | { 110 | head = false, 111 | count, 112 | }: { 113 | head?: boolean 114 | count?: 'exact' | 'planned' | 'estimated' 115 | } = {} 116 | ): PostgrestFilterBuilder< 117 | Schema, 118 | Function_['Returns'] extends any[] 119 | ? Function_['Returns'][number] extends Record 120 | ? Function_['Returns'][number] 121 | : never 122 | : never, 123 | Function_['Returns'] 124 | > { 125 | let method: 'HEAD' | 'POST' 126 | const url = new URL(`${this.url}/rpc/${fn}`) 127 | let body: unknown | undefined 128 | if (head) { 129 | method = 'HEAD' 130 | Object.entries(args).forEach(([name, value]) => { 131 | url.searchParams.append(name, `${value}`) 132 | }) 133 | } else { 134 | method = 'POST' 135 | body = args 136 | } 137 | 138 | const headers = { ...this.headers } 139 | if (count) { 140 | headers['Prefer'] = `count=${count}` 141 | } 142 | 143 | return new PostgrestFilterBuilder({ 144 | method, 145 | url, 146 | headers, 147 | schema: this.schema, 148 | body, 149 | fetch: this.fetch, 150 | allowEmpty: false, 151 | } as unknown as PostgrestBuilder) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/postgrest-js/src/PostgrestFilterBuilder.ts: -------------------------------------------------------------------------------- 1 | import PostgrestTransformBuilder from './PostgrestTransformBuilder' 2 | import { GenericSchema } from './types' 3 | 4 | type FilterOperator = 5 | | 'eq' 6 | | 'neq' 7 | | 'gt' 8 | | 'gte' 9 | | 'lt' 10 | | 'lte' 11 | | 'like' 12 | | 'ilike' 13 | | 'is' 14 | | 'in' 15 | | 'cs' 16 | | 'cd' 17 | | 'sl' 18 | | 'sr' 19 | | 'nxl' 20 | | 'nxr' 21 | | 'adj' 22 | | 'ov' 23 | | 'fts' 24 | | 'plfts' 25 | | 'phfts' 26 | | 'wfts' 27 | 28 | export default class PostgrestFilterBuilder< 29 | Schema extends GenericSchema, 30 | Row extends Record, 31 | Result 32 | > extends PostgrestTransformBuilder { 33 | eq(column: ColumnName, value: Row[ColumnName]): this 34 | eq(column: string, value: unknown): this 35 | /** 36 | * Match only rows where `column` is equal to `value`. 37 | * 38 | * To check if the value of `column` is NULL, you should use `.is()` instead. 39 | * 40 | * @param column - The column to filter on 41 | * @param value - The value to filter with 42 | */ 43 | eq(column: string, value: unknown): this { 44 | this.url.searchParams.append(column, `eq.${value}`) 45 | return this 46 | } 47 | 48 | neq(column: ColumnName, value: Row[ColumnName]): this 49 | neq(column: string, value: unknown): this 50 | /** 51 | * Match only rows where `column` is not equal to `value`. 52 | * 53 | * @param column - The column to filter on 54 | * @param value - The value to filter with 55 | */ 56 | neq(column: string, value: unknown): this { 57 | this.url.searchParams.append(column, `neq.${value}`) 58 | return this 59 | } 60 | 61 | gt(column: ColumnName, value: Row[ColumnName]): this 62 | gt(column: string, value: unknown): this 63 | /** 64 | * Match only rows where `column` is greater than `value`. 65 | * 66 | * @param column - The column to filter on 67 | * @param value - The value to filter with 68 | */ 69 | gt(column: string, value: unknown): this { 70 | this.url.searchParams.append(column, `gt.${value}`) 71 | return this 72 | } 73 | 74 | gte(column: ColumnName, value: Row[ColumnName]): this 75 | gte(column: string, value: unknown): this 76 | /** 77 | * Match only rows where `column` is greater than or equal to `value`. 78 | * 79 | * @param column - The column to filter on 80 | * @param value - The value to filter with 81 | */ 82 | gte(column: string, value: unknown): this { 83 | this.url.searchParams.append(column, `gte.${value}`) 84 | return this 85 | } 86 | 87 | lt(column: ColumnName, value: Row[ColumnName]): this 88 | lt(column: string, value: unknown): this 89 | /** 90 | * Match only rows where `column` is less than `value`. 91 | * 92 | * @param column - The column to filter on 93 | * @param value - The value to filter with 94 | */ 95 | lt(column: string, value: unknown): this { 96 | this.url.searchParams.append(column, `lt.${value}`) 97 | return this 98 | } 99 | 100 | lte(column: ColumnName, value: Row[ColumnName]): this 101 | lte(column: string, value: unknown): this 102 | /** 103 | * Match only rows where `column` is less than or equal to `value`. 104 | * 105 | * @param column - The column to filter on 106 | * @param value - The value to filter with 107 | */ 108 | lte(column: string, value: unknown): this { 109 | this.url.searchParams.append(column, `lte.${value}`) 110 | return this 111 | } 112 | 113 | like(column: ColumnName, pattern: string): this 114 | like(column: string, pattern: string): this 115 | /** 116 | * Match only rows where `column` matches `pattern` case-sensitively. 117 | * 118 | * @param column - The column to filter on 119 | * @param pattern - The pattern to match with 120 | */ 121 | like(column: string, pattern: string): this { 122 | this.url.searchParams.append(column, `like.${pattern}`) 123 | return this 124 | } 125 | 126 | ilike(column: ColumnName, pattern: string): this 127 | ilike(column: string, pattern: string): this 128 | /** 129 | * Match only rows where `column` matches `pattern` case-insensitively. 130 | * 131 | * @param column - The column to filter on 132 | * @param pattern - The pattern to match with 133 | */ 134 | ilike(column: string, pattern: string): this { 135 | this.url.searchParams.append(column, `ilike.${pattern}`) 136 | return this 137 | } 138 | 139 | is( 140 | column: ColumnName, 141 | value: Row[ColumnName] & (boolean | null) 142 | ): this 143 | is(column: string, value: boolean | null): this 144 | /** 145 | * Match only rows where `column` IS `value`. 146 | * 147 | * For non-boolean columns, this is only relevant for checking if the value of 148 | * `column` is NULL by setting `value` to `null`. 149 | * 150 | * For boolean columns, you can also set `value` to `true` or `false` and it 151 | * will behave the same way as `.eq()`. 152 | * 153 | * @param column - The column to filter on 154 | * @param value - The value to filter with 155 | */ 156 | is(column: string, value: boolean | null): this { 157 | this.url.searchParams.append(column, `is.${value}`) 158 | return this 159 | } 160 | 161 | in(column: ColumnName, values: Row[ColumnName][]): this 162 | in(column: string, values: unknown[]): this 163 | /** 164 | * Match only rows where `column` is included in the `values` array. 165 | * 166 | * @param column - The column to filter on 167 | * @param values - The values array to filter with 168 | */ 169 | in(column: string, values: unknown[]): this { 170 | const cleanedValues = values 171 | .map((s) => { 172 | // handle postgrest reserved characters 173 | // https://postgrest.org/en/v7.0.0/api.html#reserved-characters 174 | if (typeof s === 'string' && new RegExp('[,()]').test(s)) return `"${s}"` 175 | else return `${s}` 176 | }) 177 | .join(',') 178 | this.url.searchParams.append(column, `in.(${cleanedValues})`) 179 | return this 180 | } 181 | 182 | contains( 183 | column: ColumnName, 184 | value: string | Row[ColumnName][] | Record 185 | ): this 186 | contains(column: string, value: string | unknown[] | Record): this 187 | /** 188 | * Only relevant for jsonb, array, and range columns. Match only rows where 189 | * `column` contains every element appearing in `value`. 190 | * 191 | * @param column - The jsonb, array, or range column to filter on 192 | * @param value - The jsonb, array, or range value to filter with 193 | */ 194 | contains(column: string, value: string | unknown[] | Record): this { 195 | if (typeof value === 'string') { 196 | // range types can be inclusive '[', ']' or exclusive '(', ')' so just 197 | // keep it simple and accept a string 198 | this.url.searchParams.append(column, `cs.${value}`) 199 | } else if (Array.isArray(value)) { 200 | // array 201 | this.url.searchParams.append(column, `cs.{${value.join(',')}}`) 202 | } else { 203 | // json 204 | this.url.searchParams.append(column, `cs.${JSON.stringify(value)}`) 205 | } 206 | return this 207 | } 208 | 209 | containedBy( 210 | column: ColumnName, 211 | value: string | Row[ColumnName][] | Record 212 | ): this 213 | containedBy(column: string, value: string | unknown[] | Record): this 214 | /** 215 | * Only relevant for jsonb, array, and range columns. Match only rows where 216 | * every element appearing in `column` is contained by `value`. 217 | * 218 | * @param column - The jsonb, array, or range column to filter on 219 | * @param value - The jsonb, array, or range value to filter with 220 | */ 221 | containedBy(column: string, value: string | unknown[] | Record): this { 222 | if (typeof value === 'string') { 223 | // range 224 | this.url.searchParams.append(column, `cd.${value}`) 225 | } else if (Array.isArray(value)) { 226 | // array 227 | this.url.searchParams.append(column, `cd.{${value.join(',')}}`) 228 | } else { 229 | // json 230 | this.url.searchParams.append(column, `cd.${JSON.stringify(value)}`) 231 | } 232 | return this 233 | } 234 | 235 | rangeGt(column: ColumnName, range: string): this 236 | rangeGt(column: string, range: string): this 237 | /** 238 | * Only relevant for range columns. Match only rows where every element in 239 | * `column` is greater than any element in `range`. 240 | * 241 | * @param column - The range column to filter on 242 | * @param range - The range to filter with 243 | */ 244 | rangeGt(column: string, range: string): this { 245 | this.url.searchParams.append(column, `sr.${range}`) 246 | return this 247 | } 248 | 249 | rangeGte(column: ColumnName, range: string): this 250 | rangeGte(column: string, range: string): this 251 | /** 252 | * Only relevant for range columns. Match only rows where every element in 253 | * `column` is either contained in `range` or greater than any element in 254 | * `range`. 255 | * 256 | * @param column - The range column to filter on 257 | * @param range - The range to filter with 258 | */ 259 | rangeGte(column: string, range: string): this { 260 | this.url.searchParams.append(column, `nxl.${range}`) 261 | return this 262 | } 263 | 264 | rangeLt(column: ColumnName, range: string): this 265 | rangeLt(column: string, range: string): this 266 | /** 267 | * Only relevant for range columns. Match only rows where every element in 268 | * `column` is less than any element in `range`. 269 | * 270 | * @param column - The range column to filter on 271 | * @param range - The range to filter with 272 | */ 273 | rangeLt(column: string, range: string): this { 274 | this.url.searchParams.append(column, `sl.${range}`) 275 | return this 276 | } 277 | 278 | rangeLte(column: ColumnName, range: string): this 279 | rangeLte(column: string, range: string): this 280 | /** 281 | * Only relevant for range columns. Match only rows where every element in 282 | * `column` is either contained in `range` or less than any element in 283 | * `range`. 284 | * 285 | * @param column - The range column to filter on 286 | * @param range - The range to filter with 287 | */ 288 | rangeLte(column: string, range: string): this { 289 | this.url.searchParams.append(column, `nxr.${range}`) 290 | return this 291 | } 292 | 293 | rangeAdjacent(column: ColumnName, range: string): this 294 | rangeAdjacent(column: string, range: string): this 295 | /** 296 | * Only relevant for range columns. Match only rows where `column` is 297 | * mutually exclusive to `range` and there can be no element between the two 298 | * ranges. 299 | * 300 | * @param column - The range column to filter on 301 | * @param range - The range to filter with 302 | */ 303 | rangeAdjacent(column: string, range: string): this { 304 | this.url.searchParams.append(column, `adj.${range}`) 305 | return this 306 | } 307 | 308 | overlaps( 309 | column: ColumnName, 310 | value: string | Row[ColumnName][] 311 | ): this 312 | overlaps(column: string, value: string | unknown[]): this 313 | /** 314 | * Only relevant for array and range columns. Match only rows where 315 | * `column` and `value` have an element in common. 316 | * 317 | * @param column - The array or range column to filter on 318 | * @param value - The array or range value to filter with 319 | */ 320 | overlaps(column: string, value: string | unknown[]): this { 321 | if (typeof value === 'string') { 322 | // range 323 | this.url.searchParams.append(column, `ov.${value}`) 324 | } else { 325 | // array 326 | this.url.searchParams.append(column, `ov.{${value.join(',')}}`) 327 | } 328 | return this 329 | } 330 | 331 | textSearch( 332 | column: ColumnName, 333 | query: string, 334 | options?: { config?: string; type?: 'plain' | 'phrase' | 'websearch' } 335 | ): this 336 | textSearch( 337 | column: string, 338 | query: string, 339 | options?: { config?: string; type?: 'plain' | 'phrase' | 'websearch' } 340 | ): this 341 | /** 342 | * Only relevant for text and tsvector columns. Match only rows where 343 | * `column` matches the query string in `query`. 344 | * 345 | * @param column - The text or tsvector column to filter on 346 | * @param query - The query text to match with 347 | * @param options - Named parameters 348 | * @param options.config - The text search configuration to use 349 | * @param options.type - Change how the `query` text is interpreted 350 | */ 351 | textSearch( 352 | column: string, 353 | query: string, 354 | { config, type }: { config?: string; type?: 'plain' | 'phrase' | 'websearch' } = {} 355 | ): this { 356 | let typePart = '' 357 | if (type === 'plain') { 358 | typePart = 'pl' 359 | } else if (type === 'phrase') { 360 | typePart = 'ph' 361 | } else if (type === 'websearch') { 362 | typePart = 'w' 363 | } 364 | const configPart = config === undefined ? '' : `(${config})` 365 | this.url.searchParams.append(column, `${typePart}fts${configPart}.${query}`) 366 | return this 367 | } 368 | 369 | match(query: Record): this 370 | match(query: Record): this 371 | /** 372 | * Match only rows where each column in `query` keys is equal to its 373 | * associated value. Shorthand for multiple `.eq()`s. 374 | * 375 | * @param query - The object to filter with, with column names as keys mapped 376 | * to their filter values 377 | */ 378 | match(query: Record): this { 379 | Object.entries(query).forEach(([column, value]) => { 380 | this.url.searchParams.append(column, `eq.${value}`) 381 | }) 382 | return this 383 | } 384 | 385 | not( 386 | column: ColumnName, 387 | operator: FilterOperator, 388 | value: Row[ColumnName] 389 | ): this 390 | not(column: string, operator: string, value: unknown): this 391 | /** 392 | * Match only rows which doesn't satisfy the filter. 393 | * 394 | * Unlike most filters, `opearator` and `value` are used as-is and need to 395 | * follow [PostgREST 396 | * syntax](https://postgrest.org/en/stable/api.html#operators). You also need 397 | * to make sure they are properly sanitized. 398 | * 399 | * @param column - The column to filter on 400 | * @param operator - The operator to be negated to filter with, following 401 | * PostgREST syntax 402 | * @param value - The value to filter with, following PostgREST syntax 403 | */ 404 | not(column: string, operator: string, value: unknown): this { 405 | this.url.searchParams.append(column, `not.${operator}.${value}`) 406 | return this 407 | } 408 | 409 | /** 410 | * Match only rows which satisfy at least one of the filters. 411 | * 412 | * Unlike most filters, `filters` is used as-is and needs to follow [PostgREST 413 | * syntax](https://postgrest.org/en/stable/api.html#operators). You also need 414 | * to make sure it's properly sanitized. 415 | * 416 | * It's currently not possible to do an `.or()` filter across multiple tables. 417 | * 418 | * @param filters - The filters to use, following PostgREST syntax 419 | * @param foreignTable - Set this to filter on foreign tables instead of the 420 | * current table 421 | */ 422 | or(filters: string, { foreignTable }: { foreignTable?: string } = {}): this { 423 | const key = foreignTable ? `${foreignTable}.or` : 'or' 424 | this.url.searchParams.append(key, `(${filters})`) 425 | return this 426 | } 427 | 428 | filter( 429 | column: ColumnName, 430 | operator: `${'' | 'not.'}${FilterOperator}`, 431 | value: unknown 432 | ): this 433 | filter(column: string, operator: string, value: unknown): this 434 | /** 435 | * Match only rows which satisfy the filter. This is an escape hatch - you 436 | * should use the specific filter methods wherever possible. 437 | * 438 | * Unlike most filters, `opearator` and `value` are used as-is and need to 439 | * follow [PostgREST 440 | * syntax](https://postgrest.org/en/stable/api.html#operators). You also need 441 | * to make sure they are properly sanitized. 442 | * 443 | * @param column - The column to filter on 444 | * @param operator - The operator to filter with, following PostgREST syntax 445 | * @param value - The value to filter with, following PostgREST syntax 446 | */ 447 | filter(column: string, operator: string, value: unknown): this { 448 | this.url.searchParams.append(column, `${operator}.${value}`) 449 | return this 450 | } 451 | } 452 | -------------------------------------------------------------------------------- /src/postgrest-js/src/PostgrestQueryBuilder.ts: -------------------------------------------------------------------------------- 1 | import PostgrestBuilder from './PostgrestBuilder' 2 | import PostgrestFilterBuilder from './PostgrestFilterBuilder' 3 | import { GetResult } from './select-query-parser' 4 | import { Fetch, GenericSchema, GenericTable, GenericView } from './types' 5 | 6 | export default class PostgrestQueryBuilder< 7 | Schema extends GenericSchema, 8 | Relation extends GenericTable | GenericView 9 | > { 10 | url: URL 11 | headers: Record 12 | schema?: string 13 | signal?: AbortSignal 14 | fetch?: Fetch 15 | 16 | constructor( 17 | url: URL, 18 | { 19 | headers = {}, 20 | schema, 21 | fetch, 22 | }: { 23 | headers?: Record 24 | schema?: string 25 | fetch?: Fetch 26 | } 27 | ) { 28 | this.url = url 29 | this.headers = headers 30 | this.schema = schema 31 | this.fetch = fetch 32 | } 33 | 34 | /** 35 | * Perform a SELECT query on the table or view. 36 | * 37 | * @param columns - The columns to retrieve, separated by commas 38 | * 39 | * @param options - Named parameters 40 | * 41 | * @param options.head - When set to `true`, `data` will not be returned. 42 | * Useful if you only need the count. 43 | * 44 | * @param options.count - Count algorithm to use to count rows in the table or view. 45 | * 46 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 47 | * hood. 48 | * 49 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 50 | * statistics under the hood. 51 | * 52 | * `"estimated"`: Uses exact count for low numbers and planned count for high 53 | * numbers. 54 | */ 55 | select>( 56 | columns?: Query, 57 | { 58 | head = false, 59 | count, 60 | }: { 61 | head?: boolean 62 | count?: 'exact' | 'planned' | 'estimated' 63 | } = {} 64 | ): PostgrestFilterBuilder { 65 | const method = head ? 'HEAD' : 'GET' 66 | // Remove whitespaces except when quoted 67 | let quoted = false 68 | const cleanedColumns = (columns ?? '*') 69 | .split('') 70 | .map((c) => { 71 | if (/\s/.test(c) && !quoted) { 72 | return '' 73 | } 74 | if (c === '"') { 75 | quoted = !quoted 76 | } 77 | return c 78 | }) 79 | .join('') 80 | this.url.searchParams.set('select', cleanedColumns) 81 | if (count) { 82 | this.headers['Prefer'] = `count=${count}` 83 | } 84 | 85 | return new PostgrestFilterBuilder({ 86 | method, 87 | url: this.url, 88 | headers: this.headers, 89 | schema: this.schema, 90 | fetch: this.fetch, 91 | allowEmpty: false, 92 | } as unknown as PostgrestBuilder) 93 | } 94 | 95 | /** 96 | * Perform an INSERT into the table or view. 97 | * 98 | * By default, inserted rows are not returned. To return it, chain the call 99 | * with `.select()`. 100 | * 101 | * @param values - The values to insert. Pass an object to insert a single row 102 | * or an array to insert multiple rows. 103 | * 104 | * @param options - Named parameters 105 | * 106 | * @param options.count - Count algorithm to use to count inserted rows. 107 | * 108 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 109 | * hood. 110 | * 111 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 112 | * statistics under the hood. 113 | * 114 | * `"estimated"`: Uses exact count for low numbers and planned count for high 115 | * numbers. 116 | */ 117 | insert( 118 | values: Row | Row[], 119 | { 120 | count, 121 | }: { 122 | count?: 'exact' | 'planned' | 'estimated' 123 | } = {} 124 | ): PostgrestFilterBuilder { 125 | const method = 'POST' 126 | 127 | const prefersHeaders = [] 128 | const body = values 129 | if (count) { 130 | prefersHeaders.push(`count=${count}`) 131 | } 132 | if (this.headers['Prefer']) { 133 | prefersHeaders.unshift(this.headers['Prefer']) 134 | } 135 | this.headers['Prefer'] = prefersHeaders.join(',') 136 | 137 | if (Array.isArray(values)) { 138 | const columns = values.reduce((acc, x) => acc.concat(Object.keys(x)), [] as string[]) 139 | if (columns.length > 0) { 140 | const uniqueColumns = [...new Set(columns)].map((column) => `"${column}"`) 141 | this.url.searchParams.set('columns', uniqueColumns.join(',')) 142 | } 143 | } 144 | 145 | return new PostgrestFilterBuilder({ 146 | method, 147 | url: this.url, 148 | headers: this.headers, 149 | schema: this.schema, 150 | body, 151 | fetch: this.fetch, 152 | allowEmpty: false, 153 | } as unknown as PostgrestBuilder) 154 | } 155 | 156 | /** 157 | * Perform an UPSERT on the table or view. Depending on the column(s) passed 158 | * to `onConflict`, `.upsert()` allows you to perform the equivalent of 159 | * `.insert()` if a row with the corresponding `onConflict` columns doesn't 160 | * exist, or if it does exist, perform an alternative action depending on 161 | * `ignoreDuplicates`. 162 | * 163 | * By default, upserted rows are not returned. To return it, chain the call 164 | * with `.select()`. 165 | * 166 | * @param values - The values to upsert with. Pass an object to upsert a 167 | * single row or an array to upsert multiple rows. 168 | * 169 | * @param options - Named parameters 170 | * 171 | * @param options.onConflict - Comma-separated UNIQUE column(s) to specify how 172 | * duplicate rows are determined. Two rows are duplicates if all the 173 | * `onConflict` columns are equal. 174 | * 175 | * @param options.ignoreDuplicates - If `true`, duplicate rows are ignored. If 176 | * `false`, duplicate rows are merged with existing rows. 177 | * 178 | * @param options.count - Count algorithm to use to count upserted rows. 179 | * 180 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 181 | * hood. 182 | * 183 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 184 | * statistics under the hood. 185 | * 186 | * `"estimated"`: Uses exact count for low numbers and planned count for high 187 | * numbers. 188 | */ 189 | upsert( 190 | values: Row | Row[], 191 | { 192 | onConflict, 193 | ignoreDuplicates = false, 194 | count, 195 | }: { 196 | onConflict?: string 197 | ignoreDuplicates?: boolean 198 | count?: 'exact' | 'planned' | 'estimated' 199 | } = {} 200 | ): PostgrestFilterBuilder { 201 | const method = 'POST' 202 | 203 | const prefersHeaders = [`resolution=${ignoreDuplicates ? 'ignore' : 'merge'}-duplicates`] 204 | 205 | if (onConflict !== undefined) this.url.searchParams.set('on_conflict', onConflict) 206 | const body = values 207 | if (count) { 208 | prefersHeaders.push(`count=${count}`) 209 | } 210 | if (this.headers['Prefer']) { 211 | prefersHeaders.unshift(this.headers['Prefer']) 212 | } 213 | this.headers['Prefer'] = prefersHeaders.join(',') 214 | 215 | return new PostgrestFilterBuilder({ 216 | method, 217 | url: this.url, 218 | headers: this.headers, 219 | schema: this.schema, 220 | body, 221 | fetch: this.fetch, 222 | allowEmpty: false, 223 | } as unknown as PostgrestBuilder) 224 | } 225 | 226 | /** 227 | * Perform an UPDATE on the table or view. 228 | * 229 | * By default, updated rows are not returned. To return it, chain the call 230 | * with `.select()` after filters. 231 | * 232 | * @param values - The values to update with 233 | * 234 | * @param options - Named parameters 235 | * 236 | * @param options.count - Count algorithm to use to count updated rows. 237 | * 238 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 239 | * hood. 240 | * 241 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 242 | * statistics under the hood. 243 | * 244 | * `"estimated"`: Uses exact count for low numbers and planned count for high 245 | * numbers. 246 | */ 247 | update( 248 | values: Row, 249 | { 250 | count, 251 | }: { 252 | count?: 'exact' | 'planned' | 'estimated' 253 | } = {} 254 | ): PostgrestFilterBuilder { 255 | const method = 'PATCH' 256 | const prefersHeaders = [] 257 | const body = values 258 | if (count) { 259 | prefersHeaders.push(`count=${count}`) 260 | } 261 | if (this.headers['Prefer']) { 262 | prefersHeaders.unshift(this.headers['Prefer']) 263 | } 264 | this.headers['Prefer'] = prefersHeaders.join(',') 265 | 266 | return new PostgrestFilterBuilder({ 267 | method, 268 | url: this.url, 269 | headers: this.headers, 270 | schema: this.schema, 271 | body, 272 | fetch: this.fetch, 273 | allowEmpty: false, 274 | } as unknown as PostgrestBuilder) 275 | } 276 | 277 | /** 278 | * Perform a DELETE on the table or view. 279 | * 280 | * By default, deleted rows are not returned. To return it, chain the call 281 | * with `.select()` after filters. 282 | * 283 | * @param options - Named parameters 284 | * 285 | * @param options.count - Count algorithm to use to count deleted rows. 286 | * 287 | * `"exact"`: Exact but slow count algorithm. Performs a `COUNT(*)` under the 288 | * hood. 289 | * 290 | * `"planned"`: Approximated but fast count algorithm. Uses the Postgres 291 | * statistics under the hood. 292 | * 293 | * `"estimated"`: Uses exact count for low numbers and planned count for high 294 | * numbers. 295 | */ 296 | delete({ 297 | count, 298 | }: { 299 | count?: 'exact' | 'planned' | 'estimated' 300 | } = {}): PostgrestFilterBuilder { 301 | const method = 'DELETE' 302 | const prefersHeaders = [] 303 | if (count) { 304 | prefersHeaders.push(`count=${count}`) 305 | } 306 | if (this.headers['Prefer']) { 307 | prefersHeaders.unshift(this.headers['Prefer']) 308 | } 309 | this.headers['Prefer'] = prefersHeaders.join(',') 310 | 311 | return new PostgrestFilterBuilder({ 312 | method, 313 | url: this.url, 314 | headers: this.headers, 315 | schema: this.schema, 316 | fetch: this.fetch, 317 | allowEmpty: false, 318 | } as unknown as PostgrestBuilder) 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /src/postgrest-js/src/PostgrestTransformBuilder.ts: -------------------------------------------------------------------------------- 1 | import PostgrestBuilder from './PostgrestBuilder' 2 | import { GetResult } from './select-query-parser' 3 | import { 4 | GenericSchema, 5 | PostgrestMaybeSingleResponse, 6 | PostgrestResponse, 7 | PostgrestSingleResponse, 8 | } from './types' 9 | 10 | export default class PostgrestTransformBuilder< 11 | Schema extends GenericSchema, 12 | Row extends Record, 13 | Result 14 | > extends PostgrestBuilder { 15 | /** 16 | * Perform a SELECT on the query result. 17 | * 18 | * By default, `.insert()`, `.update()`, `.upsert()`, and `.delete()` do not 19 | * return modified rows. By calling this method, modified rows are returned in 20 | * `data`. 21 | * 22 | * @param columns - The columns to retrieve, separated by commas 23 | */ 24 | select>( 25 | columns?: Query 26 | ): PostgrestTransformBuilder { 27 | // Remove whitespaces except when quoted 28 | let quoted = false 29 | const cleanedColumns = (columns ?? '*') 30 | .split('') 31 | .map((c) => { 32 | if (/\s/.test(c) && !quoted) { 33 | return '' 34 | } 35 | if (c === '"') { 36 | quoted = !quoted 37 | } 38 | return c 39 | }) 40 | .join('') 41 | this.url.searchParams.set('select', cleanedColumns) 42 | if (this.headers['Prefer']) { 43 | this.headers['Prefer'] += ',' 44 | } 45 | this.headers['Prefer'] += 'return=representation' 46 | return this as unknown as PostgrestTransformBuilder 47 | } 48 | 49 | order( 50 | column: ColumnName, 51 | options?: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: undefined } 52 | ): this 53 | order( 54 | column: string, 55 | options?: { ascending?: boolean; nullsFirst?: boolean; foreignTable: string } 56 | ): this 57 | /** 58 | * Order the query result by `column`. 59 | * 60 | * You can call this method multiple times to order by multiple columns. 61 | * 62 | * You can order foreign tables, but it doesn't affect the ordering of the 63 | * current table. 64 | * 65 | * @param column - The column to order by 66 | * @param options - Named parameters 67 | * @param options.ascending - If `true`, the result will be in ascending order 68 | * @param options.nullsFirst - If `true`, `null`s appear first. If `false`, 69 | * `null`s appear last. 70 | * @param options.foreignTable - Set this to order a foreign table by foreign 71 | * columns 72 | */ 73 | order( 74 | column: string, 75 | { 76 | ascending = true, 77 | nullsFirst, 78 | foreignTable, 79 | }: { ascending?: boolean; nullsFirst?: boolean; foreignTable?: string } = {} 80 | ): this { 81 | const key = foreignTable ? `${foreignTable}.order` : 'order' 82 | const existingOrder = this.url.searchParams.get(key) 83 | 84 | this.url.searchParams.set( 85 | key, 86 | `${existingOrder ? `${existingOrder},` : ''}${column}.${ascending ? 'asc' : 'desc'}${ 87 | nullsFirst === undefined ? '' : nullsFirst ? '.nullsfirst' : '.nullslast' 88 | }` 89 | ) 90 | return this 91 | } 92 | 93 | /** 94 | * Limit the query result by `count`. 95 | * 96 | * @param count - The maximum number of rows to return 97 | * @param options - Named parameters 98 | * @param options.foreignTable - Set this to limit rows of foreign tables 99 | * instead of the current table 100 | */ 101 | limit(count: number, { foreignTable }: { foreignTable?: string } = {}): this { 102 | const key = typeof foreignTable === 'undefined' ? 'limit' : `${foreignTable}.limit` 103 | this.url.searchParams.set(key, `${count}`) 104 | return this 105 | } 106 | 107 | /** 108 | * Limit the query result by `from` and `to` inclusively. 109 | * 110 | * @param from - The starting index from which to limit the result 111 | * @param to - The last index to which to limit the result 112 | * @param options - Named parameters 113 | * @param options.foreignTable - Set this to limit rows of foreign tables 114 | * instead of the current table 115 | */ 116 | range(from: number, to: number, { foreignTable }: { foreignTable?: string } = {}): this { 117 | const keyOffset = typeof foreignTable === 'undefined' ? 'offset' : `${foreignTable}.offset` 118 | const keyLimit = typeof foreignTable === 'undefined' ? 'limit' : `${foreignTable}.limit` 119 | this.url.searchParams.set(keyOffset, `${from}`) 120 | // Range is inclusive, so add 1 121 | this.url.searchParams.set(keyLimit, `${to - from + 1}`) 122 | return this 123 | } 124 | 125 | /** 126 | * Set the AbortSignal for the fetch request. 127 | * 128 | * @param signal - The AbortSignal to use for the fetch request 129 | */ 130 | abortSignal(signal: AbortSignal): this { 131 | this.signal = signal 132 | return this 133 | } 134 | 135 | /** 136 | * Return `data` as a single object instead of an array of objects. 137 | * 138 | * Query result must be one row (e.g. using `.limit(1)`), otherwise this 139 | * returns an error. 140 | */ 141 | single(): PromiseLike> { 142 | this.headers['Accept'] = 'application/vnd.pgrst.object+json' 143 | return this as PromiseLike> 144 | } 145 | 146 | /** 147 | * Return `data` as a single object instead of an array of objects. 148 | * 149 | * Query result must be zero or one row (e.g. using `.limit(1)`), otherwise 150 | * this returns an error. 151 | */ 152 | maybeSingle(): PromiseLike> { 153 | this.headers['Accept'] = 'application/vnd.pgrst.object+json' 154 | this.allowEmpty = true 155 | return this as PromiseLike> 156 | } 157 | 158 | /** 159 | * Return `data` as a string in CSV format. 160 | */ 161 | csv(): PromiseLike> { 162 | this.headers['Accept'] = 'text/csv' 163 | return this as PromiseLike> 164 | } 165 | 166 | /** 167 | * Return `data` as an object in [GeoJSON](https://geojson.org) format. 168 | */ 169 | geojson(): PromiseLike>> { 170 | this.headers['Accept'] = 'application/geo+json' 171 | return this as PromiseLike>> 172 | } 173 | 174 | /** 175 | * Return `data` as the EXPLAIN plan for the query. 176 | * 177 | * @param options - Named parameters 178 | * 179 | * @param options.analyze - If `true`, the query will be executed and the 180 | * actual run time will be returned 181 | * 182 | * @param options.verbose - If `true`, the query identifier will be returned 183 | * and `data` will include the output columns of the query 184 | * 185 | * @param options.settings - If `true`, include information on configuration 186 | * parameters that affect query planning 187 | * 188 | * @param options.buffers - If `true`, include information on buffer usage 189 | * 190 | * @param options.wal - If `true`, include information on WAL record generation 191 | * 192 | * @param options.format - The format of the output, can be `"text"` (default) 193 | * or `"json"` 194 | */ 195 | explain({ 196 | analyze = false, 197 | verbose = false, 198 | settings = false, 199 | buffers = false, 200 | wal = false, 201 | format = 'text', 202 | }: { 203 | analyze?: boolean 204 | verbose?: boolean 205 | settings?: boolean 206 | buffers?: boolean 207 | wal?: boolean 208 | format?: 'json' | 'text' 209 | } = {}): 210 | | PromiseLike>> 211 | | PromiseLike> { 212 | const options = [ 213 | analyze ? 'analyze' : null, 214 | verbose ? 'verbose' : null, 215 | settings ? 'settings' : null, 216 | buffers ? 'buffers' : null, 217 | wal ? 'wal' : null, 218 | ] 219 | .filter(Boolean) 220 | .join('|') 221 | // An Accept header can carry multiple media types but postgrest-js always sends one 222 | const forMediatype = this.headers['Accept'] 223 | this.headers[ 224 | 'Accept' 225 | ] = `application/vnd.pgrst.plan+${format}; for="${forMediatype}"; options=${options};` 226 | if (format === 'json') return this as PromiseLike>> 227 | else return this as PromiseLike> 228 | } 229 | 230 | /** 231 | * Rollback the query. 232 | * 233 | * `data` will still be returned, but the query is not committed. 234 | */ 235 | rollback(): this { 236 | if ((this.headers['Prefer'] ?? '').trim().length > 0) { 237 | this.headers['Prefer'] += ',tx=rollback' 238 | } else { 239 | this.headers['Prefer'] = 'tx=rollback' 240 | } 241 | return this 242 | } 243 | 244 | /** 245 | * Override the type of the returned `data`. 246 | * 247 | * @typeParam NewResult - The new result type to override with 248 | */ 249 | returns(): PostgrestTransformBuilder { 250 | return this as unknown as PostgrestTransformBuilder 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /src/postgrest-js/src/constants.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version' 2 | export const DEFAULT_HEADERS = { 'X-Client-Info': `postgrest-js/${version}` } 3 | -------------------------------------------------------------------------------- /src/postgrest-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { default as PostgrestClient } from './PostgrestClient' 2 | export { default as PostgrestQueryBuilder } from './PostgrestQueryBuilder' 3 | export { default as PostgrestFilterBuilder } from './PostgrestFilterBuilder' 4 | export { default as PostgrestTransformBuilder } from './PostgrestTransformBuilder' 5 | export { default as PostgrestBuilder } from './PostgrestBuilder' 6 | export { 7 | PostgrestResponse, 8 | PostgrestSingleResponse, 9 | PostgrestMaybeSingleResponse, 10 | PostgrestError, 11 | } from './types' 12 | -------------------------------------------------------------------------------- /src/postgrest-js/src/select-query-parser.ts: -------------------------------------------------------------------------------- 1 | // Credits to @bnjmnt4n (https://www.npmjs.com/package/postgrest-query) 2 | 3 | import { GenericSchema } from './types' 4 | 5 | type Whitespace = ' ' | '\n' | '\t' 6 | 7 | type LowerAlphabet = 8 | | 'a' 9 | | 'b' 10 | | 'c' 11 | | 'd' 12 | | 'e' 13 | | 'f' 14 | | 'g' 15 | | 'h' 16 | | 'i' 17 | | 'j' 18 | | 'k' 19 | | 'l' 20 | | 'm' 21 | | 'n' 22 | | 'o' 23 | | 'p' 24 | | 'q' 25 | | 'r' 26 | | 's' 27 | | 't' 28 | | 'u' 29 | | 'v' 30 | | 'w' 31 | | 'x' 32 | | 'y' 33 | | 'z' 34 | 35 | type Alphabet = LowerAlphabet | Uppercase 36 | 37 | type Digit = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9' | '0' 38 | 39 | type Letter = Alphabet | Digit | '_' 40 | 41 | // /** 42 | // * Parsed node types. 43 | // * Currently only `*` and all other fields. 44 | // */ 45 | // type ParsedNode = 46 | // | { star: true } 47 | // | { name: string; original: string } 48 | // | { name: string; foreignTable: true }; 49 | 50 | /** 51 | * Parser errors. 52 | */ 53 | type ParserError = { error: true } & Message 54 | type GenericStringError = ParserError<'Received a generic string'> 55 | 56 | /** 57 | * Trims whitespace from the left of the input. 58 | */ 59 | type EatWhitespace = string extends Input 60 | ? GenericStringError 61 | : Input extends `${Whitespace}${infer Remainder}` 62 | ? EatWhitespace 63 | : Input 64 | 65 | /** 66 | * Constructs a type definition for a single field of an object. 67 | * 68 | * @param Definitions Record of definitions, possibly generated from PostgREST's OpenAPI spec. 69 | * @param Name Name of the table being queried. 70 | * @param Field Single field parsed by `ParseQuery`. 71 | */ 72 | type ConstructFieldDefinition< 73 | Schema extends GenericSchema, 74 | Row extends Record, 75 | Field 76 | > = Field extends { 77 | star: true 78 | } 79 | ? Row 80 | : Field extends { name: string; original: string; children: unknown[] } 81 | ? { 82 | [_ in Field['name']]: GetResultHelper< 83 | Schema, 84 | (Schema['Tables'] & Schema['Views'])[Field['original']]['Row'], 85 | Field['children'], 86 | unknown 87 | > extends infer Child 88 | ? Child | Child[] | null 89 | : never 90 | } 91 | : Field extends { name: string; original: string } 92 | ? { [K in Field['name']]: Row[Field['original']] } 93 | : Record 94 | 95 | /** 96 | * Notes: all `Parse*` types assume that their input strings have their whitespace 97 | * removed. They return tuples of ["Return Value", "Remainder of text"] or 98 | * a `ParserError`. 99 | */ 100 | 101 | /** 102 | * Reads a consecutive sequence of more than 1 letter, 103 | * where letters are `[0-9a-zA-Z_]`. 104 | */ 105 | type ReadLetters = string extends Input 106 | ? GenericStringError 107 | : ReadLettersHelper extends [`${infer Letters}`, `${infer Remainder}`] 108 | ? Letters extends '' 109 | ? ParserError<`Expected letter at \`${Input}\``> 110 | : [Letters, Remainder] 111 | : ReadLettersHelper 112 | 113 | type ReadLettersHelper = string extends Input 114 | ? GenericStringError 115 | : Input extends `${infer L}${infer Remainder}` 116 | ? L extends Letter 117 | ? ReadLettersHelper 118 | : [Acc, Input] 119 | : [Acc, ''] 120 | 121 | /** 122 | * Parses an identifier. 123 | * For now, identifiers are just sequences of more than 1 letter. 124 | * 125 | * TODO: allow for double quoted strings. 126 | */ 127 | type ParseIdentifier = ReadLetters 128 | 129 | /** 130 | * Parses a node. 131 | * A node is one of the following: 132 | * - `*` 133 | * - `field` 134 | * - `field(nodes)` 135 | * - `field!hint(nodes)` 136 | * - `field!inner(nodes)` 137 | * - `field!hint!inner(nodes)` 138 | * - `renamed_field:field` 139 | * - `renamed_field:field(nodes)` 140 | * - `renamed_field:field!hint(nodes)` 141 | * - `renamed_field:field!inner(nodes)` 142 | * - `renamed_field:field!hint!inner(nodes)` 143 | * 144 | * TODO: casting operators `::text`, JSON operators `->`, `->>`. 145 | */ 146 | type ParseNode = Input extends '' 147 | ? ParserError<'Empty string'> 148 | : // `*` 149 | Input extends `*${infer Remainder}` 150 | ? [{ star: true }, EatWhitespace] 151 | : ParseIdentifier extends [infer Name, `${infer Remainder}`] 152 | ? EatWhitespace extends `!inner${infer Remainder}` 153 | ? ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] 154 | ? // `field!inner(nodes)` 155 | [{ name: Name; original: Name; children: Fields }, EatWhitespace] 156 | : ParseEmbeddedResource> extends ParserError 157 | ? ParseEmbeddedResource> 158 | : ParserError<'Expected embedded resource after `!inner`'> 159 | : EatWhitespace extends `!${infer Remainder}` 160 | ? ParseIdentifier> extends [infer _Hint, `${infer Remainder}`] 161 | ? EatWhitespace extends `!inner${infer Remainder}` 162 | ? ParseEmbeddedResource> extends [ 163 | infer Fields, 164 | `${infer Remainder}` 165 | ] 166 | ? // `field!hint!inner(nodes)` 167 | [{ name: Name; original: Name; children: Fields }, EatWhitespace] 168 | : ParseEmbeddedResource> extends ParserError 169 | ? ParseEmbeddedResource> 170 | : ParserError<'Expected embedded resource after `!inner`'> 171 | : ParseEmbeddedResource> extends [ 172 | infer Fields, 173 | `${infer Remainder}` 174 | ] 175 | ? // `field!hint(nodes)` 176 | [{ name: Name; original: Name; children: Fields }, EatWhitespace] 177 | : ParseEmbeddedResource> extends ParserError 178 | ? ParseEmbeddedResource> 179 | : ParserError<'Expected embedded resource after `!hint`'> 180 | : ParserError<'Expected identifier after `!`'> 181 | : EatWhitespace extends `:${infer Remainder}` 182 | ? ParseIdentifier> extends [infer OriginalName, `${infer Remainder}`] 183 | ? EatWhitespace extends `!inner${infer Remainder}` 184 | ? ParseEmbeddedResource> extends [ 185 | infer Fields, 186 | `${infer Remainder}` 187 | ] 188 | ? // `renamed_field:field!inner(nodes)` 189 | [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] 190 | : ParseEmbeddedResource> extends ParserError 191 | ? ParseEmbeddedResource> 192 | : ParserError<'Expected embedded resource after `!inner`'> 193 | : EatWhitespace extends `!${infer Remainder}` 194 | ? ParseIdentifier> extends [infer _Hint, `${infer Remainder}`] 195 | ? EatWhitespace extends `!inner${infer Remainder}` 196 | ? ParseEmbeddedResource> extends [ 197 | infer Fields, 198 | `${infer Remainder}` 199 | ] 200 | ? // `renamed_field:field!hint!inner(nodes)` 201 | [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] 202 | : ParseEmbeddedResource> extends ParserError 203 | ? ParseEmbeddedResource> 204 | : ParserError<'Expected embedded resource after `!inner`'> 205 | : ParseEmbeddedResource> extends [ 206 | infer Fields, 207 | `${infer Remainder}` 208 | ] 209 | ? // `renamed_field:field!hint(nodes)` 210 | [ 211 | { 212 | name: Name 213 | original: OriginalName 214 | children: Fields 215 | }, 216 | EatWhitespace 217 | ] 218 | : ParseEmbeddedResource> extends ParserError 219 | ? ParseEmbeddedResource> 220 | : ParserError<'Expected embedded resource after `!hint`'> 221 | : ParserError<'Expected identifier after `!`'> 222 | : ParseEmbeddedResource> extends [ 223 | infer Fields, 224 | `${infer Remainder}` 225 | ] 226 | ? // `renamed_field:field(nodes)` 227 | [{ name: Name; original: OriginalName; children: Fields }, EatWhitespace] 228 | : ParseEmbeddedResource> extends ParserError 229 | ? ParseEmbeddedResource> 230 | : // `renamed_field:field` 231 | [{ name: Name; original: OriginalName }, EatWhitespace] 232 | : ParseIdentifier> 233 | : ParseEmbeddedResource> extends [infer Fields, `${infer Remainder}`] 234 | ? // `field(nodes)` 235 | [{ name: Name; original: Name; children: Fields }, EatWhitespace] 236 | : ParseEmbeddedResource> extends ParserError 237 | ? ParseEmbeddedResource> 238 | : // `field` 239 | [{ name: Name; original: Name }, EatWhitespace] 240 | : ParserError<`Expected identifier at \`${Input}\``> 241 | 242 | /** 243 | * Parses an embedded resource, which is an opening `(`, followed by a sequence of 244 | * nodes, separated by `,`, then a closing `)`. 245 | * 246 | * Returns a tuple of ["Parsed fields", "Remainder of text"], an error, 247 | * or the original string input indicating that no opening `(` was found. 248 | */ 249 | type ParseEmbeddedResource = Input extends `(${infer Remainder}` 250 | ? ParseNodes> extends [infer Fields, `${infer Remainder}`] 251 | ? EatWhitespace extends `)${infer Remainder}` 252 | ? Fields extends [] 253 | ? ParserError<'Expected fields after `(`'> 254 | : [Fields, EatWhitespace] 255 | : ParserError<`Expected ")"`> 256 | : ParseNodes> 257 | : Input 258 | 259 | /** 260 | * Parses a sequence of nodes, separated by `,`. 261 | * 262 | * Returns a tuple of ["Parsed fields", "Remainder of text"] or an error. 263 | */ 264 | type ParseNodes = string extends Input 265 | ? GenericStringError 266 | : ParseNodesHelper 267 | 268 | type ParseNodesHelper = ParseNode extends [ 269 | infer Field, 270 | `${infer Remainder}` 271 | ] 272 | ? EatWhitespace extends `,${infer Remainder}` 273 | ? ParseNodesHelper, [Field, ...Fields]> 274 | : [[Field, ...Fields], EatWhitespace] 275 | : ParseNode 276 | 277 | /** 278 | * Parses a query. 279 | * A query is a sequence of nodes, separated by `,`, ensuring that there is 280 | * no remaining input after all nodes have been parsed. 281 | * 282 | * Returns an array of parsed nodes, or an error. 283 | */ 284 | type ParseQuery = string extends Query 285 | ? GenericStringError 286 | : ParseNodes> extends [infer Fields, `${infer Remainder}`] 287 | ? EatWhitespace extends '' 288 | ? Fields 289 | : ParserError<`Unexpected input: ${Remainder}`> 290 | : ParseNodes> 291 | 292 | type GetResultHelper< 293 | Schema extends GenericSchema, 294 | Row extends Record, 295 | Fields extends unknown[], 296 | Acc 297 | > = Fields extends [infer R] 298 | ? GetResultHelper & Acc> 299 | : Fields extends [infer R, ...infer Rest] 300 | ? GetResultHelper & Acc> 301 | : Acc 302 | 303 | /** 304 | * Constructs a type definition for an object based on a given PostgREST query. 305 | * 306 | * @param Row Record. 307 | * @param Query Select query string literal to parse. 308 | */ 309 | export type GetResult< 310 | Schema extends GenericSchema, 311 | Row extends Record, 312 | Query extends string 313 | > = ParseQuery extends unknown[] 314 | ? GetResultHelper, unknown> 315 | : ParseQuery 316 | -------------------------------------------------------------------------------- /src/postgrest-js/src/types.ts: -------------------------------------------------------------------------------- 1 | export type Fetch = typeof fetch 2 | 3 | /** 4 | * Error format 5 | * 6 | * {@link https://postgrest.org/en/stable/api.html?highlight=options#errors-and-http-status-codes} 7 | */ 8 | export type PostgrestError = { 9 | message: string 10 | details: string 11 | hint: string 12 | code: string 13 | } 14 | 15 | /** 16 | * Response format 17 | * 18 | * {@link https://github.com/supabase/supabase-js/issues/32} 19 | */ 20 | interface PostgrestResponseBase { 21 | status: number 22 | statusText: string 23 | } 24 | 25 | interface PostgrestResponseSuccess extends PostgrestResponseBase { 26 | error: null 27 | data: T[] 28 | count: number | null 29 | } 30 | interface PostgrestResponseFailure extends PostgrestResponseBase { 31 | error: PostgrestError 32 | data: null 33 | count: null 34 | } 35 | export type PostgrestResponse = PostgrestResponseSuccess | PostgrestResponseFailure 36 | 37 | interface PostgrestSingleResponseSuccess extends PostgrestResponseBase { 38 | error: null 39 | data: T 40 | count: number | null 41 | } 42 | export type PostgrestSingleResponse = 43 | | PostgrestSingleResponseSuccess 44 | | PostgrestResponseFailure 45 | export type PostgrestMaybeSingleResponse = PostgrestSingleResponse 46 | 47 | export type GenericTable = { 48 | Row: Record 49 | Insert: Record 50 | Update: Record 51 | } 52 | 53 | export type GenericUpdatableView = { 54 | Row: Record 55 | Insert: Record 56 | Update: Record 57 | } 58 | 59 | export type GenericNonUpdatableView = { 60 | Row: Record 61 | } 62 | 63 | export type GenericView = GenericUpdatableView | GenericNonUpdatableView 64 | 65 | export type GenericFunction = { 66 | Args: Record 67 | Returns: unknown 68 | } 69 | 70 | export type GenericSchema = { 71 | Tables: Record 72 | Views: Record 73 | Functions: Record 74 | } 75 | -------------------------------------------------------------------------------- /src/postgrest-js/src/version.ts: -------------------------------------------------------------------------------- 1 | export const version = '1.1.1' 2 | -------------------------------------------------------------------------------- /src/realtime-js/src/RealtimeClient.ts: -------------------------------------------------------------------------------- 1 | // import { w3cwebsocket } from 'websocket' 2 | let { URLSearchParams } = require('../../wechaturl-parse/index') 3 | import { 4 | VSN, 5 | CHANNEL_EVENTS, 6 | TRANSPORTS, 7 | SOCKET_STATES, 8 | DEFAULT_TIMEOUT, 9 | WS_CLOSE_NORMAL, 10 | DEFAULT_HEADERS, 11 | CONNECTION_STATE, 12 | } from './lib/constants' 13 | import Timer from './lib/timer' 14 | import Serializer from './lib/serializer' 15 | import RealtimeChannel, { RealtimeChannelOptions } from './RealtimeChannel' 16 | 17 | export type RealtimeClientOptions = { 18 | transport?: WebSocket 19 | timeout?: number 20 | heartbeatIntervalMs?: number 21 | logger?: Function 22 | encode?: Function 23 | decode?: Function 24 | reconnectAfterMs?: Function 25 | headers?: { [key: string]: string } 26 | params?: { [key: string]: any } 27 | } 28 | 29 | export type RealtimeMessage = { 30 | topic: string 31 | event: string 32 | payload: any 33 | ref: string 34 | join_ref?: string 35 | } 36 | 37 | export type RealtimeRemoveChannelResponse = 'ok' | 'timed out' | 'error' 38 | 39 | const noop = () => {} 40 | 41 | export default class RealtimeClient { 42 | accessToken: string | null = null 43 | channels: RealtimeChannel[] = [] 44 | endPoint: string = '' 45 | headers?: { [key: string]: string } = DEFAULT_HEADERS 46 | params?: { [key: string]: string } = {} 47 | timeout: number = DEFAULT_TIMEOUT 48 | transport: any = null 49 | heartbeatIntervalMs: number = 30000 50 | heartbeatTimer: ReturnType | undefined = undefined 51 | pendingHeartbeatRef: string | null = null 52 | ref: number = 0 53 | reconnectTimer: Timer 54 | logger: Function = noop 55 | encode: Function 56 | decode: Function 57 | reconnectAfterMs: Function 58 | conn: WebSocket | null = null 59 | sendBuffer: Function[] = [] 60 | serializer: Serializer = new Serializer() 61 | stateChangeCallbacks: { 62 | open: Function[] 63 | close: Function[] 64 | error: Function[] 65 | message: Function[] 66 | } = { 67 | open: [], 68 | close: [], 69 | error: [], 70 | message: [], 71 | } 72 | eventsPerSecondLimitMs: number = 100 73 | inThrottle: boolean = false 74 | 75 | /** 76 | * Initializes the Socket. 77 | * 78 | * @param endPoint The string WebSocket endpoint, ie, "ws://example.com/socket", "wss://example.com", "/socket" (inherited host & protocol) 79 | * @param options.transport The Websocket Transport, for example WebSocket. 80 | * @param options.timeout The default timeout in milliseconds to trigger push timeouts. 81 | * @param options.params The optional params to pass when connecting. 82 | * @param options.headers The optional headers to pass when connecting. 83 | * @param options.heartbeatIntervalMs The millisec interval to send a heartbeat message. 84 | * @param options.logger The optional function for specialized logging, ie: logger: (kind, msg, data) => { console.log(`${kind}: ${msg}`, data) } 85 | * @param options.encode The function to encode outgoing messages. Defaults to JSON: (payload, callback) => callback(JSON.stringify(payload)) 86 | * @param options.decode The function to decode incoming messages. Defaults to Serializer's decode. 87 | * @param options.reconnectAfterMs he optional function that returns the millsec reconnect interval. Defaults to stepped backoff off. 88 | */ 89 | constructor(endPoint: string, options?: RealtimeClientOptions) { 90 | this.endPoint = `${endPoint}/${TRANSPORTS.websocket}` 91 | 92 | if (options?.params) this.params = options.params 93 | if (options?.headers) this.headers = { ...this.headers, ...options.headers } 94 | if (options?.timeout) this.timeout = options.timeout 95 | if (options?.logger) this.logger = options.logger 96 | if (options?.transport) this.transport = options.transport 97 | if (options?.heartbeatIntervalMs) this.heartbeatIntervalMs = options.heartbeatIntervalMs 98 | 99 | const eventsPerSecond = options?.params?.eventsPerSecond 100 | if (eventsPerSecond) this.eventsPerSecondLimitMs = Math.floor(1000 / eventsPerSecond) 101 | 102 | this.reconnectAfterMs = options?.reconnectAfterMs 103 | ? options.reconnectAfterMs 104 | : (tries: number) => { 105 | return [1000, 2000, 5000, 10000][tries - 1] || 10000 106 | } 107 | this.encode = options?.encode 108 | ? options.encode 109 | : (payload: JSON, callback: Function) => { 110 | return callback(JSON.stringify(payload)) 111 | } 112 | this.decode = options?.decode ? options.decode : this.serializer.decode.bind(this.serializer) 113 | this.reconnectTimer = new Timer(async () => { 114 | this.disconnect() 115 | this.connect() 116 | }, this.reconnectAfterMs) 117 | } 118 | 119 | /** 120 | * Connects the socket, unless already connected. 121 | */ 122 | connect(): void { 123 | if (this.conn) { 124 | return 125 | } 126 | // let url = this._endPointURL() 127 | // this.ws_connect(url,(data:any)=>{ 128 | // console.log(data,'datad') 129 | // }) 130 | this.conn = wx.connectSocket({ url: this._endPointURL(), header: this.headers }) 131 | if (this.conn) { 132 | this.conn.binaryType = 'arraybuffer' 133 | // this.conn.onOpen = () => { 134 | // console.log('收到小心') 135 | // this._onConnOpen() 136 | // } 137 | this.conn.onOpen((res) => { 138 | this._onConnOpen() 139 | }) 140 | this.conn.onClose((event) => { 141 | this._onConnClose(event) 142 | }) 143 | this.conn.onError((error) => { 144 | this._onConnError(error as unknown as ErrorEvent) 145 | }) 146 | this.conn.onMessage((event) => { 147 | // var data = JSON.parse(onMessage.data); 148 | this._onConnMessage(event) 149 | // reMsg(onMessage.data); 150 | }) 151 | } 152 | } 153 | 154 | /** 155 | * Disconnects the socket. 156 | * 157 | * @param code A numeric status code to send on disconnect. 158 | * @param reason A custom reason for the disconnect. 159 | */ 160 | disconnect(code?: number, reason?: string): void { 161 | if (this.conn) { 162 | this.conn.onClose = function () {} // noop 163 | if (code) { 164 | this.conn.close({ 165 | success(data) { 166 | code, reason ?? '' 167 | }, 168 | }) 169 | } else { 170 | this.conn.close() 171 | } 172 | this.conn = null 173 | // remove open handles 174 | this.heartbeatTimer && clearInterval(this.heartbeatTimer) 175 | this.reconnectTimer.reset() 176 | } 177 | } 178 | 179 | /** 180 | * Returns all created channels 181 | */ 182 | getChannels(): RealtimeChannel[] { 183 | return this.channels 184 | } 185 | 186 | /** 187 | * Unsubscribes and removes a single channel 188 | * @param channel A RealtimeChannel instance 189 | */ 190 | removeChannel(channel: RealtimeChannel): Promise { 191 | return channel.unsubscribe().then((status) => { 192 | if (this.channels.length === 0) { 193 | this.disconnect() 194 | } 195 | return status 196 | }) 197 | } 198 | 199 | /** 200 | * Unsubscribes and removes all channels 201 | */ 202 | removeAllChannels(): Promise { 203 | return Promise.all(this.channels.map((channel) => channel.unsubscribe())).then((values) => { 204 | this.disconnect() 205 | return values 206 | }) 207 | } 208 | 209 | /** 210 | * Logs the message. 211 | * 212 | * For customized logging, `this.logger` can be overridden. 213 | */ 214 | log(kind: string, msg: string, data?: any) { 215 | this.logger(kind, msg, data) 216 | } 217 | 218 | /** 219 | * Returns the current state of the socket. 220 | */ 221 | connectionState(): CONNECTION_STATE { 222 | switch (this.conn && this.conn.readyState) { 223 | case SOCKET_STATES.connecting: 224 | return CONNECTION_STATE.Connecting 225 | case SOCKET_STATES.open: 226 | return CONNECTION_STATE.Open 227 | case SOCKET_STATES.closing: 228 | return CONNECTION_STATE.Closing 229 | default: 230 | return CONNECTION_STATE.Closed 231 | } 232 | } 233 | 234 | /** 235 | * Returns `true` is the connection is open. 236 | */ 237 | isConnected(): boolean { 238 | return this.connectionState() === CONNECTION_STATE.Open 239 | } 240 | 241 | channel(topic: string, params: RealtimeChannelOptions = { config: {} }): RealtimeChannel { 242 | if (!this.isConnected()) { 243 | this.connect() 244 | } 245 | 246 | const chan = new RealtimeChannel(`realtime:${topic}`, params, this) 247 | this.channels.push(chan) 248 | return chan 249 | } 250 | 251 | /** 252 | * Push out a message if the socket is connected. 253 | * 254 | * If the socket is not connected, the message gets enqueued within a local buffer, and sent out when a connection is next established. 255 | */ 256 | push(data: RealtimeMessage): 'rate limited' | void { 257 | const { topic, event, payload, ref } = data 258 | let callback = () => { 259 | this.encode(data, (result: any) => { 260 | this.conn?.send({ 261 | data: result, 262 | }) 263 | }) 264 | } 265 | this.log('push', `${topic} ${event} (${ref})`, payload) 266 | if (this.isConnected()) { 267 | if (['broadcast', 'presence', 'postgres_changes'].includes(event)) { 268 | const isThrottled = this._throttle(callback)() 269 | if (isThrottled) { 270 | return 'rate limited' 271 | } 272 | } else { 273 | callback() 274 | } 275 | } else { 276 | this.sendBuffer.push(callback) 277 | } 278 | } 279 | 280 | /** 281 | * Sets the JWT access token used for channel subscription authorization and Realtime RLS. 282 | * 283 | * @param token A JWT string. 284 | */ 285 | setAuth(token: string | null): void { 286 | this.accessToken = token 287 | 288 | this.channels.forEach((channel) => { 289 | token && channel.updateJoinPayload({ access_token: token }) 290 | 291 | if (channel.joinedOnce && channel._isJoined()) { 292 | channel._pushEvent(CHANNEL_EVENTS.access_token, { access_token: token }) 293 | } 294 | }) 295 | } 296 | 297 | /** 298 | * Return the next message ref, accounting for overflows 299 | * 300 | * @internal 301 | */ 302 | _makeRef(): string { 303 | let newRef = this.ref + 1 304 | if (newRef === this.ref) { 305 | this.ref = 0 306 | } else { 307 | this.ref = newRef 308 | } 309 | 310 | return this.ref.toString() 311 | } 312 | 313 | /** 314 | * Unsubscribe from channels with the specified topic. 315 | * 316 | * @internal 317 | */ 318 | _leaveOpenTopic(topic: string): void { 319 | let dupChannel = this.channels.find( 320 | (c) => c.topic === topic && (c._isJoined() || c._isJoining()) 321 | ) 322 | if (dupChannel) { 323 | this.log('transport', `leaving duplicate topic "${topic}"`) 324 | dupChannel.unsubscribe() 325 | } 326 | } 327 | 328 | /** 329 | * Removes a subscription from the socket. 330 | * 331 | * @param channel An open subscription. 332 | * 333 | * @internal 334 | */ 335 | _remove(channel: RealtimeChannel) { 336 | this.channels = this.channels.filter( 337 | (c: RealtimeChannel) => c._joinRef() !== channel._joinRef() 338 | ) 339 | } 340 | 341 | /** 342 | * Returns the URL of the websocket. 343 | * 344 | * @internal 345 | */ 346 | private _endPointURL(): string { 347 | return this._appendParams(this.endPoint, Object.assign({}, this.params, { vsn: VSN })) 348 | } 349 | 350 | /** @internal */ 351 | private _onConnMessage(rawMessage: { data: any }) { 352 | this.decode(rawMessage.data, (msg: RealtimeMessage) => { 353 | let { topic, event, payload, ref } = msg 354 | 355 | if ((ref && ref === this.pendingHeartbeatRef) || event === payload?.type) { 356 | this.pendingHeartbeatRef = null 357 | } 358 | 359 | this.log( 360 | 'receive', 361 | `${payload.status || ''} ${topic} ${event} ${(ref && '(' + ref + ')') || ''}`, 362 | payload 363 | ) 364 | this.channels 365 | .filter((channel: RealtimeChannel) => channel._isMember(topic)) 366 | .forEach((channel: RealtimeChannel) => channel._trigger(event, payload, ref)) 367 | this.stateChangeCallbacks.message.forEach((callback) => callback(msg)) 368 | }) 369 | } 370 | 371 | /** @internal */ 372 | private _onConnOpen() { 373 | this.log('transport', `connected to ${this._endPointURL()}`) 374 | this._flushSendBuffer() 375 | this.reconnectTimer.reset() 376 | this.heartbeatTimer && clearInterval(this.heartbeatTimer) 377 | this.heartbeatTimer = setInterval(() => this._sendHeartbeat(), this.heartbeatIntervalMs) 378 | this.stateChangeCallbacks.open.forEach((callback) => callback())! 379 | } 380 | 381 | /** @internal */ 382 | private _onConnClose(event: any) { 383 | this.log('transport', 'close', event) 384 | this._triggerChanError() 385 | this.heartbeatTimer && clearInterval(this.heartbeatTimer) 386 | this.reconnectTimer.scheduleTimeout() 387 | this.stateChangeCallbacks.close.forEach((callback) => callback(event)) 388 | } 389 | 390 | /** @internal */ 391 | private _onConnError(error: ErrorEvent) { 392 | this.log('transport', error.message) 393 | this._triggerChanError() 394 | this.stateChangeCallbacks.error.forEach((callback) => callback(error)) 395 | } 396 | 397 | /** @internal */ 398 | private _triggerChanError() { 399 | this.channels.forEach((channel: RealtimeChannel) => channel._trigger(CHANNEL_EVENTS.error)) 400 | } 401 | 402 | /** @internal */ 403 | private _appendParams(url: string, params: { [key: string]: string }): string { 404 | if (Object.keys(params).length === 0) { 405 | return url 406 | } 407 | const prefix = url.match(/\?/) ? '&' : '?' 408 | const query = new URLSearchParams(params) 409 | 410 | return `${url}${prefix}${query}` 411 | } 412 | 413 | /** @internal */ 414 | private _flushSendBuffer() { 415 | if (this.isConnected() && this.sendBuffer.length > 0) { 416 | this.sendBuffer.forEach((callback) => callback()) 417 | this.sendBuffer = [] 418 | } 419 | } 420 | /** @internal */ 421 | private _sendHeartbeat() { 422 | if (!this.isConnected()) { 423 | return 424 | } 425 | if (this.pendingHeartbeatRef) { 426 | this.pendingHeartbeatRef = null 427 | this.log('transport', 'heartbeat timeout. Attempting to re-establish connection') 428 | this.conn?.close(WS_CLOSE_NORMAL, 'hearbeat timeout') 429 | return 430 | } 431 | this.pendingHeartbeatRef = this._makeRef() 432 | this.push({ 433 | topic: 'phoenix', 434 | event: 'heartbeat', 435 | payload: {}, 436 | ref: this.pendingHeartbeatRef, 437 | }) 438 | this.setAuth(this.accessToken) 439 | } 440 | 441 | /** @internal */ 442 | private _throttle( 443 | callback: Function, 444 | eventsPerSecondLimitMs: number = this.eventsPerSecondLimitMs 445 | ): () => boolean { 446 | return () => { 447 | if (this.inThrottle) return true 448 | 449 | callback() 450 | 451 | if (eventsPerSecondLimitMs > 0) { 452 | this.inThrottle = true 453 | 454 | setTimeout(() => { 455 | this.inThrottle = false 456 | }, eventsPerSecondLimitMs) 457 | } 458 | 459 | return false 460 | } 461 | } 462 | } 463 | -------------------------------------------------------------------------------- /src/realtime-js/src/RealtimePresence.ts: -------------------------------------------------------------------------------- 1 | /* 2 | This file draws heavily from https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/assets/js/phoenix/presence.js 3 | License: https://github.com/phoenixframework/phoenix/blob/d344ec0a732ab4ee204215b31de69cf4be72e3bf/LICENSE.md 4 | */ 5 | 6 | import { PresenceOpts, PresenceOnJoinCallback, PresenceOnLeaveCallback } from 'phoenix' 7 | import RealtimeChannel from './RealtimeChannel' 8 | 9 | type Presence = { 10 | presence_ref: string 11 | } & T 12 | 13 | export type RealtimePresenceState = { [key: string]: Presence[] } 14 | 15 | export type RealtimePresenceJoinPayload = { 16 | event: `${REALTIME_PRESENCE_LISTEN_EVENTS.JOIN}` 17 | key: string 18 | currentPresences: Presence[] 19 | newPresences: Presence[] 20 | } 21 | 22 | export type RealtimePresenceLeavePayload = { 23 | event: `${REALTIME_PRESENCE_LISTEN_EVENTS.LEAVE}` 24 | key: string 25 | currentPresences: Presence[] 26 | leftPresences: Presence[] 27 | } 28 | 29 | export enum REALTIME_PRESENCE_LISTEN_EVENTS { 30 | SYNC = 'sync', 31 | JOIN = 'join', 32 | LEAVE = 'leave', 33 | } 34 | 35 | type PresenceDiff = { 36 | joins: RealtimePresenceState 37 | leaves: RealtimePresenceState 38 | } 39 | 40 | type RawPresenceState = { 41 | [key: string]: { 42 | metas: { 43 | phx_ref?: string 44 | phx_ref_prev?: string 45 | [key: string]: any 46 | }[] 47 | } 48 | } 49 | 50 | type RawPresenceDiff = { 51 | joins: RawPresenceState 52 | leaves: RawPresenceState 53 | } 54 | 55 | type PresenceChooser = (key: string, presences: Presence[]) => T 56 | 57 | export default class RealtimePresence { 58 | state: RealtimePresenceState = {} 59 | pendingDiffs: RawPresenceDiff[] = [] 60 | joinRef: string | null = null 61 | caller: { 62 | onJoin: PresenceOnJoinCallback 63 | onLeave: PresenceOnLeaveCallback 64 | onSync: () => void 65 | } = { 66 | onJoin: () => {}, 67 | onLeave: () => {}, 68 | onSync: () => {}, 69 | } 70 | 71 | /** 72 | * Initializes the Presence. 73 | * 74 | * @param channel - The RealtimeChannel 75 | * @param opts - The options, 76 | * for example `{events: {state: 'state', diff: 'diff'}}` 77 | */ 78 | constructor(public channel: RealtimeChannel, opts?: PresenceOpts) { 79 | const events = opts?.events || { 80 | state: 'presence_state', 81 | diff: 'presence_diff', 82 | } 83 | 84 | this.channel._on(events.state, {}, (newState: RawPresenceState) => { 85 | const { onJoin, onLeave, onSync } = this.caller 86 | 87 | this.joinRef = this.channel._joinRef() 88 | 89 | this.state = RealtimePresence.syncState(this.state, newState, onJoin, onLeave) 90 | 91 | this.pendingDiffs.forEach((diff) => { 92 | this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave) 93 | }) 94 | 95 | this.pendingDiffs = [] 96 | 97 | onSync() 98 | }) 99 | 100 | this.channel._on(events.diff, {}, (diff: RawPresenceDiff) => { 101 | const { onJoin, onLeave, onSync } = this.caller 102 | 103 | if (this.inPendingSyncState()) { 104 | this.pendingDiffs.push(diff) 105 | } else { 106 | this.state = RealtimePresence.syncDiff(this.state, diff, onJoin, onLeave) 107 | 108 | onSync() 109 | } 110 | }) 111 | 112 | this.onJoin((key, currentPresences, newPresences) => { 113 | this.channel._trigger('presence', { 114 | event: 'join', 115 | key, 116 | currentPresences, 117 | newPresences, 118 | }) 119 | }) 120 | 121 | this.onLeave((key, currentPresences, leftPresences) => { 122 | this.channel._trigger('presence', { 123 | event: 'leave', 124 | key, 125 | currentPresences, 126 | leftPresences, 127 | }) 128 | }) 129 | 130 | this.onSync(() => { 131 | this.channel._trigger('presence', { event: 'sync' }) 132 | }) 133 | } 134 | 135 | /** 136 | * Used to sync the list of presences on the server with the 137 | * client's state. 138 | * 139 | * An optional `onJoin` and `onLeave` callback can be provided to 140 | * react to changes in the client's local presences across 141 | * disconnects and reconnects with the server. 142 | * 143 | * @internal 144 | */ 145 | private static syncState( 146 | currentState: RealtimePresenceState, 147 | newState: RawPresenceState | RealtimePresenceState, 148 | onJoin: PresenceOnJoinCallback, 149 | onLeave: PresenceOnLeaveCallback 150 | ): RealtimePresenceState { 151 | const state = this.cloneDeep(currentState) 152 | const transformedState = this.transformState(newState) 153 | const joins: RealtimePresenceState = {} 154 | const leaves: RealtimePresenceState = {} 155 | 156 | this.map(state, (key: string, presences: Presence[]) => { 157 | if (!transformedState[key]) { 158 | leaves[key] = presences 159 | } 160 | }) 161 | 162 | this.map(transformedState, (key, newPresences: Presence[]) => { 163 | const currentPresences: Presence[] = state[key] 164 | 165 | if (currentPresences) { 166 | const newPresenceRefs = newPresences.map((m: Presence) => m.presence_ref) 167 | const curPresenceRefs = currentPresences.map((m: Presence) => m.presence_ref) 168 | const joinedPresences: Presence[] = newPresences.filter( 169 | (m: Presence) => curPresenceRefs.indexOf(m.presence_ref) < 0 170 | ) 171 | const leftPresences: Presence[] = currentPresences.filter( 172 | (m: Presence) => newPresenceRefs.indexOf(m.presence_ref) < 0 173 | ) 174 | 175 | if (joinedPresences.length > 0) { 176 | joins[key] = joinedPresences 177 | } 178 | 179 | if (leftPresences.length > 0) { 180 | leaves[key] = leftPresences 181 | } 182 | } else { 183 | joins[key] = newPresences 184 | } 185 | }) 186 | 187 | return this.syncDiff(state, { joins, leaves }, onJoin, onLeave) 188 | } 189 | 190 | /** 191 | * Used to sync a diff of presence join and leave events from the 192 | * server, as they happen. 193 | * 194 | * Like `syncState`, `syncDiff` accepts optional `onJoin` and 195 | * `onLeave` callbacks to react to a user joining or leaving from a 196 | * device. 197 | * 198 | * @internal 199 | */ 200 | private static syncDiff( 201 | state: RealtimePresenceState, 202 | diff: RawPresenceDiff | PresenceDiff, 203 | onJoin: PresenceOnJoinCallback, 204 | onLeave: PresenceOnLeaveCallback 205 | ): RealtimePresenceState { 206 | const { joins, leaves } = { 207 | joins: this.transformState(diff.joins), 208 | leaves: this.transformState(diff.leaves), 209 | } 210 | 211 | if (!onJoin) { 212 | onJoin = () => {} 213 | } 214 | 215 | if (!onLeave) { 216 | onLeave = () => {} 217 | } 218 | 219 | this.map(joins, (key, newPresences: Presence[]) => { 220 | const currentPresences: Presence[] = state[key] ?? [] 221 | state[key] = this.cloneDeep(newPresences) 222 | 223 | if (currentPresences.length > 0) { 224 | const joinedPresenceRefs = state[key].map((m: Presence) => m.presence_ref) 225 | const curPresences: Presence[] = currentPresences.filter( 226 | (m: Presence) => joinedPresenceRefs.indexOf(m.presence_ref) < 0 227 | ) 228 | 229 | state[key].unshift(...curPresences) 230 | } 231 | 232 | onJoin(key, currentPresences, newPresences) 233 | }) 234 | 235 | this.map(leaves, (key, leftPresences: Presence[]) => { 236 | let currentPresences: Presence[] = state[key] 237 | 238 | if (!currentPresences) return 239 | 240 | const presenceRefsToRemove = leftPresences.map((m: Presence) => m.presence_ref) 241 | currentPresences = currentPresences.filter( 242 | (m: Presence) => presenceRefsToRemove.indexOf(m.presence_ref) < 0 243 | ) 244 | 245 | state[key] = currentPresences 246 | 247 | onLeave(key, currentPresences, leftPresences) 248 | 249 | if (currentPresences.length === 0) delete state[key] 250 | }) 251 | 252 | return state 253 | } 254 | 255 | /** @internal */ 256 | private static map(obj: RealtimePresenceState, func: PresenceChooser): T[] { 257 | return Object.getOwnPropertyNames(obj).map((key) => func(key, obj[key])) 258 | } 259 | 260 | /** 261 | * Remove 'metas' key 262 | * Change 'phx_ref' to 'presence_ref' 263 | * Remove 'phx_ref' and 'phx_ref_prev' 264 | * 265 | * @example 266 | * // returns { 267 | * abc123: [ 268 | * { presence_ref: '2', user_id: 1 }, 269 | * { presence_ref: '3', user_id: 2 } 270 | * ] 271 | * } 272 | * RealtimePresence.transformState({ 273 | * abc123: { 274 | * metas: [ 275 | * { phx_ref: '2', phx_ref_prev: '1' user_id: 1 }, 276 | * { phx_ref: '3', user_id: 2 } 277 | * ] 278 | * } 279 | * }) 280 | * 281 | * @internal 282 | */ 283 | private static transformState( 284 | state: RawPresenceState | RealtimePresenceState 285 | ): RealtimePresenceState { 286 | state = this.cloneDeep(state) 287 | 288 | return Object.getOwnPropertyNames(state).reduce((newState, key) => { 289 | const presences = state[key] 290 | 291 | if ('metas' in presences) { 292 | newState[key] = presences.metas.map((presence) => { 293 | presence['presence_ref'] = presence['phx_ref'] 294 | 295 | delete presence['phx_ref'] 296 | delete presence['phx_ref_prev'] 297 | 298 | return presence 299 | }) as Presence[] 300 | } else { 301 | newState[key] = presences 302 | } 303 | 304 | return newState 305 | }, {} as RealtimePresenceState) 306 | } 307 | 308 | /** @internal */ 309 | private static cloneDeep(obj: { [key: string]: any }) { 310 | return JSON.parse(JSON.stringify(obj)) 311 | } 312 | 313 | /** @internal */ 314 | private onJoin(callback: PresenceOnJoinCallback): void { 315 | this.caller.onJoin = callback 316 | } 317 | 318 | /** @internal */ 319 | private onLeave(callback: PresenceOnLeaveCallback): void { 320 | this.caller.onLeave = callback 321 | } 322 | 323 | /** @internal */ 324 | private onSync(callback: () => void): void { 325 | this.caller.onSync = callback 326 | } 327 | 328 | /** @internal */ 329 | private inPendingSyncState(): boolean { 330 | return !this.joinRef || this.joinRef !== this.channel._joinRef() 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/realtime-js/src/index.ts: -------------------------------------------------------------------------------- 1 | import RealtimeClient, { 2 | RealtimeClientOptions, 3 | RealtimeMessage, 4 | RealtimeRemoveChannelResponse, 5 | } from './RealtimeClient' 6 | import RealtimeChannel, { 7 | RealtimeChannelOptions, 8 | RealtimeChannelSendResponse, 9 | RealtimePostgresChangesFilter, 10 | RealtimePostgresChangesPayload, 11 | RealtimePostgresInsertPayload, 12 | RealtimePostgresUpdatePayload, 13 | RealtimePostgresDeletePayload, 14 | REALTIME_LISTEN_TYPES, 15 | REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, 16 | REALTIME_SUBSCRIBE_STATES, 17 | } from './RealtimeChannel' 18 | import RealtimePresence, { 19 | RealtimePresenceState, 20 | RealtimePresenceJoinPayload, 21 | RealtimePresenceLeavePayload, 22 | REALTIME_PRESENCE_LISTEN_EVENTS, 23 | } from './RealtimePresence' 24 | 25 | export { 26 | RealtimePresence, 27 | RealtimeChannel, 28 | RealtimeChannelOptions, 29 | RealtimeChannelSendResponse, 30 | RealtimeClient, 31 | RealtimeClientOptions, 32 | RealtimeMessage, 33 | RealtimePostgresChangesFilter, 34 | RealtimePostgresChangesPayload, 35 | RealtimePostgresInsertPayload, 36 | RealtimePostgresUpdatePayload, 37 | RealtimePostgresDeletePayload, 38 | RealtimePresenceJoinPayload, 39 | RealtimePresenceLeavePayload, 40 | RealtimePresenceState, 41 | RealtimeRemoveChannelResponse, 42 | REALTIME_LISTEN_TYPES, 43 | REALTIME_POSTGRES_CHANGES_LISTEN_EVENT, 44 | REALTIME_PRESENCE_LISTEN_EVENTS, 45 | REALTIME_SUBSCRIBE_STATES, 46 | } 47 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version' 2 | 3 | export const DEFAULT_HEADERS = { 'X-Client-Info': `realtime-js/${version}` } 4 | 5 | export const VSN: string = '1.0.0' 6 | 7 | export const DEFAULT_TIMEOUT = 10000 8 | 9 | export const WS_CLOSE_NORMAL = 1000 10 | 11 | export enum SOCKET_STATES { 12 | connecting = 0, 13 | open = 1, 14 | closing = 2, 15 | closed = 3, 16 | } 17 | 18 | export enum CHANNEL_STATES { 19 | closed = 'closed', 20 | errored = 'errored', 21 | joined = 'joined', 22 | joining = 'joining', 23 | leaving = 'leaving', 24 | } 25 | 26 | export enum CHANNEL_EVENTS { 27 | close = 'phx_close', 28 | error = 'phx_error', 29 | join = 'phx_join', 30 | reply = 'phx_reply', 31 | leave = 'phx_leave', 32 | access_token = 'access_token', 33 | } 34 | 35 | export enum TRANSPORTS { 36 | websocket = 'websocket', 37 | } 38 | 39 | export enum CONNECTION_STATE { 40 | Connecting = 'connecting', 41 | Open = 'open', 42 | Closing = 'closing', 43 | Closed = 'closed', 44 | } 45 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/push.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_TIMEOUT } from '../lib/constants' 2 | import RealtimeChannel from '../RealtimeChannel' 3 | 4 | export default class Push { 5 | sent: boolean = false 6 | timeoutTimer: number | undefined = undefined 7 | ref: string = '' 8 | receivedResp: { 9 | status: string 10 | response: { [key: string]: any } 11 | } | null = null 12 | recHooks: { 13 | status: string 14 | callback: Function 15 | }[] = [] 16 | refEvent: string | null = null 17 | rateLimited: boolean = false 18 | 19 | /** 20 | * Initializes the Push 21 | * 22 | * @param channel The Channel 23 | * @param event The event, for example `"phx_join"` 24 | * @param payload The payload, for example `{user_id: 123}` 25 | * @param timeout The push timeout in milliseconds 26 | */ 27 | constructor( 28 | public channel: RealtimeChannel, 29 | public event: string, 30 | public payload: { [key: string]: any } = {}, 31 | public timeout: number = DEFAULT_TIMEOUT 32 | ) {} 33 | 34 | resend(timeout: number) { 35 | this.timeout = timeout 36 | this._cancelRefEvent() 37 | this.ref = '' 38 | this.refEvent = null 39 | this.receivedResp = null 40 | this.sent = false 41 | this.send() 42 | } 43 | 44 | send() { 45 | if (this._hasReceived('timeout')) { 46 | return 47 | } 48 | this.startTimeout() 49 | this.sent = true 50 | const status = this.channel.socket.push({ 51 | topic: this.channel.topic, 52 | event: this.event, 53 | payload: this.payload, 54 | ref: this.ref, 55 | join_ref: this.channel._joinRef(), 56 | }) 57 | if (status === 'rate limited') { 58 | this.rateLimited = true 59 | } 60 | } 61 | 62 | updatePayload(payload: { [key: string]: any }): void { 63 | this.payload = { ...this.payload, ...payload } 64 | } 65 | 66 | receive(status: string, callback: Function) { 67 | if (this._hasReceived(status)) { 68 | callback(this.receivedResp?.response) 69 | } 70 | 71 | this.recHooks.push({ status, callback }) 72 | return this 73 | } 74 | 75 | startTimeout() { 76 | if (this.timeoutTimer) { 77 | return 78 | } 79 | this.ref = this.channel.socket._makeRef() 80 | this.refEvent = this.channel._replyEventName(this.ref) 81 | 82 | const callback = (payload: any) => { 83 | this._cancelRefEvent() 84 | this._cancelTimeout() 85 | this.receivedResp = payload 86 | this._matchReceive(payload) 87 | } 88 | 89 | this.channel._on(this.refEvent, {}, callback) 90 | 91 | this.timeoutTimer = setTimeout(() => { 92 | this.trigger('timeout', {}) 93 | }, this.timeout) 94 | } 95 | 96 | trigger(status: string, response: any) { 97 | if (this.refEvent) this.channel._trigger(this.refEvent, { status, response }) 98 | } 99 | 100 | destroy() { 101 | this._cancelRefEvent() 102 | this._cancelTimeout() 103 | } 104 | 105 | private _cancelRefEvent() { 106 | if (!this.refEvent) { 107 | return 108 | } 109 | 110 | this.channel._off(this.refEvent, {}) 111 | } 112 | 113 | private _cancelTimeout() { 114 | clearTimeout(this.timeoutTimer) 115 | this.timeoutTimer = undefined 116 | } 117 | 118 | private _matchReceive({ status, response }: { status: string; response: Function }) { 119 | this.recHooks.filter((h) => h.status === status).forEach((h) => h.callback(response)) 120 | } 121 | 122 | private _hasReceived(status: string) { 123 | return this.receivedResp && this.receivedResp.status === status 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/serializer.ts: -------------------------------------------------------------------------------- 1 | // This file draws heavily from https://github.com/phoenixframework/phoenix/commit/cf098e9cf7a44ee6479d31d911a97d3c7430c6fe 2 | // License: https://github.com/phoenixframework/phoenix/blob/master/LICENSE.md 3 | 4 | export default class Serializer { 5 | HEADER_LENGTH = 1 6 | 7 | decode(rawPayload: ArrayBuffer | string, callback: Function) { 8 | if (rawPayload.constructor === ArrayBuffer) { 9 | return callback(this._binaryDecode(rawPayload)) 10 | } 11 | 12 | if (typeof rawPayload === 'string') { 13 | return callback(JSON.parse(rawPayload)) 14 | } 15 | 16 | return callback({}) 17 | } 18 | 19 | private _binaryDecode(buffer: ArrayBuffer) { 20 | const view = new DataView(buffer) 21 | const decoder = new TextDecoder() 22 | 23 | return this._decodeBroadcast(buffer, view, decoder) 24 | } 25 | 26 | private _decodeBroadcast( 27 | buffer: ArrayBuffer, 28 | view: DataView, 29 | decoder: TextDecoder 30 | ): { 31 | ref: null 32 | topic: string 33 | event: string 34 | payload: { [key: string]: any } 35 | } { 36 | const topicSize = view.getUint8(1) 37 | const eventSize = view.getUint8(2) 38 | let offset = this.HEADER_LENGTH + 2 39 | const topic = decoder.decode(buffer.slice(offset, offset + topicSize)) 40 | offset = offset + topicSize 41 | const event = decoder.decode(buffer.slice(offset, offset + eventSize)) 42 | offset = offset + eventSize 43 | const data = JSON.parse(decoder.decode(buffer.slice(offset, buffer.byteLength))) 44 | 45 | return { ref: null, topic: topic, event: event, payload: data } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/timer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a timer that accepts a `timerCalc` function to perform calculated timeout retries, such as exponential backoff. 3 | * 4 | * @example 5 | * let reconnectTimer = new Timer(() => this.connect(), function(tries){ 6 | * return [1000, 5000, 10000][tries - 1] || 10000 7 | * }) 8 | * reconnectTimer.scheduleTimeout() // fires after 1000 9 | * reconnectTimer.scheduleTimeout() // fires after 5000 10 | * reconnectTimer.reset() 11 | * reconnectTimer.scheduleTimeout() // fires after 1000 12 | */ 13 | export default class Timer { 14 | timer: number | undefined = undefined 15 | tries: number = 0 16 | 17 | constructor(public callback: Function, public timerCalc: Function) { 18 | this.callback = callback 19 | this.timerCalc = timerCalc 20 | } 21 | 22 | reset() { 23 | this.tries = 0 24 | clearTimeout(this.timer) 25 | } 26 | 27 | // Cancels any previous scheduleTimeout and schedules callback 28 | scheduleTimeout() { 29 | clearTimeout(this.timer) 30 | 31 | this.timer = setTimeout(() => { 32 | this.tries = this.tries + 1 33 | this.callback() 34 | }, this.timerCalc(this.tries + 1)) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/transformers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Helpers to convert the change Payload into native JS types. 3 | */ 4 | 5 | // Adapted from epgsql (src/epgsql_binary.erl), this module licensed under 6 | // 3-clause BSD found here: https://raw.githubusercontent.com/epgsql/epgsql/devel/LICENSE 7 | 8 | export enum PostgresTypes { 9 | abstime = 'abstime', 10 | bool = 'bool', 11 | date = 'date', 12 | daterange = 'daterange', 13 | float4 = 'float4', 14 | float8 = 'float8', 15 | int2 = 'int2', 16 | int4 = 'int4', 17 | int4range = 'int4range', 18 | int8 = 'int8', 19 | int8range = 'int8range', 20 | json = 'json', 21 | jsonb = 'jsonb', 22 | money = 'money', 23 | numeric = 'numeric', 24 | oid = 'oid', 25 | reltime = 'reltime', 26 | text = 'text', 27 | time = 'time', 28 | timestamp = 'timestamp', 29 | timestamptz = 'timestamptz', 30 | timetz = 'timetz', 31 | tsrange = 'tsrange', 32 | tstzrange = 'tstzrange', 33 | } 34 | 35 | type Columns = { 36 | name: string // the column name. eg: "user_id" 37 | type: string // the column type. eg: "uuid" 38 | flags?: string[] // any special flags for the column. eg: ["key"] 39 | type_modifier?: number // the type modifier. eg: 4294967295 40 | }[] 41 | 42 | type BaseValue = null | string | number | boolean 43 | type RecordValue = BaseValue | BaseValue[] 44 | 45 | type Record = { 46 | [key: string]: RecordValue 47 | } 48 | 49 | /** 50 | * Takes an array of columns and an object of string values then converts each string value 51 | * to its mapped type. 52 | * 53 | * @param {{name: String, type: String}[]} columns 54 | * @param {Object} record 55 | * @param {Object} options The map of various options that can be applied to the mapper 56 | * @param {Array} options.skipTypes The array of types that should not be converted 57 | * 58 | * @example convertChangeData([{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age:'33'}, {}) 59 | * //=>{ first_name: 'Paul', age: 33 } 60 | */ 61 | export const convertChangeData = ( 62 | columns: Columns, 63 | record: Record, 64 | options: { skipTypes?: string[] } = {} 65 | ): Record => { 66 | const skipTypes = options.skipTypes ?? [] 67 | 68 | return Object.keys(record).reduce((acc, rec_key) => { 69 | acc[rec_key] = convertColumn(rec_key, columns, record, skipTypes) 70 | return acc 71 | }, {} as Record) 72 | } 73 | 74 | /** 75 | * Converts the value of an individual column. 76 | * 77 | * @param {String} columnName The column that you want to convert 78 | * @param {{name: String, type: String}[]} columns All of the columns 79 | * @param {Object} record The map of string values 80 | * @param {Array} skipTypes An array of types that should not be converted 81 | * @return {object} Useless information 82 | * 83 | * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, []) 84 | * //=> 33 85 | * @example convertColumn('age', [{name: 'first_name', type: 'text'}, {name: 'age', type: 'int4'}], {first_name: 'Paul', age: '33'}, ['int4']) 86 | * //=> "33" 87 | */ 88 | export const convertColumn = ( 89 | columnName: string, 90 | columns: Columns, 91 | record: Record, 92 | skipTypes: string[] 93 | ): RecordValue => { 94 | const column = columns.find((x) => x.name === columnName) 95 | const colType = column?.type 96 | const value = record[columnName] 97 | 98 | if (colType && !skipTypes.includes(colType)) { 99 | return convertCell(colType, value) 100 | } 101 | 102 | return noop(value) 103 | } 104 | 105 | /** 106 | * If the value of the cell is `null`, returns null. 107 | * Otherwise converts the string value to the correct type. 108 | * @param {String} type A postgres column type 109 | * @param {String} stringValue The cell value 110 | * 111 | * @example convertCell('bool', 't') 112 | * //=> true 113 | * @example convertCell('int8', '10') 114 | * //=> 10 115 | * @example convertCell('_int4', '{1,2,3,4}') 116 | * //=> [1,2,3,4] 117 | */ 118 | export const convertCell = (type: string, value: RecordValue): RecordValue => { 119 | // if data type is an array 120 | if (type.charAt(0) === '_') { 121 | const dataType = type.slice(1, type.length) 122 | return toArray(value, dataType) 123 | } 124 | 125 | // If not null, convert to correct type. 126 | switch (type) { 127 | case PostgresTypes.bool: 128 | return toBoolean(value) 129 | case PostgresTypes.float4: 130 | case PostgresTypes.float8: 131 | case PostgresTypes.int2: 132 | case PostgresTypes.int4: 133 | case PostgresTypes.int8: 134 | case PostgresTypes.numeric: 135 | case PostgresTypes.oid: 136 | return toNumber(value) 137 | case PostgresTypes.json: 138 | case PostgresTypes.jsonb: 139 | return toJson(value) 140 | case PostgresTypes.timestamp: 141 | return toTimestampString(value) // Format to be consistent with PostgREST 142 | case PostgresTypes.abstime: // To allow users to cast it based on Timezone 143 | case PostgresTypes.date: // To allow users to cast it based on Timezone 144 | case PostgresTypes.daterange: 145 | case PostgresTypes.int4range: 146 | case PostgresTypes.int8range: 147 | case PostgresTypes.money: 148 | case PostgresTypes.reltime: // To allow users to cast it based on Timezone 149 | case PostgresTypes.text: 150 | case PostgresTypes.time: // To allow users to cast it based on Timezone 151 | case PostgresTypes.timestamptz: // To allow users to cast it based on Timezone 152 | case PostgresTypes.timetz: // To allow users to cast it based on Timezone 153 | case PostgresTypes.tsrange: 154 | case PostgresTypes.tstzrange: 155 | return noop(value) 156 | default: 157 | // Return the value for remaining types 158 | return noop(value) 159 | } 160 | } 161 | 162 | const noop = (value: RecordValue): RecordValue => { 163 | return value 164 | } 165 | export const toBoolean = (value: RecordValue): RecordValue => { 166 | switch (value) { 167 | case 't': 168 | return true 169 | case 'f': 170 | return false 171 | default: 172 | return value 173 | } 174 | } 175 | export const toNumber = (value: RecordValue): RecordValue => { 176 | if (typeof value === 'string') { 177 | const parsedValue = parseFloat(value) 178 | if (!Number.isNaN(parsedValue)) { 179 | return parsedValue 180 | } 181 | } 182 | return value 183 | } 184 | export const toJson = (value: RecordValue): RecordValue => { 185 | if (typeof value === 'string') { 186 | try { 187 | return JSON.parse(value) 188 | } catch (error) { 189 | console.log(`JSON parse error: ${error}`) 190 | return value 191 | } 192 | } 193 | return value 194 | } 195 | 196 | /** 197 | * Converts a Postgres Array into a native JS array 198 | * 199 | * @example toArray('{}', 'int4') 200 | * //=> [] 201 | * @example toArray('{"[2021-01-01,2021-12-31)","(2021-01-01,2021-12-32]"}', 'daterange') 202 | * //=> ['[2021-01-01,2021-12-31)', '(2021-01-01,2021-12-32]'] 203 | * @example toArray([1,2,3,4], 'int4') 204 | * //=> [1,2,3,4] 205 | */ 206 | export const toArray = (value: RecordValue, type: string): RecordValue => { 207 | if (typeof value !== 'string') { 208 | return value 209 | } 210 | 211 | const lastIdx = value.length - 1 212 | const closeBrace = value[lastIdx] 213 | const openBrace = value[0] 214 | 215 | // Confirm value is a Postgres array by checking curly brackets 216 | if (openBrace === '{' && closeBrace === '}') { 217 | let arr 218 | const valTrim = value.slice(1, lastIdx) 219 | 220 | // TODO: find a better solution to separate Postgres array data 221 | try { 222 | arr = JSON.parse('[' + valTrim + ']') 223 | } catch (_) { 224 | // WARNING: splitting on comma does not cover all edge cases 225 | arr = valTrim ? valTrim.split(',') : [] 226 | } 227 | 228 | return arr.map((val: BaseValue) => convertCell(type, val)) 229 | } 230 | 231 | return value 232 | } 233 | 234 | /** 235 | * Fixes timestamp to be ISO-8601. Swaps the space between the date and time for a 'T' 236 | * See https://github.com/supabase/supabase/issues/18 237 | * 238 | * @example toTimestampString('2019-09-10 00:00:00') 239 | * //=> '2019-09-10T00:00:00' 240 | */ 241 | export const toTimestampString = (value: RecordValue): RecordValue => { 242 | if (typeof value === 'string') { 243 | return value.replace(' ', 'T') 244 | } 245 | 246 | return value 247 | } 248 | -------------------------------------------------------------------------------- /src/realtime-js/src/lib/version.ts: -------------------------------------------------------------------------------- 1 | export const version = '2.4.0' 2 | -------------------------------------------------------------------------------- /src/storage-js/src/StorageClient.ts: -------------------------------------------------------------------------------- 1 | import StorageFileApi from './packages/StorageFileApi' 2 | import StorageBucketApi from './packages/StorageBucketApi' 3 | import { Fetch } from './lib/fetch' 4 | 5 | export class StorageClient extends StorageBucketApi { 6 | constructor( 7 | supabaseKey: string, 8 | url: string, 9 | headers: { [key: string]: string } = {}, 10 | fetch?: Fetch 11 | ) { 12 | super(supabaseKey, url, headers, fetch) 13 | } 14 | 15 | /** 16 | * Perform file operation in a bucket. 17 | * 18 | * @param id The bucket id to operate on. 19 | */ 20 | from(id: string): StorageFileApi { 21 | return new StorageFileApi(this.supabaseKey, this.url, this.headers, id, this.fetch) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/storage-js/src/index.ts: -------------------------------------------------------------------------------- 1 | export { StorageClient as StorageClient } from './StorageClient' 2 | export * from './lib/types' 3 | export * from './lib/errors' 4 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { version } from './version' 2 | export const DEFAULT_HEADERS = { 'X-Client-Info': `storage-js/${version}` } 3 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/errors.ts: -------------------------------------------------------------------------------- 1 | export class StorageError extends Error { 2 | protected __isStorageError = true 3 | 4 | constructor(message: string) { 5 | super(message) 6 | this.name = 'StorageError' 7 | } 8 | } 9 | 10 | export function isStorageError(error: unknown): error is StorageError { 11 | return typeof error === 'object' && error !== null && '__isStorageError' in error 12 | } 13 | 14 | export class StorageApiError extends StorageError { 15 | status: number 16 | 17 | constructor(message: string, status: number) { 18 | super(message) 19 | this.name = 'StorageApiError' 20 | this.status = status 21 | } 22 | 23 | toJSON() { 24 | return { 25 | name: this.name, 26 | message: this.message, 27 | status: this.status, 28 | } 29 | } 30 | } 31 | 32 | export class StorageUnknownError extends StorageError { 33 | originalError: unknown 34 | 35 | constructor(message: string, originalError: unknown) { 36 | super(message) 37 | this.name = 'StorageUnknownError' 38 | this.originalError = originalError 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/fetch.ts: -------------------------------------------------------------------------------- 1 | import { StorageApiError, StorageUnknownError } from './errors' 2 | import { resolveResponse } from './helpers' 3 | import { FetchParameters } from './types' 4 | 5 | export type Fetch = typeof fetch 6 | 7 | export interface FetchOptions { 8 | headers?: { 9 | [key: string]: string 10 | } 11 | noResolveJson?: boolean 12 | } 13 | 14 | export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'DELETE' 15 | 16 | const _getErrorMessage = (err: any): string => 17 | err.msg || err.message || err.error_description || err.error || JSON.stringify(err) 18 | 19 | const handleError = async (error: unknown, reject: (reason?: any) => void) => { 20 | const Res = await resolveResponse() 21 | 22 | if (error instanceof Res) { 23 | error.json().then((err) => { 24 | reject(new StorageApiError(_getErrorMessage(err), error.status || 500)) 25 | }) 26 | } else { 27 | reject(new StorageUnknownError(_getErrorMessage(error), error)) 28 | } 29 | } 30 | 31 | const _getRequestParams = ( 32 | method: RequestMethodType, 33 | options?: FetchOptions, 34 | parameters?: FetchParameters, 35 | body?: object 36 | ) => { 37 | const params: { [k: string]: any } = { method, headers: options?.headers || {} } 38 | if (method === 'GET') { 39 | return params 40 | } 41 | 42 | params.headers = { 'Content-Type': 'application/json', ...options?.headers } 43 | params.body = JSON.stringify(body) 44 | return { ...params, ...parameters } 45 | } 46 | 47 | async function _handleRequest( 48 | fetcher: Fetch, 49 | method: RequestMethodType, 50 | url: string, 51 | options?: FetchOptions, 52 | parameters?: FetchParameters, 53 | body?: object 54 | ): Promise { 55 | return new Promise((resolve, reject) => { 56 | fetcher(url, _getRequestParams(method, options, parameters, body)) 57 | .then((result) => { 58 | if (!result.ok) throw result 59 | if (options?.noResolveJson) return result 60 | return result 61 | }) 62 | .then((data) => resolve(data)) 63 | .catch((error) => handleError(error, reject)) 64 | }) 65 | } 66 | 67 | export async function get( 68 | fetcher: Fetch, 69 | url: string, 70 | options?: FetchOptions, 71 | parameters?: FetchParameters 72 | ): Promise { 73 | return _handleRequest(fetcher, 'GET', url, options, parameters) 74 | } 75 | 76 | export async function post( 77 | fetcher: Fetch, 78 | url: string, 79 | body: object, 80 | options?: FetchOptions, 81 | parameters?: FetchParameters 82 | ): Promise { 83 | return _handleRequest(fetcher, 'POST', url, options, parameters, body) 84 | } 85 | 86 | export async function put( 87 | fetcher: Fetch, 88 | url: string, 89 | body: object, 90 | options?: FetchOptions, 91 | parameters?: FetchParameters 92 | ): Promise { 93 | return _handleRequest(fetcher, 'PUT', url, options, parameters, body) 94 | } 95 | 96 | export async function remove( 97 | fetcher: Fetch, 98 | url: string, 99 | body: object, 100 | options?: FetchOptions, 101 | parameters?: FetchParameters 102 | ): Promise { 103 | return _handleRequest(fetcher, 'DELETE', url, options, parameters, body) 104 | } 105 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/formData.js: -------------------------------------------------------------------------------- 1 | const mimeMap = require('./mimeMap.js') 2 | 3 | function FormData() { 4 | let fileManager = wx.getFileSystemManager() 5 | let data = {} 6 | let files = [] 7 | 8 | this.append = (name, value) => { 9 | data[name] = value 10 | return true 11 | } 12 | 13 | this.appendFile = (name, path, fileName) => { 14 | let buffer = fileManager.readFileSync(path) 15 | if (Object.prototype.toString.call(buffer).indexOf('ArrayBuffer') < 0) { 16 | return false 17 | } 18 | 19 | if (!fileName) { 20 | fileName = getFileNameFromPath(path) 21 | } 22 | 23 | files.push({ 24 | name: name, 25 | buffer: buffer, 26 | fileName: fileName, 27 | }) 28 | return true 29 | } 30 | 31 | this.getData = () => convert(data, files) 32 | } 33 | 34 | function getFileNameFromPath(path) { 35 | let idx = path.lastIndexOf('/') 36 | return path.substr(idx + 1) 37 | } 38 | 39 | function convert(data, files) { 40 | let boundaryKey = 'wxmpFormBoundary' + randString() // 数据分割符,一般是随机的字符串 41 | let boundary = '--' + boundaryKey 42 | let endBoundary = boundary + '--' 43 | 44 | let postArray = [] 45 | //拼接参数 46 | if (data && Object.prototype.toString.call(data) == '[object Object]') { 47 | for (let key in data) { 48 | postArray = postArray.concat(formDataArray(boundary, key, data[key])) 49 | } 50 | } 51 | //拼接文件 52 | if (files && Object.prototype.toString.call(files) == '[object Array]') { 53 | for (let i in files) { 54 | let file = files[i] 55 | postArray = postArray.concat(formDataArray(boundary, file.name, file.buffer, file.fileName)) 56 | } 57 | } 58 | //结尾 59 | let endBoundaryArray = [] 60 | endBoundaryArray.push(...endBoundary.toUtf8Bytes()) 61 | postArray = postArray.concat(endBoundaryArray) 62 | return { 63 | contentType: 'multipart/form-data; boundary=' + boundaryKey, 64 | buffer: new Uint8Array(postArray).buffer, 65 | } 66 | } 67 | 68 | function randString() { 69 | let res = '' 70 | for (let i = 0; i < 17; i++) { 71 | let n = parseInt(Math.random() * 62) 72 | if (n <= 9) { 73 | res += n 74 | } else if (n <= 35) { 75 | res += String.fromCharCode(n + 55) 76 | } else { 77 | res += String.fromCharCode(n + 61) 78 | } 79 | } 80 | return res 81 | } 82 | 83 | function formDataArray(boundary, name, value, fileName) { 84 | let dataString = '' 85 | let isFile = !!fileName 86 | 87 | dataString += boundary + '\r\n' 88 | dataString += 'Content-Disposition: form-data; name="' + name + '"' 89 | if (isFile) { 90 | dataString += '; filename="' + fileName + '"' + '\r\n' 91 | dataString += 'Content-Type: ' + getFileMime(fileName) + '\r\n\r\n' 92 | } else { 93 | dataString += '\r\n\r\n' 94 | dataString += value 95 | } 96 | 97 | var dataArray = [] 98 | dataArray.push(...dataString.toUtf8Bytes()) 99 | 100 | if (isFile) { 101 | let fileArray = new Uint8Array(value) 102 | dataArray = dataArray.concat(Array.prototype.slice.call(fileArray)) 103 | } 104 | dataArray.push(...'\r'.toUtf8Bytes()) 105 | dataArray.push(...'\n'.toUtf8Bytes()) 106 | 107 | return dataArray 108 | } 109 | 110 | function getFileMime(fileName) { 111 | let idx = fileName.lastIndexOf('.') 112 | let mime = mimeMap[fileName.substr(idx)] 113 | return mime ? mime : 'application/octet-stream' 114 | } 115 | 116 | String.prototype.toUtf8Bytes = function () { 117 | var str = this 118 | var bytes = [] 119 | for (var i = 0; i < str.length; i++) { 120 | bytes.push(...str.utf8CodeAt(i)) 121 | if (str.codePointAt(i) > 0xffff) { 122 | i++ 123 | } 124 | } 125 | return bytes 126 | } 127 | 128 | String.prototype.utf8CodeAt = function (i) { 129 | var str = this 130 | var out = [], 131 | p = 0 132 | var c = str.charCodeAt(i) 133 | if (c < 128) { 134 | out[p++] = c 135 | } else if (c < 2048) { 136 | out[p++] = (c >> 6) | 192 137 | out[p++] = (c & 63) | 128 138 | } else if ( 139 | (c & 0xfc00) == 0xd800 && 140 | i + 1 < str.length && 141 | (str.charCodeAt(i + 1) & 0xfc00) == 0xdc00 142 | ) { 143 | // Surrogate Pair 144 | c = 0x10000 + ((c & 0x03ff) << 10) + (str.charCodeAt(++i) & 0x03ff) 145 | out[p++] = (c >> 18) | 240 146 | out[p++] = ((c >> 12) & 63) | 128 147 | out[p++] = ((c >> 6) & 63) | 128 148 | out[p++] = (c & 63) | 128 149 | } else { 150 | out[p++] = (c >> 12) | 224 151 | out[p++] = ((c >> 6) & 63) | 128 152 | out[p++] = (c & 63) | 128 153 | } 154 | return out 155 | } 156 | 157 | module.exports = FormData 158 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/helpers.ts: -------------------------------------------------------------------------------- 1 | type Fetch = typeof fetch 2 | 3 | export const resolveFetch = (customFetch?: Fetch): Fetch => { 4 | let _fetch: Fetch 5 | if (customFetch) { 6 | _fetch = customFetch 7 | } else if (typeof fetch === 'undefined') { 8 | // _fetch = async (...args) => await (await import('cross-fetch')).fetch(...args) 9 | } else { 10 | _fetch = fetch 11 | } 12 | return (...args) => _fetch(...args) 13 | } 14 | 15 | export const resolveResponse = async () => { 16 | if (typeof Response === 'undefined') { 17 | // return (await import('cross-fetch')).Response 18 | } 19 | 20 | return Response 21 | } 22 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/index.ts: -------------------------------------------------------------------------------- 1 | export * from '../packages/StorageBucketApi' 2 | export * from '../packages/StorageFileApi' 3 | export * from './types' 4 | export * from './constants' 5 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/mimeMap.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 0.001: 'application/x-001', 3 | 0.323: 'text/h323', 4 | 0.907: 'drawing/907', 5 | '.acp': 'audio/x-mei-aac', 6 | '.aif': 'audio/aiff', 7 | '.aiff': 'audio/aiff', 8 | '.asa': 'text/asa', 9 | '.asp': 'text/asp', 10 | '.au': 'audio/basic', 11 | '.awf': 'application/vnd.adobe.workflow', 12 | '.bmp': 'application/x-bmp', 13 | '.c4t': 'application/x-c4t', 14 | '.cal': 'application/x-cals', 15 | '.cdf': 'application/x-netcdf', 16 | '.cel': 'application/x-cel', 17 | '.cg4': 'application/x-g4', 18 | '.cit': 'application/x-cit', 19 | '.cml': 'text/xml', 20 | '.cmx': 'application/x-cmx', 21 | '.crl': 'application/pkix-crl', 22 | '.csi': 'application/x-csi', 23 | '.cut': 'application/x-cut', 24 | '.dbm': 'application/x-dbm', 25 | '.dcd': 'text/xml', 26 | '.der': 'application/x-x509-ca-cert', 27 | '.dib': 'application/x-dib', 28 | '.doc': 'application/msword', 29 | '.drw': 'application/x-drw', 30 | '.dwf': 'Model/vnd.dwf', 31 | '.dwg': 'application/x-dwg', 32 | '.dxf': 'application/x-dxf', 33 | '.emf': 'application/x-emf', 34 | '.ent': 'text/xml', 35 | '.eps': 'application/x-ps', 36 | '.etd': 'application/x-ebx', 37 | '.fax': 'image/fax', 38 | '.fif': 'application/fractals', 39 | '.frm': 'application/x-frm', 40 | '.gbr': 'application/x-gbr', 41 | '.gif': 'image/gif', 42 | '.gp4': 'application/x-gp4', 43 | '.hmr': 'application/x-hmr', 44 | '.hpl': 'application/x-hpl', 45 | '.hrf': 'application/x-hrf', 46 | '.htc': 'text/x-component', 47 | '.html': 'text/html', 48 | '.htx': 'text/html', 49 | '.ico': 'image/x-icon', 50 | '.iff': 'application/x-iff', 51 | '.igs': 'application/x-igs', 52 | '.img': 'application/x-img', 53 | '.isp': 'application/x-internet-signup', 54 | '.java': 'java/*', 55 | '.jpe': 'image/jpeg', 56 | '.jpeg': 'image/jpeg', 57 | '.jpg': 'application/x-jpg', 58 | '.jsp': 'text/html', 59 | '.lar': 'application/x-laplayer-reg', 60 | '.lavs': 'audio/x-liquid-secure', 61 | '.lmsff': 'audio/x-la-lms', 62 | '.ltr': 'application/x-ltr', 63 | '.m2v': 'video/x-mpeg', 64 | '.m4e': 'video/mpeg4', 65 | '.man': 'application/x-troff-man', 66 | '.mdb': 'application/msaccess', 67 | '.mfp': 'application/x-shockwave-flash', 68 | '.mhtml': 'message/rfc822', 69 | '.mid': 'audio/mid', 70 | '.mil': 'application/x-mil', 71 | '.mnd': 'audio/x-musicnet-download', 72 | '.mocha': 'application/x-javascript', 73 | '.mp1': 'audio/mp1', 74 | '.mp2v': 'video/mpeg', 75 | '.mp4': 'video/mpeg4', 76 | '.mpd': 'application/vnd.ms-project', 77 | '.mpeg': 'video/mpg', 78 | '.mpga': 'audio/rn-mpeg', 79 | '.mps': 'video/x-mpeg', 80 | '.mpv': 'video/mpg', 81 | '.mpw': 'application/vnd.ms-project', 82 | '.mtx': 'text/xml', 83 | '.net': 'image/pnetvue', 84 | '.nws': 'message/rfc822', 85 | '.out': 'application/x-out', 86 | '.p12': 'application/x-pkcs12', 87 | '.p7c': 'application/pkcs7-mime', 88 | '.p7r': 'application/x-pkcs7-certreqresp', 89 | '.pc5': 'application/x-pc5', 90 | '.pcl': 'application/x-pcl', 91 | '.pdf': 'application/pdf', 92 | '.pdx': 'application/vnd.adobe.pdx', 93 | '.pgl': 'application/x-pgl', 94 | '.pko': 'application/vnd.ms-pki.pko', 95 | '.plg': 'text/html', 96 | '.plt': 'application/x-plt', 97 | '.png': 'application/x-png', 98 | '.ppa': 'application/vnd.ms-powerpoint', 99 | '.pps': 'application/vnd.ms-powerpoint', 100 | '.ppt': 'application/x-ppt', 101 | '.prf': 'application/pics-rules', 102 | '.prt': 'application/x-prt', 103 | '.ps': 'application/postscript', 104 | '.pwz': 'application/vnd.ms-powerpoint', 105 | '.ra': 'audio/vnd.rn-realaudio', 106 | '.ras': 'application/x-ras', 107 | '.rdf': 'text/xml', 108 | '.red': 'application/x-red', 109 | '.rjs': 'application/vnd.rn-realsystem-rjs', 110 | '.rlc': 'application/x-rlc', 111 | '.rm': 'application/vnd.rn-realmedia', 112 | '.rmi': 'audio/mid', 113 | '.rmm': 'audio/x-pn-realaudio', 114 | '.rms': 'application/vnd.rn-realmedia-secure', 115 | '.rmx': 'application/vnd.rn-realsystem-rmx', 116 | '.rp': 'image/vnd.rn-realpix', 117 | '.rsml': 'application/vnd.rn-rsml', 118 | '.rtf': 'application/msword', 119 | '.rv': 'video/vnd.rn-realvideo', 120 | '.sat': 'application/x-sat', 121 | '.sdw': 'application/x-sdw', 122 | '.slb': 'application/x-slb', 123 | '.slk': 'drawing/x-slk', 124 | '.smil': 'application/smil', 125 | '.snd': 'audio/basic', 126 | '.sor': 'text/plain', 127 | '.spl': 'application/futuresplash', 128 | '.ssm': 'application/streamingmedia', 129 | '.stl': 'application/vnd.ms-pki.stl', 130 | '.sty': 'application/x-sty', 131 | '.swf': 'application/x-shockwave-flash', 132 | '.tg4': 'application/x-tg4', 133 | '.tif': 'image/tiff', 134 | '.tiff': 'image/tiff', 135 | '.top': 'drawing/x-top', 136 | '.tsd': 'text/xml', 137 | '.uin': 'application/x-icq', 138 | '.vcf': 'text/x-vcard', 139 | '.vdx': 'application/vnd.visio', 140 | '.vpg': 'application/x-vpeg005', 141 | '.vsd': 'application/x-vsd', 142 | '.vst': 'application/vnd.visio', 143 | '.vsw': 'application/vnd.visio', 144 | '.vtx': 'application/vnd.visio', 145 | '.wav': 'audio/wav', 146 | '.wb1': 'application/x-wb1', 147 | '.wb3': 'application/x-wb3', 148 | '.wiz': 'application/msword', 149 | '.wk4': 'application/x-wk4', 150 | '.wks': 'application/x-wks', 151 | '.wma': 'audio/x-ms-wma', 152 | '.wmf': 'application/x-wmf', 153 | '.wmv': 'video/x-ms-wmv', 154 | '.wmz': 'application/x-ms-wmz', 155 | '.wpd': 'application/x-wpd', 156 | '.wpl': 'application/vnd.ms-wpl', 157 | '.wr1': 'application/x-wr1', 158 | '.wrk': 'application/x-wrk', 159 | '.ws2': 'application/x-ws', 160 | '.wsdl': 'text/xml', 161 | '.xdp': 'application/vnd.adobe.xdp', 162 | '.xfd': 'application/vnd.adobe.xfd', 163 | '.xhtml': 'text/html', 164 | '.xls': 'application/x-xls', 165 | '.xml': 'text/xml', 166 | '.xq': 'text/xml', 167 | '.xquery': 'text/xml', 168 | '.xsl': 'text/xml', 169 | '.xwd': 'application/x-xwd', 170 | '.sis': 'application/vnd.symbian.install', 171 | '.x_t': 'application/x-x_t', 172 | '.apk': 'application/vnd.android.package-archive', 173 | 0.301: 'application/x-301', 174 | 0.906: 'application/x-906', 175 | '.a11': 'application/x-a11', 176 | '.ai': 'application/postscript', 177 | '.aifc': 'audio/aiff', 178 | '.anv': 'application/x-anv', 179 | '.asf': 'video/x-ms-asf', 180 | '.asx': 'video/x-ms-asf', 181 | '.avi': 'video/avi', 182 | '.biz': 'text/xml', 183 | '.bot': 'application/x-bot', 184 | '.c90': 'application/x-c90', 185 | '.cat': 'application/vnd.ms-pki.seccat', 186 | '.cdr': 'application/x-cdr', 187 | '.cer': 'application/x-x509-ca-cert', 188 | '.cgm': 'application/x-cgm', 189 | '.class': 'java/*', 190 | '.cmp': 'application/x-cmp', 191 | '.cot': 'application/x-cot', 192 | '.crt': 'application/x-x509-ca-cert', 193 | '.css': 'text/css', 194 | '.dbf': 'application/x-dbf', 195 | '.dbx': 'application/x-dbx', 196 | '.dcx': 'application/x-dcx', 197 | '.dgn': 'application/x-dgn', 198 | '.dll': 'application/x-msdownload', 199 | '.dot': 'application/msword', 200 | '.dtd': 'text/xml', 201 | '.dwf': 'application/x-dwf', 202 | '.dxb': 'application/x-dxb', 203 | '.edn': 'application/vnd.adobe.edn', 204 | '.eml': 'message/rfc822', 205 | '.epi': 'application/x-epi', 206 | '.eps': 'application/postscript', 207 | '.exe': 'application/x-msdownload', 208 | '.fdf': 'application/vnd.fdf', 209 | '.fo': 'text/xml', 210 | '.g4': 'application/x-g4', 211 | '.tif': 'image/tiff', 212 | '.gl2': 'application/x-gl2', 213 | '.hgl': 'application/x-hgl', 214 | '.hpg': 'application/x-hpgl', 215 | '.hqx': 'application/mac-binhex40', 216 | '.hta': 'application/hta', 217 | '.htm': 'text/html', 218 | '.htt': 'text/webviewhtml', 219 | '.icb': 'application/x-icb', 220 | '.ico': 'application/x-ico', 221 | '.ig4': 'application/x-g4', 222 | '.iii': 'application/x-iphone', 223 | '.ins': 'application/x-internet-signup', 224 | '.IVF': 'video/x-ivf', 225 | '.jfif': 'image/jpeg', 226 | '.jpe': 'application/x-jpe', 227 | '.jpg': 'image/jpeg', 228 | '.js': 'application/x-javascript', 229 | '.la1': 'audio/x-liquid-file', 230 | '.latex': 'application/x-latex', 231 | '.lbm': 'application/x-lbm', 232 | '.ls': 'application/x-javascript', 233 | '.m1v': 'video/x-mpeg', 234 | '.m3u': 'audio/mpegurl', 235 | '.mac': 'application/x-mac', 236 | '.math': 'text/xml', 237 | '.mdb': 'application/x-mdb', 238 | '.mht': 'message/rfc822', 239 | '.mi': 'application/x-mi', 240 | '.midi': 'audio/mid', 241 | '.mml': 'text/xml', 242 | '.mns': 'audio/x-musicnet-stream', 243 | '.movie': 'video/x-sgi-movie', 244 | '.mp2': 'audio/mp2', 245 | '.mp3': 'audio/mp3', 246 | '.mpa': 'video/x-mpg', 247 | '.mpe': 'video/x-mpeg', 248 | '.mpg': 'video/mpg', 249 | '.mpp': 'application/vnd.ms-project', 250 | '.mpt': 'application/vnd.ms-project', 251 | '.mpv2': 'video/mpeg', 252 | '.mpx': 'application/vnd.ms-project', 253 | '.mxp': 'application/x-mmxp', 254 | '.nrf': 'application/x-nrf', 255 | '.odc': 'text/x-ms-odc', 256 | '.p10': 'application/pkcs10', 257 | '.p7b': 'application/x-pkcs7-certificates', 258 | '.p7m': 'application/pkcs7-mime', 259 | '.p7s': 'application/pkcs7-signature', 260 | '.pci': 'application/x-pci', 261 | '.pcx': 'application/x-pcx', 262 | '.pdf': 'application/pdf', 263 | '.pfx': 'application/x-pkcs12', 264 | '.pic': 'application/x-pic', 265 | '.pl': 'application/x-perl', 266 | '.pls': 'audio/scpls', 267 | '.png': 'image/png', 268 | '.pot': 'application/vnd.ms-powerpoint', 269 | '.ppm': 'application/x-ppm', 270 | '.ppt': 'application/vnd.ms-powerpoint', 271 | '.pr': 'application/x-pr', 272 | '.prn': 'application/x-prn', 273 | '.ps': 'application/x-ps', 274 | '.ptn': 'application/x-ptn', 275 | '.r3t': 'text/vnd.rn-realtext3d', 276 | '.ram': 'audio/x-pn-realaudio', 277 | '.rat': 'application/rat-file', 278 | '.rec': 'application/vnd.rn-recording', 279 | '.rgb': 'application/x-rgb', 280 | '.rjt': 'application/vnd.rn-realsystem-rjt', 281 | '.rle': 'application/x-rle', 282 | '.rmf': 'application/vnd.adobe.rmf', 283 | '.rmj': 'application/vnd.rn-realsystem-rmj', 284 | '.rmp': 'application/vnd.rn-rn_music_package', 285 | '.rmvb': 'application/vnd.rn-realmedia-vbr', 286 | '.rnx': 'application/vnd.rn-realplayer', 287 | '.rpm': 'audio/x-pn-realaudio-plugin', 288 | '.rt': 'text/vnd.rn-realtext', 289 | '.rtf': 'application/x-rtf', 290 | '.sam': 'application/x-sam', 291 | '.sdp': 'application/sdp', 292 | '.sit': 'application/x-stuffit', 293 | '.sld': 'application/x-sld', 294 | '.smi': 'application/smil', 295 | '.smk': 'application/x-smk', 296 | '.sol': 'text/plain', 297 | '.spc': 'application/x-pkcs7-certificates', 298 | '.spp': 'text/xml', 299 | '.sst': 'application/vnd.ms-pki.certstore', 300 | '.stm': 'text/html', 301 | '.svg': 'text/xml', 302 | '.tdf': 'application/x-tdf', 303 | '.tga': 'application/x-tga', 304 | '.tif': 'application/x-tif', 305 | '.tld': 'text/xml', 306 | '.torrent': 'application/x-bittorrent', 307 | '.txt': 'text/plain', 308 | '.uls': 'text/iuls', 309 | '.vda': 'application/x-vda', 310 | '.vml': 'text/xml', 311 | '.vsd': 'application/vnd.visio', 312 | '.vss': 'application/vnd.visio', 313 | '.vst': 'application/x-vst', 314 | '.vsx': 'application/vnd.visio', 315 | '.vxml': 'text/xml', 316 | '.wax': 'audio/x-ms-wax', 317 | '.wb2': 'application/x-wb2', 318 | '.wbmp': 'image/vnd.wap.wbmp', 319 | '.wk3': 'application/x-wk3', 320 | '.wkq': 'application/x-wkq', 321 | '.wm': 'video/x-ms-wm', 322 | '.wmd': 'application/x-ms-wmd', 323 | '.wml': 'text/vnd.wap.wml', 324 | '.wmx': 'video/x-ms-wmx', 325 | '.wp6': 'application/x-wp6', 326 | '.wpg': 'application/x-wpg', 327 | '.wq1': 'application/x-wq1', 328 | '.wri': 'application/x-wri', 329 | '.ws': 'application/x-ws', 330 | '.wsc': 'text/scriptlet', 331 | '.wvx': 'video/x-ms-wvx', 332 | '.xdr': 'text/xml', 333 | '.xfdf': 'application/vnd.adobe.xfdf', 334 | '.xls': 'application/vnd.ms-excel', 335 | '.xlw': 'application/x-xlw', 336 | '.xpl': 'audio/scpls', 337 | '.xql': 'text/xml', 338 | '.xsd': 'text/xml', 339 | '.xslt': 'text/xml', 340 | '.x_b': 'application/x-x_b', 341 | '.sisx': 'application/vnd.symbian.install', 342 | '.ipa': 'application/vnd.iphone', 343 | '.xap': 'application/x-silverlight-app', 344 | '.zip': 'application/x-zip-compressed', 345 | } 346 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/types.ts: -------------------------------------------------------------------------------- 1 | export interface Bucket { 2 | id: string 3 | name: string 4 | owner: string 5 | created_at: string 6 | updated_at: string 7 | public: boolean 8 | } 9 | 10 | export interface FileObject { 11 | name: string 12 | bucket_id: string 13 | owner: string 14 | id: string 15 | updated_at: string 16 | created_at: string 17 | last_accessed_at: string 18 | metadata: Record 19 | buckets: Bucket 20 | } 21 | 22 | export interface SortBy { 23 | column?: string 24 | order?: string 25 | } 26 | 27 | export interface FileOptions { 28 | /** 29 | * The number of seconds the asset is cached in the browser and in the Supabase CDN. This is set in the `Cache-Control: max-age=` header. Defaults to 3600 seconds. 30 | */ 31 | cacheControl?: string 32 | /** 33 | * the `Content-Type` header value. Should be specified if using a `fileBody` that is neither `Blob` nor `File` nor `FormData`, otherwise will default to `text/plain;charset=UTF-8`. 34 | */ 35 | contentType?: string 36 | /** 37 | * When upsert is set to true, the file is overwritten if it exists. When set to false, an error is thrown if the object already exists. Defaults to false. 38 | */ 39 | upsert?: boolean 40 | } 41 | 42 | export interface SearchOptions { 43 | /** 44 | * The number of files you want to be returned. 45 | */ 46 | limit?: number 47 | 48 | /** 49 | * The starting position. 50 | */ 51 | offset?: number 52 | 53 | /** 54 | * The column to sort by. Can be any column inside a FileObject. 55 | */ 56 | sortBy?: SortBy 57 | 58 | /** 59 | * The search string to filter files by. 60 | */ 61 | search?: string 62 | } 63 | 64 | export interface FetchParameters { 65 | /** 66 | * Pass in an AbortController's signal to cancel the request. 67 | */ 68 | signal?: AbortSignal 69 | } 70 | 71 | // TODO: need to check for metadata props. The api swagger doesnt have. 72 | export interface Metadata { 73 | name: string 74 | } 75 | 76 | export interface TransformOptions { 77 | /** 78 | * The width of the image in pixels. 79 | */ 80 | width?: number 81 | /** 82 | * The height of the image in pixels. 83 | */ 84 | height?: number 85 | /** 86 | * The resize mode can be cover, contain or fill. Defaults to cover. 87 | * Cover resizes the image to maintain it's aspect ratio while filling the entire width and height. 88 | * Contain resizes the image to maintain it's aspect ratio while fitting the entire image within the width and height. 89 | * Fill resizes the image to fill the entire width and height. If the object's aspect ratio does not match the width and height, the image will be stretched to fit. 90 | */ 91 | resize?: 'cover' | 'contain' | 'fill' 92 | } 93 | -------------------------------------------------------------------------------- /src/storage-js/src/lib/version.ts: -------------------------------------------------------------------------------- 1 | // generated by genversion 2 | export const version = '2.1.0' 3 | -------------------------------------------------------------------------------- /src/storage-js/src/packages/StorageBucketApi.ts: -------------------------------------------------------------------------------- 1 | import { DEFAULT_HEADERS } from '../lib/constants' 2 | import { isStorageError, StorageError } from '../lib/errors' 3 | import { Fetch, get, post, put, remove } from '../lib/fetch' 4 | import { resolveFetch } from '../lib/helpers' 5 | import { Bucket } from '../lib/types' 6 | 7 | export default class StorageBucketApi { 8 | protected supabaseKey: string 9 | protected url: string 10 | protected headers: { [key: string]: string } 11 | protected fetch: Fetch 12 | 13 | constructor( 14 | supabaseKey: string, 15 | url: string, 16 | headers: { [key: string]: string } = {}, 17 | fetch?: Fetch 18 | ) { 19 | this.supabaseKey = supabaseKey 20 | this.url = url 21 | this.headers = { ...DEFAULT_HEADERS, ...headers } 22 | this.fetch = resolveFetch(fetch) 23 | } 24 | 25 | /** 26 | * Retrieves the details of all Storage buckets within an existing project. 27 | */ 28 | async listBuckets(): Promise< 29 | | { 30 | data: Bucket[] 31 | error: null 32 | } 33 | | { 34 | data: null 35 | error: StorageError 36 | } 37 | > { 38 | try { 39 | const data = await get(this.fetch, `${this.url}/bucket`, { headers: this.headers }) 40 | return { data, error: null } 41 | } catch (error) { 42 | if (isStorageError(error)) { 43 | return { data: null, error } 44 | } 45 | 46 | throw error 47 | } 48 | } 49 | 50 | /** 51 | * Retrieves the details of an existing Storage bucket. 52 | * 53 | * @param id The unique identifier of the bucket you would like to retrieve. 54 | */ 55 | async getBucket(id: string): Promise< 56 | | { 57 | data: Bucket 58 | error: null 59 | } 60 | | { 61 | data: null 62 | error: StorageError 63 | } 64 | > { 65 | try { 66 | const data = await get(this.fetch, `${this.url}/bucket/${id}`, { headers: this.headers }) 67 | return { data, error: null } 68 | } catch (error) { 69 | if (isStorageError(error)) { 70 | return { data: null, error } 71 | } 72 | 73 | throw error 74 | } 75 | } 76 | 77 | /** 78 | * Creates a new Storage bucket 79 | * 80 | * @param id A unique identifier for the bucket you are creating. 81 | * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. By default, buckets are private. 82 | * @returns newly created bucket id 83 | */ 84 | async createBucket( 85 | id: string, 86 | options: { public: boolean } = { public: false } 87 | ): Promise< 88 | | { 89 | data: Pick 90 | error: null 91 | } 92 | | { 93 | data: null 94 | error: StorageError 95 | } 96 | > { 97 | try { 98 | const data = await post( 99 | this.fetch, 100 | `${this.url}/bucket`, 101 | { id, name: id, public: options.public }, 102 | { headers: this.headers } 103 | ) 104 | return { data, error: null } 105 | } catch (error) { 106 | if (isStorageError(error)) { 107 | return { data: null, error } 108 | } 109 | 110 | throw error 111 | } 112 | } 113 | 114 | /** 115 | * Updates a Storage bucket 116 | * 117 | * @param id A unique identifier for the bucket you are updating. 118 | * @param options.public The visibility of the bucket. Public buckets don't require an authorization token to download objects, but still require a valid token for all other operations. 119 | */ 120 | async updateBucket( 121 | id: string, 122 | options: { public: boolean } 123 | ): Promise< 124 | | { 125 | data: { message: string } 126 | error: null 127 | } 128 | | { 129 | data: null 130 | error: StorageError 131 | } 132 | > { 133 | try { 134 | const data = await put( 135 | this.fetch, 136 | `${this.url}/bucket/${id}`, 137 | { id, name: id, public: options.public }, 138 | { headers: this.headers } 139 | ) 140 | return { data, error: null } 141 | } catch (error) { 142 | if (isStorageError(error)) { 143 | return { data: null, error } 144 | } 145 | 146 | throw error 147 | } 148 | } 149 | 150 | /** 151 | * Removes all objects inside a single bucket. 152 | * 153 | * @param id The unique identifier of the bucket you would like to empty. 154 | */ 155 | async emptyBucket(id: string): Promise< 156 | | { 157 | data: { message: string } 158 | error: null 159 | } 160 | | { 161 | data: null 162 | error: StorageError 163 | } 164 | > { 165 | try { 166 | const data = await post( 167 | this.fetch, 168 | `${this.url}/bucket/${id}/empty`, 169 | {}, 170 | { headers: this.headers } 171 | ) 172 | return { data, error: null } 173 | } catch (error) { 174 | if (isStorageError(error)) { 175 | return { data: null, error } 176 | } 177 | 178 | throw error 179 | } 180 | } 181 | 182 | /** 183 | * Deletes an existing bucket. A bucket can't be deleted with existing objects inside it. 184 | * You must first `empty()` the bucket. 185 | * 186 | * @param id The unique identifier of the bucket you would like to delete. 187 | */ 188 | async deleteBucket(id: string): Promise< 189 | | { 190 | data: { message: string } 191 | error: null 192 | } 193 | | { 194 | data: null 195 | error: StorageError 196 | } 197 | > { 198 | try { 199 | const data = await remove( 200 | this.fetch, 201 | `${this.url}/bucket/${id}`, 202 | {}, 203 | { headers: this.headers } 204 | ) 205 | return { data, error: null } 206 | } catch (error) { 207 | if (isStorageError(error)) { 208 | return { data: null, error } 209 | } 210 | 211 | throw error 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/browser.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemFire-Cloud/supabase-wechat-stable-v2/76b190d8e1ce20a84459475c4969ce05d7210990/src/wechaturl-parse/lib/bootstrap/browser.js -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Alphabet chars. 3 | CHAR_UPPERCASE_A: 65 /* A */, 4 | CHAR_LOWERCASE_A: 97 /* a */, 5 | CHAR_UPPERCASE_Z: 90 /* Z */, 6 | CHAR_LOWERCASE_Z: 122 /* z */, 7 | CHAR_UPPERCASE_C: 67 /* C */, 8 | CHAR_LOWERCASE_B: 98 /* b */, 9 | CHAR_LOWERCASE_E: 101 /* e */, 10 | CHAR_LOWERCASE_N: 110 /* n */, 11 | 12 | // Non-alphabetic chars. 13 | CHAR_DOT: 46 /* . */, 14 | CHAR_FORWARD_SLASH: 47 /* / */, 15 | CHAR_BACKWARD_SLASH: 92 /* \ */, 16 | CHAR_VERTICAL_LINE: 124 /* | */, 17 | CHAR_COLON: 58 /* : */, 18 | CHAR_QUESTION_MARK: 63 /* ? */, 19 | CHAR_UNDERSCORE: 95 /* _ */, 20 | CHAR_LINE_FEED: 10 /* \n */, 21 | CHAR_CARRIAGE_RETURN: 13 /* \r */, 22 | CHAR_TAB: 9 /* \t */, 23 | CHAR_FORM_FEED: 12 /* \f */, 24 | CHAR_EXCLAMATION_MARK: 33 /* ! */, 25 | CHAR_HASH: 35 /* # */, 26 | CHAR_SPACE: 32 /* */, 27 | CHAR_NO_BREAK_SPACE: 160 /* \u00A0 */, 28 | CHAR_ZERO_WIDTH_NOBREAK_SPACE: 65279 /* \uFEFF */, 29 | CHAR_LEFT_SQUARE_BRACKET: 91 /* [ */, 30 | CHAR_RIGHT_SQUARE_BRACKET: 93 /* ] */, 31 | CHAR_LEFT_ANGLE_BRACKET: 60 /* < */, 32 | CHAR_RIGHT_ANGLE_BRACKET: 62 /* > */, 33 | CHAR_LEFT_CURLY_BRACKET: 123 /* { */, 34 | CHAR_RIGHT_CURLY_BRACKET: 125 /* } */, 35 | CHAR_HYPHEN_MINUS: 45 /* - */, 36 | CHAR_PLUS: 43 /* + */, 37 | CHAR_DOUBLE_QUOTE: 34 /* " */, 38 | CHAR_SINGLE_QUOTE: 39 /* ' */, 39 | CHAR_PERCENT: 37 /* % */, 40 | CHAR_SEMICOLON: 59 /* ; */, 41 | CHAR_CIRCUMFLEX_ACCENT: 94 /* ^ */, 42 | CHAR_GRAVE_ACCENT: 96 /* ` */, 43 | CHAR_AT: 64 /* @ */, 44 | CHAR_AMPERSAND: 38 /* & */, 45 | CHAR_EQUAL: 61 /* = */, 46 | 47 | // Digits 48 | CHAR_0: 48 /* 0 */, 49 | CHAR_9: 57 /* 9 */, 50 | 51 | EOL: '\n', 52 | } 53 | -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/node.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MemFire-Cloud/supabase-wechat-stable-v2/76b190d8e1ce20a84459475c4969ce05d7210990/src/wechaturl-parse/lib/bootstrap/node.js -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/querystring.js: -------------------------------------------------------------------------------- 1 | const hexTable = new Array(256) 2 | for (let i = 0; i < 256; ++i) 3 | hexTable[i] = 4 | '%' + 5 | ((i < 16 ? '0' : '').toUpperCase() + 6 | // StringPrototypeToUpperCase((i < 16 ? '0' : '') + 7 | i.toString(16)) 8 | // NumberPrototypeToString(i, 16); 9 | 10 | const isHexTable = new Int8Array([ 11 | 0, 12 | 0, 13 | 0, 14 | 0, 15 | 0, 16 | 0, 17 | 0, 18 | 0, 19 | 0, 20 | 0, 21 | 0, 22 | 0, 23 | 0, 24 | 0, 25 | 0, 26 | 0, // 0 - 15 27 | 0, 28 | 0, 29 | 0, 30 | 0, 31 | 0, 32 | 0, 33 | 0, 34 | 0, 35 | 0, 36 | 0, 37 | 0, 38 | 0, 39 | 0, 40 | 0, 41 | 0, 42 | 0, // 16 - 31 43 | 0, 44 | 0, 45 | 0, 46 | 0, 47 | 0, 48 | 0, 49 | 0, 50 | 0, 51 | 0, 52 | 0, 53 | 0, 54 | 0, 55 | 0, 56 | 0, 57 | 0, 58 | 0, // 32 - 47 59 | 1, 60 | 1, 61 | 1, 62 | 1, 63 | 1, 64 | 1, 65 | 1, 66 | 1, 67 | 1, 68 | 1, 69 | 0, 70 | 0, 71 | 0, 72 | 0, 73 | 0, 74 | 0, // 48 - 63 75 | 0, 76 | 1, 77 | 1, 78 | 1, 79 | 1, 80 | 1, 81 | 1, 82 | 0, 83 | 0, 84 | 0, 85 | 0, 86 | 0, 87 | 0, 88 | 0, 89 | 0, 90 | 0, // 64 - 79 91 | 0, 92 | 0, 93 | 0, 94 | 0, 95 | 0, 96 | 0, 97 | 0, 98 | 0, 99 | 0, 100 | 0, 101 | 0, 102 | 0, 103 | 0, 104 | 0, 105 | 0, 106 | 0, // 80 - 95 107 | 0, 108 | 1, 109 | 1, 110 | 1, 111 | 1, 112 | 1, 113 | 1, 114 | 0, 115 | 0, 116 | 0, 117 | 0, 118 | 0, 119 | 0, 120 | 0, 121 | 0, 122 | 0, // 96 - 111 123 | 0, 124 | 0, 125 | 0, 126 | 0, 127 | 0, 128 | 0, 129 | 0, 130 | 0, 131 | 0, 132 | 0, 133 | 0, 134 | 0, 135 | 0, 136 | 0, 137 | 0, 138 | 0, // 112 - 127 139 | 0, 140 | 0, 141 | 0, 142 | 0, 143 | 0, 144 | 0, 145 | 0, 146 | 0, 147 | 0, 148 | 0, 149 | 0, 150 | 0, 151 | 0, 152 | 0, 153 | 0, 154 | 0, // 128 ... 155 | 0, 156 | 0, 157 | 0, 158 | 0, 159 | 0, 160 | 0, 161 | 0, 162 | 0, 163 | 0, 164 | 0, 165 | 0, 166 | 0, 167 | 0, 168 | 0, 169 | 0, 170 | 0, 171 | 0, 172 | 0, 173 | 0, 174 | 0, 175 | 0, 176 | 0, 177 | 0, 178 | 0, 179 | 0, 180 | 0, 181 | 0, 182 | 0, 183 | 0, 184 | 0, 185 | 0, 186 | 0, 187 | 0, 188 | 0, 189 | 0, 190 | 0, 191 | 0, 192 | 0, 193 | 0, 194 | 0, 195 | 0, 196 | 0, 197 | 0, 198 | 0, 199 | 0, 200 | 0, 201 | 0, 202 | 0, 203 | 0, 204 | 0, 205 | 0, 206 | 0, 207 | 0, 208 | 0, 209 | 0, 210 | 0, 211 | 0, 212 | 0, 213 | 0, 214 | 0, 215 | 0, 216 | 0, 217 | 0, 218 | 0, 219 | 0, 220 | 0, 221 | 0, 222 | 0, 223 | 0, 224 | 0, 225 | 0, 226 | 0, 227 | 0, 228 | 0, 229 | 0, 230 | 0, 231 | 0, 232 | 0, 233 | 0, 234 | 0, 235 | 0, 236 | 0, 237 | 0, 238 | 0, 239 | 0, 240 | 0, 241 | 0, 242 | 0, 243 | 0, 244 | 0, 245 | 0, 246 | 0, 247 | 0, 248 | 0, 249 | 0, 250 | 0, 251 | 0, 252 | 0, 253 | 0, 254 | 0, 255 | 0, 256 | 0, 257 | 0, 258 | 0, 259 | 0, 260 | 0, 261 | 0, 262 | 0, 263 | 0, 264 | 0, 265 | 0, 266 | 0, // ... 256 267 | ]) 268 | 269 | module.exports = { 270 | hexTable, 271 | isHexTable, 272 | } 273 | -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/querystringify-wechat.js: -------------------------------------------------------------------------------- 1 | var has = Object.prototype.hasOwnProperty, 2 | undef 3 | 4 | /** 5 | * Decode a URI encoded string. 6 | * 7 | * @param {String} input The URI encoded string. 8 | * @returns {String|Null} The decoded string. 9 | * @api private 10 | */ 11 | function decode(input) { 12 | try { 13 | return decodeURIComponent(input.replace(/\+/g, ' ')) 14 | } catch (e) { 15 | return null 16 | } 17 | } 18 | 19 | /** 20 | * Attempts to encode a given input. 21 | * 22 | * @param {String} input The string that needs to be encoded. 23 | * @returns {String|Null} The encoded string. 24 | * @api private 25 | */ 26 | function encode(input) { 27 | try { 28 | return encodeURIComponent(input) 29 | } catch (e) { 30 | return null 31 | } 32 | } 33 | 34 | /** 35 | * Simple query string parser. 36 | * 37 | * @param {String} query The query string that needs to be parsed. 38 | * @returns {Object} 39 | * @api public 40 | */ 41 | function querystring(query) { 42 | var parser = /([^=?#&]+)=?([^&]*)/g, 43 | result = {}, 44 | part 45 | 46 | while ((part = parser.exec(query))) { 47 | var key = decode(part[1]), 48 | value = decode(part[2]) 49 | 50 | // 51 | // Prevent overriding of existing properties. This ensures that build-in 52 | // methods like `toString` or __proto__ are not overriden by malicious 53 | // querystrings. 54 | // 55 | // In the case if failed decoding, we want to omit the key/value pairs 56 | // from the result. 57 | // 58 | if (key === null || value === null || key in result) continue 59 | result[key] = value 60 | } 61 | 62 | return result 63 | } 64 | 65 | /** 66 | * Transform a query string to an object. 67 | * 68 | * @param {Object} obj Object that should be transformed. 69 | * @param {String} prefix Optional prefix. 70 | * @returns {String} 71 | * @api public 72 | */ 73 | function querystringify(obj, prefix) { 74 | prefix = prefix || '' 75 | 76 | var pairs = [], 77 | value, 78 | key 79 | 80 | // 81 | // Optionally prefix with a '?' if needed 82 | // 83 | if ('string' !== typeof prefix) prefix = '?' 84 | 85 | for (key in obj) { 86 | if (has.call(obj, key)) { 87 | value = obj[key] 88 | 89 | // 90 | // Edge cases where we actually want to encode the value to an empty 91 | // string instead of the stringified value. 92 | // 93 | if (!value && (value === null || value === undef || isNaN(value))) { 94 | value = '' 95 | } 96 | 97 | key = encode(key) 98 | value = encode(value) 99 | 100 | // 101 | // If we failed to encode the strings, we should bail out as we don't 102 | // want to add invalid strings to the query. 103 | // 104 | if (key === null || value === null) continue 105 | pairs.push(key + '=' + value) 106 | } 107 | } 108 | 109 | return pairs.length ? prefix + pairs.join('&') : '' 110 | } 111 | 112 | // 113 | // Expose the module. 114 | // 115 | exports.stringify = querystringify 116 | exports.parse = querystring 117 | -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/util.js: -------------------------------------------------------------------------------- 1 | const colorRegExp = /\u001b\[\d\d?m/g // eslint-disable-line no-control-regex 2 | 3 | function toUSVString(val) { 4 | return `${val}` 5 | } 6 | function removeColors(str) { 7 | return str.replace(colorRegExp, '') 8 | } 9 | 10 | const kEnumerableProperty = Object.create(null) 11 | kEnumerableProperty.enumerable = true 12 | Object.freeze(kEnumerableProperty) 13 | 14 | const kEmptyObject = Object.freeze(Object.create(null)) 15 | 16 | module.exports = { 17 | toUSVString, 18 | removeColors, 19 | kEmptyObject, 20 | kEnumerableProperty, 21 | } 22 | -------------------------------------------------------------------------------- /src/wechaturl-parse/lib/bootstrap/validators.js: -------------------------------------------------------------------------------- 1 | const { 2 | hideStackFrames, 3 | codes: { ERR_INVALID_ARG_TYPE }, 4 | } = require('./errors') 5 | function getOwnPropertyValueOrDefault(options, key, defaultValue) { 6 | return options == null || !options.hasOwnProperty(key) ? defaultValue : options[key] 7 | } 8 | 9 | /** 10 | * @callback validateObject 11 | * @param {*} value 12 | * @param {string} name 13 | * @param {{ 14 | * allowArray?: boolean, 15 | * allowFunction?: boolean, 16 | * nullable?: boolean 17 | * }} [options] 18 | */ 19 | 20 | /** @type {validateObject} */ 21 | const validateObject = hideStackFrames((value, name, options = null) => { 22 | const allowArray = getOwnPropertyValueOrDefault(options, 'allowArray', false) 23 | const allowFunction = getOwnPropertyValueOrDefault(options, 'allowFunction', false) 24 | const nullable = getOwnPropertyValueOrDefault(options, 'nullable', false) 25 | if ( 26 | (!nullable && value === null) || 27 | (!allowArray && Array.isArray(value)) || 28 | (typeof value !== 'object' && (!allowFunction || typeof value !== 'function')) 29 | ) { 30 | throw new ERR_INVALID_ARG_TYPE(name, 'Object', value) 31 | } 32 | }) 33 | 34 | /** 35 | * @callback validateFunction 36 | * @param {*} value 37 | * @param {string} name 38 | * @returns {asserts value is Function} 39 | */ 40 | 41 | /** @type {validateFunction} */ 42 | const validateFunction = hideStackFrames((value, name) => { 43 | if (typeof value !== 'function') throw new ERR_INVALID_ARG_TYPE(name, 'Function', value) 44 | }) 45 | 46 | module.exports = { 47 | validateFunction, 48 | 49 | validateObject, 50 | } 51 | -------------------------------------------------------------------------------- /src/wefetch.js: -------------------------------------------------------------------------------- 1 | function myfetch(url, options) { 2 | return new Promise((resolve, reject) => { 3 | wx.request({ 4 | url: url, 5 | data: options.body, 6 | method: options.method, 7 | dataType: 'json', 8 | header: 9 | Object.prototype.toString.call(options.headers) == '[object Map]' 10 | ? Object.fromEntries(options.headers.entries()) 11 | : options.headers, 12 | success: resolve, 13 | success(res) { 14 | if (res.statusCode >= 200 && res.statusCode <= 299) { 15 | res.ok = true 16 | } else { 17 | res.ok = false 18 | } 19 | res.headers = new Map(Object.entries(lowerJSONKey(res.header))) 20 | res.status = res.statusCode 21 | res.json = function () { 22 | return new Promise((resolve, reject) => { 23 | resolve(res.data) 24 | }) 25 | } 26 | delete res.header 27 | delete res.statusCode 28 | resolve(res) 29 | }, 30 | fail(err) { 31 | reject(err) 32 | }, 33 | }) 34 | }) 35 | } 36 | 37 | // header小写转换 38 | function lowerJSONKey(jsonObj) { 39 | for (var key in jsonObj) { 40 | if (/[A-Z]/.test(key)) { 41 | jsonObj[key.toLowerCase()] = jsonObj[key] 42 | delete jsonObj[key] 43 | } 44 | } 45 | return jsonObj 46 | } 47 | 48 | export default myfetch 49 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src"], 3 | "exclude": ["node_modules/**/*.ts"], 4 | "compilerOptions": { 5 | "declaration": true, 6 | "declarationMap": true, 7 | "module": "CommonJS", 8 | "outDir": "dist/main", 9 | "rootDir": "src", 10 | "sourceMap": true, 11 | "target": "ES2015", 12 | "allowJs": true, 13 | "strict": true, 14 | 15 | "esModuleInterop": true, 16 | "moduleResolution": "Node", 17 | 18 | "forceConsistentCasingInFileNames": true, 19 | "stripInternal": true, 20 | "allowSyntheticDefaultImports": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.module.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "ES2020", 5 | "outDir": "dist/module" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack') 2 | const path = require('path') 3 | 4 | module.exports = { 5 | entry: './src/index.ts', 6 | output: { 7 | path: path.resolve(__dirname, 'dist/umd'), 8 | filename: 'supabase.js', 9 | library: { 10 | type: 'umd', 11 | name: 'supabase', 12 | }, 13 | }, 14 | module: { 15 | rules: [ 16 | { 17 | test: /\.ts$/, 18 | loader: 'ts-loader', 19 | options: { 20 | transpileOnly: true, 21 | }, 22 | }, 23 | ], 24 | }, 25 | resolve: { 26 | extensions: ['.ts', '.js', '.json'], 27 | }, 28 | plugins: [ 29 | new webpack.DefinePlugin({ 30 | process: 'process/browser', 31 | }), 32 | ], 33 | } 34 | --------------------------------------------------------------------------------