Skip to content

Commit c94557f

Browse files
committedJun 18, 2021
Initial commit
1 parent 1681467 commit c94557f

7 files changed

+5072
-0
lines changed
 

‎.gitignore

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
npm-debug.log
2+
yarn-error.log
3+
lerna-debug.log
4+
node_modules
5+
lib
6+
esm
7+
es6
8+
node_modules

‎LICENSE.md

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2016-present, Francois Zaninotto, Marmelab
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 NON INFRINGEMENT. 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.

‎README.md

+61
Original file line numberDiff line numberDiff line change
@@ -1 +1,62 @@
11
# ra-supabase
2+
3+
Supabase Data Provider for [react-admin](https://github.com/marmelab/react-admin), the frontend framework for building admin applications on top of REST/GraphQL services.
4+
5+
## Installation
6+
7+
```sh
8+
npm install --save ra-supabase
9+
```
10+
11+
## Usage
12+
13+
```jsx
14+
import { createClient } from "@supabase/supabase-js";
15+
import { supabaseDataProvider } from "ra-supabase";
16+
17+
const SUPABASE_URL = "";
18+
const SUPABASE_ANON_KEY = "";
19+
20+
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY);
21+
22+
const resources = {
23+
// Define the resource by passing its fields as an array
24+
posts: ["id", "title", "created_at"],
25+
// Or as an object
26+
tags: {
27+
fields: ["id", "name", "created_at"],
28+
// This allows you to customize on which fields react-admin will do a full-text search
29+
fullTextSearchFields: ["name"],
30+
},
31+
};
32+
33+
export const dataProvider = supabaseDataProvider(supabase, resources);
34+
```
35+
36+
## Resources Configuration
37+
38+
Supabase requires a list of fields to query. This is why you have to provide the supported resources and their fields.
39+
40+
You can define the resources as a simple array of field names:
41+
42+
```jsx
43+
const resources = {
44+
posts: ["id", "title", "created_at"],
45+
};
46+
```
47+
48+
However, you might want to support `q` filters for [full text search](https://marmelab.com/react-admin/List.html#full-text-search) accross multiple fields. To ensure you don't try to apply a full text search on incompatible fields, you may want to define them:
49+
50+
```jsx
51+
const resources = {
52+
tags: {
53+
fields: ["id", "name", "created_at"],
54+
fullTextSearchFields: ["name"],
55+
},
56+
};
57+
```
58+
59+
## Roadmap
60+
61+
- [ ] Add an authProvider with configurable support for supabase authentication methods
62+
- [ ] Add support for proper fulltext search https://supabase.io/docs/reference/javascript/textsearch

‎package.json

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "ra-supabase",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"repository": "git@github.com:marmelab/ra-supabase.git",
6+
"author": "Gildas Garcia <1122076+djhi@users.noreply.github.com>",
7+
"license": "MIT",
8+
"dependencies": {
9+
"@supabase/supabase-js": "^1.15.1",
10+
"ra-core": "^3.16.2"
11+
},
12+
"devDependencies": {
13+
"eslint": "^7.7.0",
14+
"eslint-config-prettier": "^6.11.0",
15+
"eslint-config-react-app": "^5.2.1",
16+
"eslint-plugin-cypress": "^2.11.1",
17+
"eslint-plugin-flowtype": "^5.2.0",
18+
"eslint-plugin-import": "^2.22.0",
19+
"eslint-plugin-jsx-a11y": "^6.3.1",
20+
"eslint-plugin-prettier": "^3.1.4",
21+
"eslint-plugin-react": "^7.20.6",
22+
"eslint-plugin-react-hooks": "^4.1.0",
23+
"husky": "^2.3.0",
24+
"jest": "26.6.0",
25+
"prettier": "~2.1.1",
26+
"typescript": "^4.3.4"
27+
},
28+
"scripts": {
29+
"build": "",
30+
"test": "",
31+
"lint": "eslint --fix ./src"
32+
}
33+
}

‎src/dataProvider.ts

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { DataProvider } from "ra-core";
2+
import { SupabaseClient } from "@supabase/supabase-js";
3+
4+
export const supabaseDataProvider = (
5+
client: SupabaseClient,
6+
resources: ResourcesOptions
7+
): DataProvider => ({
8+
getList: async (resource, params) => {
9+
const {
10+
pagination,
11+
sort,
12+
filter: { q, ...filter },
13+
} = params;
14+
15+
const resourceOptions = resources[resource];
16+
const fields = Array.isArray(resourceOptions)
17+
? resourceOptions
18+
: resourceOptions.fields;
19+
20+
const rangeFrom = (pagination.page - 1) * pagination.perPage;
21+
const rangeTo = rangeFrom + pagination.perPage;
22+
23+
let query = client
24+
.from(resource)
25+
.select(fields.join(", "), { count: "exact" })
26+
.order(sort.field, { ascending: sort.order === "ASC" })
27+
.match(filter)
28+
.range(rangeFrom, rangeTo);
29+
30+
if (q) {
31+
const fullTextSearchFields = Array.isArray(resourceOptions)
32+
? resourceOptions
33+
: resourceOptions.fullTextSearchFields;
34+
35+
query = query.or(
36+
fullTextSearchFields.map((field) => `${field}.ilike.%${q}%`).join(",")
37+
);
38+
}
39+
40+
const { data, error, count } = await query;
41+
42+
if (error) {
43+
throw error;
44+
}
45+
return { data: data ?? [], total: count ?? 0 };
46+
},
47+
getOne: async (resource, { id }) => {
48+
const resourceOptions = resources[resource];
49+
const fields = Array.isArray(resourceOptions)
50+
? resourceOptions
51+
: resourceOptions.fields;
52+
53+
const { data, error } = await client
54+
.from(resource)
55+
.select(fields.join(", "))
56+
.match({ id })
57+
.single();
58+
59+
if (error) {
60+
throw error;
61+
}
62+
return { data };
63+
},
64+
getMany: async (resource, { ids }) => {
65+
const resourceOptions = resources[resource];
66+
const fields = Array.isArray(resourceOptions)
67+
? resourceOptions
68+
: resourceOptions.fields;
69+
70+
const { data, error } = await client
71+
.from(resource)
72+
.select(fields.join(", "))
73+
.in("id", ids);
74+
75+
if (error) {
76+
throw error;
77+
}
78+
return { data: data ?? [] };
79+
},
80+
getManyReference: async (
81+
resource,
82+
{ target, id, pagination, filter, sort }
83+
) => {
84+
return { data: [], total: 0 };
85+
},
86+
create: async (resource, { data }) => {
87+
const { data: record, error } = await client
88+
.from(resource)
89+
.insert(data)
90+
.single();
91+
92+
if (error) {
93+
throw error;
94+
}
95+
return { data: record };
96+
},
97+
update: async (resource, { id, data }) => {
98+
const { data: record, error } = await client
99+
.from(resource)
100+
.update(data)
101+
.match({ id })
102+
.single();
103+
104+
if (error) {
105+
throw error;
106+
}
107+
return { data: record };
108+
},
109+
updateMany: async (resource, { ids, data }) => {
110+
const { data: records, error } = await client
111+
.from(resource)
112+
.update(data)
113+
.in("id", ids);
114+
115+
if (error) {
116+
throw error;
117+
}
118+
return { data: records?.map((record) => record.id) };
119+
},
120+
delete: async (resource, { id }) => {
121+
const { data: record, error } = await client
122+
.from(resource)
123+
.delete()
124+
.match({ id })
125+
.single();
126+
127+
if (error) {
128+
throw error;
129+
}
130+
return { data: record };
131+
},
132+
deleteMany: async (resource, { ids }) => {
133+
const { data: records, error } = await client
134+
.from(resource)
135+
.delete()
136+
.match({ id: ids });
137+
138+
if (error) {
139+
throw error;
140+
}
141+
return { data: records?.map((record) => record.id) };
142+
},
143+
});
144+
145+
type ResourceOptionsWithFullTextSearch = {
146+
fields: string[];
147+
fullTextSearchFields: string[];
148+
};
149+
export type ResourceOptions = string[] | ResourceOptionsWithFullTextSearch;
150+
export type ResourcesOptions = Record<string, ResourceOptions>;

‎tsconfig.json

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
4+
"outDir": "lib",
5+
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
6+
"lib": [
7+
"es2017",
8+
"dom"
9+
] /* Specify library files to be included in the compilation. */,
10+
"rootDir": "src",
11+
"declaration": true,
12+
"declarationMap": true,
13+
"allowJs": false,
14+
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
15+
"noImplicitAny": false /* Raise error on expressions and declarations with an implied 'any' type. */,
16+
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
17+
"moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */,
18+
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
19+
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
20+
"skipLibCheck": true
21+
},
22+
"exclude": ["**/*.spec.ts", "**/*.spec.tsx", "**/*.spec.js"],
23+
"include": ["src"]
24+
}

‎yarn.lock

+4,775
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.