├── .gitignore
├── CNAME
├── LICENSE
├── README.md
├── docs
├── concepts.md
├── getting-started.md
├── reference.md
├── release-notes.md
├── schema.graphql
├── spec-create.md
├── spec-datasync
│ ├── spec-conflict.md
│ └── spec-datasync.md
├── spec-delete.md
├── spec-find.md
├── spec-getOne.md
├── spec-overview.md
├── spec-subscriptions.md
└── spec-update.md
├── scripts
└── ghpages.sh
└── website
├── .gitignore
├── createFiles.js
├── docusaurus.config.js
├── package.json
├── sidebars.json
├── src
├── components
│ ├── Features
│ │ ├── components
│ │ │ ├── FeatureContent.jsx
│ │ │ ├── FeatureImage.jsx
│ │ │ ├── FeatureList.jsx
│ │ │ ├── FeaturesHeader.jsx
│ │ │ ├── LineConnectors.jsx
│ │ │ ├── index.js
│ │ │ └── styled.components.js
│ │ ├── features.js
│ │ ├── index.jsx
│ │ ├── styled.components.js
│ │ └── styles.module.css
│ ├── Hero
│ │ ├── animations.js
│ │ ├── index.jsx
│ │ └── styled.components.js
│ ├── Introduction
│ │ ├── index.jsx
│ │ └── styled.components.js
│ ├── UI
│ │ ├── Container.jsx
│ │ ├── Flex.jsx
│ │ ├── Row.jsx
│ │ └── index.js
│ ├── Video
│ │ ├── index.jsx
│ │ └── styled.components.js
│ └── useWindowSize.jsx
├── css
│ └── custom.css
└── pages
│ ├── index.js
│ └── versions.js
├── static
├── .nojekyll
├── CNAME
├── css
│ └── custom.css
└── img
│ ├── aerogear.png
│ ├── browser-frame.png
│ ├── favicon.ico
│ ├── logo.png
│ ├── pixel-frame.png
│ ├── play.png
│ ├── undraw_abstract_x68e.svg
│ ├── undraw_code_review.svg
│ ├── undraw_contrast.svg
│ ├── undraw_data_extraction.svg
│ ├── undraw_design.svg
│ ├── undraw_online_connection.svg
│ ├── undraw_portfolio_update.svg
│ ├── undraw_progressive_app.svg
│ ├── undraw_redesign.svg
│ ├── undraw_usability_testing.svg
│ └── undraw_yoga.svg
├── versions.json
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | graphqlcrud.org
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Licensed under the Apache License, Version 2.0 (the "License");
190 | you may not use this file except in compliance with the License.
191 | You may obtain a copy of the License at
192 |
193 | http://www.apache.org/licenses/LICENSE-2.0
194 |
195 | Unless required by applicable law or agreed to in writing, software
196 | distributed under the License is distributed on an "AS IS" BASIS,
197 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
198 | See the License for the specific language governing permissions and
199 | limitations under the License.
200 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # GraphQLCRUD
4 |
5 | GraphQLCRUD is a GraphQL CRUD API specification for databases
6 |
7 | ## Motivation
8 |
9 | GraphQL is a flexible query language supporting many different data access patterns. In practice, simple CRUD operations turn out to be a very common pattern. Standardising this very common pattern enables the community to build tooling specific to the common CRUD style API.
10 |
11 | ## Contributing
12 |
13 | Specification is being build using https://docusaurus.io generator.
14 | Please inspect docs folder for all content
15 |
16 | ## License
17 |
18 | Apache License 2.0
19 |
20 |
--------------------------------------------------------------------------------
/docs/concepts.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: concepts
3 | title: Concepts
4 | sidebar_label: Concepts
5 | ---
6 |
7 | ## CRUD methods
8 |
9 | GraphQL CRUD defines different CRUD capabilities that represents
10 | various operations that can be executed on a set of objects:
11 |
12 | - Create: create an object
13 | - Update: update a specific object's properties
14 | - Delete: delete a specific object by its ID
15 | - Get: get a specific object by its ID
16 | - Find: find multiple objects
17 |
18 | ## Input types
19 |
20 | GraphQL CRUD defines common input type categories that can be used in various CRUD methods to define operations.
21 | For example, the Create operation will use a specific input type that does not require an object ID.
22 |
23 | ## Capabilities
24 |
25 | GraphQL CRUD defines different capabilities that developers can enable to modify
26 | what queries can be made against the service. Examples of these capabilities include:
27 |
28 | - Pagination: Ability to paginate content
29 | - Filtering: Ability to peform filtering on specific fields
30 | - Countability: Ability to count the total number of objects
31 | - Consistency: Ability to verify whether a write operation is overriding data
32 |
33 | ## Variations
34 |
35 | Apart from a reference implementation, GraphQL CRUD provides different variations
36 | of the provided queries and mutations that can be used for different needs.
37 |
38 |
--------------------------------------------------------------------------------
/docs/getting-started.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: gettingstarted
3 | title: Getting Started
4 | sidebar_label: Getting Started
5 | ---
6 |
7 | ## Introduction
8 |
9 | GraphQL CRUD is a superset of the GraphQL specification that gives developers
10 | patterns for building standard, data-driven GraphQL APIs.
11 | GraphQL CRUD is a fully [GraphQL-compliant](http://facebook.github.io/graphql/) specification that provides patterns for access and modification of data.
12 | For example, this GraphQL CRUD query retrieves a single user:
13 |
14 | ```graphql
15 | {
16 | getUser(id: 4) {
17 | name
18 | }
19 | }
20 | ```
21 |
22 | and returns the following response:
23 |
24 | ```json
25 | {
26 | "user": {
27 | "name": "Mark Zuckerberg"
28 | }
29 | }
30 | ```
31 |
32 | ## Rationale
33 |
34 | GraphQL is a flexible query language supporting many different data access patterns.
35 | For most projects, CRUD operations turn out to be a very common pattern. Defining this pattern enables the community to build tooling specific to it.
36 |
37 | ## Targets of GraphQL CRUD
38 |
39 | 1. Define a minimal subset of CRUD capabilities
40 | that every application or developer can implement.
41 | 2. Provide an overview of the data access methods that are used to fetch data
42 | 3. Avoid corner cases or specifics of individual service implementations
43 | 4. Define a lenient standard based on practices, existing APIs and providers
44 | that do not enforce specific naming of root fields, types, etc.
45 | 5. Provide a set of the reference implementations for *different programming languages*
46 | 6. Provide capabilities native to GraphQL (no preprocessors, helpers, annotations, etc.)
47 |
48 | ## Non-targets of GraphQL CRUD
49 |
50 | 1. Define every possible method to map database capabilities
51 |
52 | Over the years, we have seen issues with developers adopting very open CRUD capabilities on the client.
53 | There is no silver bullet that will give developers both flexiblity of the query capabilities on the client
54 | and underlying security and control over what data is exposed to the public.
55 | That is why we define only the most common use cases and do not provide a mapping to every capability that a database might expose.
56 |
57 | 2) Include CRUD specifics of any specific platforms
58 |
59 | GraphQL CRUD borrows patterns from existing GraphQL schemas and large GraphQL providers like AWS AppSync and Hasura;
60 | however, it is does not focus on any specific provider itself.
61 |
62 | 3) Defining best practices for writing GraphQL schemas
63 |
64 | GraphQL CRUD focuses only on providing CRUD capabilities.
65 | For general rules for GraphQL schemas see [GraphQL Rules](https://graphql-rules.com)
66 |
--------------------------------------------------------------------------------
/docs/reference.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: reference
3 | title: Reference implementations
4 | sidebar_label: Implementations
5 | ---
6 |
7 | GraphQL CRUD can be applied out of the box using https://graphback.dev
8 |
9 | ## Reference Implementations
10 |
11 | ### Node.js
12 |
13 | [Graphback](https://graphback.dev) provides the ability to generate a schema that will be fully compatible with
14 | GraphQL CRUD and also connect it directly to Postgres, MongoDB and other datasources without writing any code.
15 |
16 | ### JavaScript
17 |
18 | [Offix](https://offix.dev) provides a reference implementation for delta sync queries and implements GraphQL CRUD on the client side.
19 |
20 |
21 | ## Libraries that partialy implement GraphQL CRUD
22 |
23 | This specification was built based on numerous community implementations:
24 |
25 | - AWS AppSync
26 | - Prisma
27 | - Hasura
28 | - PostGraphile
29 | - SQLmancer
30 | - TypeORM
31 | - GraphCMS
32 | - GraphQL CLI
33 | - graphql-serve
34 | - create-graphql
35 |
36 | ## Relation to Relay
37 |
38 | Relay (https://relay.dev/) provides similar capabilites to GraphQL CRUD.
39 | However, GraphQL CRUD takes a more lenient and less verbose approach to building GraphQL queries.
40 | Both standard are incompatibile with each other.
41 |
--------------------------------------------------------------------------------
/docs/release-notes.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: release
3 | title: Releases
4 | sidebar_label: Spec Releases
5 | ---
6 |
7 |
8 | ### Early Draft 2020
9 |
10 | - Definition of the concepts and CRUD methods
11 | - Added initial versions without support for different variations
--------------------------------------------------------------------------------
/docs/schema.graphql:
--------------------------------------------------------------------------------
1 |
2 | """ @model """
3 | type Comment {
4 | id: ID!
5 | text: String
6 | description: String
7 | note: Note
8 | }
9 |
10 | input CommentFilter {
11 | id: IDInput
12 | text: StringInput
13 | description: StringInput
14 | noteId: IDInput
15 | and: [CommentFilter!]
16 | or: [CommentFilter!]
17 | not: CommentFilter
18 | }
19 |
20 | type CommentResultList {
21 | items: [Comment]!
22 | offset: Int
23 | limit: Int
24 | count: Int
25 | }
26 |
27 | input CommentSubscriptionFilter {
28 | id: ID
29 | text: String
30 | description: String
31 | }
32 |
33 | input CreateCommentInput {
34 | id: ID
35 | text: String
36 | description: String
37 | noteId: ID
38 | }
39 |
40 | input CreateNoteInput {
41 | id: ID
42 | title: String!
43 | description: String
44 | }
45 |
46 | input IDInput {
47 | ne: ID
48 | eq: ID
49 | le: ID
50 | lt: ID
51 | ge: ID
52 | gt: ID
53 | in: [ID!]
54 | contains: ID
55 | startsWith: ID
56 | endsWith: ID
57 | }
58 |
59 | input MutateCommentInput {
60 | id: ID!
61 | text: String
62 | description: String
63 | noteId: ID
64 | }
65 |
66 | input MutateNoteInput {
67 | id: ID!
68 | title: String
69 | description: String
70 | }
71 |
72 | type Mutation {
73 | createNote(input: CreateNoteInput!): Note!
74 | updateNote(input: MutateNoteInput!): Note!
75 | deleteNote(input: MutateNoteInput!): Note!
76 | createComment(input: CreateCommentInput!): Comment!
77 | updateComment(input: MutateCommentInput!): Comment!
78 | deleteComment(input: MutateCommentInput!): Comment!
79 | }
80 |
81 | """ @model """
82 | type Note {
83 | id: ID!
84 | title: String!
85 | description: String
86 |
87 | """@oneToMany field: 'note', key: 'noteId'"""
88 | comments(filter: CommentFilter): [Comment]!
89 | }
90 |
91 | input NoteFilter {
92 | id: IDInput
93 | title: StringInput
94 | description: StringInput
95 | and: [NoteFilter!]
96 | or: [NoteFilter!]
97 | not: NoteFilter
98 | }
99 |
100 | type NoteResultList {
101 | items: [Note]!
102 | offset: Int
103 | limit: Int
104 | count: Int
105 | }
106 |
107 | input NoteSubscriptionFilter {
108 | id: ID
109 | title: String
110 | description: String
111 | }
112 |
113 | input OrderByInput {
114 | field: String!
115 | order: SortDirectionEnum = ASC
116 | }
117 |
118 | input PageRequest {
119 | limit: Int
120 | offset: Int
121 | }
122 |
123 | type Query {
124 | getNote(id: ID!): Note
125 | findNotes(filter: NoteFilter, page: PageRequest, orderBy: OrderByInput): NoteResultList!
126 | getComment(id: ID!): Comment
127 | findComments(filter: CommentFilter, page: PageRequest, orderBy: OrderByInput): CommentResultList!
128 | }
129 |
130 | enum SortDirectionEnum {
131 | DESC
132 | ASC
133 | }
134 |
135 | input StringInput {
136 | ne: String
137 | eq: String
138 | le: String
139 | lt: String
140 | ge: String
141 | gt: String
142 | in: [String!]
143 | contains: String
144 | startsWith: String
145 | endsWith: String
146 | }
147 |
148 | type Subscription {
149 | newNote(filter: NoteSubscriptionFilter): Note!
150 | updatedNote(filter: NoteSubscriptionFilter): Note!
151 | deletedNote(filter: NoteSubscriptionFilter): Note!
152 | newComment(filter: CommentSubscriptionFilter): Comment!
153 | updatedComment(filter: CommentSubscriptionFilter): Comment!
154 | deletedComment(filter: CommentSubscriptionFilter): Comment!
155 | }
--------------------------------------------------------------------------------
/docs/spec-create.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: create
3 | title: Create Operation
4 | sidebar_label: Create Operation
5 | ---
6 |
7 | ## Create Operation
8 |
9 | The create operation accepts a single input type as argument.
10 |
11 | For example, given a `Note` type like:
12 |
13 | ```graphql
14 | type Note {
15 | id: ID!
16 | title: String!
17 | description: String
18 | comments: [Comment]!
19 | }
20 | ```
21 |
22 | The following mutation can be used:
23 |
24 | ```graphql
25 | type Mutation {
26 | createNote(input: CreateNoteInput!): Note
27 | }
28 | ```
29 |
30 | The input type for this create operation looks as follows:
31 |
32 | ```graphql
33 | input CreateNoteInput {
34 | ## To support client side ID creation
35 | id: ID
36 | title: String!
37 | description: String
38 | }
39 | ```
40 |
--------------------------------------------------------------------------------
/docs/spec-datasync/spec-conflict.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: conflict-resolution
3 | title: Server-Side Conflict Resolution
4 | sidebar_label: Conflict Resolution
5 | ---
6 |
7 | If a client goes offline, there is a strong possiblity that the cached data may not be consistent with the data from the source. Thus it is advisable to have some mechanism for detecting this inconsistency and some way to resolve this. For example, requiring an `updatedAt` timestamp for every mutation is a decent way to ensure this:
8 |
9 | ```graphql
10 | input MutateCommentInput {
11 | id: ID!
12 | title: String
13 | description: String
14 | updatedAt: String!
15 | }
16 | ```
17 |
18 | This could possibly detect inconsistencies when issuing mutations and possibly resolve them or inform the client about the differences, for example:
19 |
20 | ```json
21 | {
22 | "conflictInfo": {
23 | "serverState": {
24 | "id": "5eedae1367d72e2192561723",
25 | "text": "AlreadyUpdatedTitle",
26 | "_deleted": false,
27 | "createdAt": "1592634899084",
28 | "updatedAt": "1592634899084"
29 | },
30 | "clientState": {
31 | "id": "5eedae1367d72e2192561723",
32 | "text": "ClientSideUpdate",
33 | "updatedAt": "1592634898093"
34 | }
35 | }
36 | }
37 | ```
38 |
39 |
--------------------------------------------------------------------------------
/docs/spec-datasync/spec-datasync.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: delta-queries
3 | title: Delta Query Specification
4 | ---
5 |
6 | Delta Queries extend the GraphQLCRUD spec to support offline-first GraphQL clients. It outlines the features and specifications needed to for smooth offline operation.
7 |
8 | It consists of two aspects:
9 |
10 | - Fetch data that was changed based on a client side `lastChanged` token.
11 | - Ensure data consistency using the aforementioned `lastChanged` token provided.
12 |
13 | ### What are Delta Queries?
14 |
15 | These are a special kind of query that necessarily takes a `lastChanged` argument and for every type that fetches the changed data for that type since the point in time specified by the `lastChanged` argument. An example type definition for this would be:
16 |
17 | ```graphql
18 | type Comment {
19 | id: ID!
20 | text: String
21 | description: String
22 | }
23 |
24 | type Query {
25 | syncComments(lastChanged: String!, filter: CommentFilter): CommentDeltaList!
26 | }
27 | ```
28 |
29 | In the above example, the delta query is `syncComments` which returns a list of `CommentDelta` type:
30 |
31 | ```graphql
32 | type CommentDelta {
33 | id: ID!
34 | text: String
35 | description: String
36 | createdAt: String
37 | updatedAt: String
38 | _deleted: Boolean
39 | }
40 |
41 | type CommentDeltaList {
42 | items: [CommentDelta]!
43 | lastChanged: String
44 | }
45 | ```
46 |
47 | Each object of this list is a snapshot of the current state of the row/document in the database, along with the timestamps that show when it was last changed(`updatedAt`), and when it was created(`createdAt`). It also provides info on if the row was deleted(`_deleted`), in which case `updatedAt` says when it was deleted.
48 |
49 | The objects to be fetched in the delta query can also be filtered by using the `filter` argument
50 | which would work exactly like the filter in the [find](./spec-find.md) query.
51 |
--------------------------------------------------------------------------------
/docs/spec-delete.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: delete
3 | title: Delete Operation
4 | ---
5 |
6 | ## Delete Operation
7 |
8 | The delete operation accepts a single input type as an argument.
9 |
10 | For example, given a `Note` type like:
11 |
12 | ```graphql
13 | type Note {
14 | id: ID!
15 | title: String!
16 | description: String
17 | comments: [Comment]!
18 | }
19 | ```
20 |
21 | The following mutation can be used:
22 |
23 | ```graphql
24 | type Mutation {
25 | deleteNote(input: MutateNoteInput!): Note
26 | }
27 | ```
28 |
29 | The input type for this delete operation looks as follows:
30 |
31 | ```graphql
32 | input MutateNoteInput {
33 | title: String
34 | description: String
35 | }
36 | ```
37 |
--------------------------------------------------------------------------------
/docs/spec-find.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: find
3 | title: Find Operation
4 | ---
5 |
6 | ## Find Operation
7 |
8 | The find operation allows the client to fetch multiple objects from the database using filtering, sorting and pagination capabilities.
9 |
10 | ## Filtering
11 |
12 | Boolean operators supported when filtering:
13 | - AND
14 | - NOT
15 | - OR
16 |
17 | Matematical operators supported when filtering:
18 | - Not equal: '<>'
19 | - Equal: '='
20 | - Less than or equal: '<='
21 | - Less than: '<'
22 | - Greater than or equal: '>='
23 | - Greater than: '>'
24 |
25 | String opperations supported when filtering:
26 |
27 | - Contains: 'like'
28 | - Starts with: 'like'
29 | - Ends with: 'like'
30 |
31 | Capabilities not supported:
32 |
33 | - Sorting by multiple fields
34 | - Aggregation apart from counting
35 |
36 | ## Example query
37 |
38 | ```graphql
39 | type Query {
40 | findNotes(filter: NoteFilter, orderBy: OrderByInput): [NoteResultList]!
41 | }
42 | ```
43 |
44 | Input type for `filter` argument:
45 |
46 | ```graphql
47 | input NoteFilter {
48 | id: IDInput
49 | title: StringInput
50 | clickCount: IntInput
51 | floatValue: FloatInput
52 | description: StringInput
53 | and: [NoteFilter!]
54 | or: [NoteFilter!]
55 | not: NoteFilter
56 | }
57 | ```
58 |
59 | Input type for `orderBy` argument:
60 |
61 | ```graphql
62 | input OrderByInput {
63 | field: String!
64 | order: SortDirectionEnum = ASC
65 | }
66 |
67 | enum SortDirectionEnum {
68 | DESC
69 | ASC
70 | }
71 | ```
72 |
73 | To enable filtering by specific scalar fields, we can create individual input types for each scalar:
74 | For example, for the five built-in Scalars this could be:
75 |
76 | ```graphql
77 | input StringInput {
78 | ne: String
79 | eq: String
80 | le: String
81 | lt: String
82 | ge: String
83 | gt: String
84 | in: [String!]
85 | contains: String
86 | startsWith: String
87 | endsWith: String
88 | }
89 |
90 | input BooleanInput {
91 | ne: Boolean
92 | eq: Boolean
93 | }
94 |
95 | input FloatInput {
96 | ne: Float
97 | eq: Float
98 | le: Float
99 | lt: Float
100 | ge: Float
101 | gt: Float
102 | in: [Float!]
103 | }
104 |
105 | input IntInput {
106 | ne: Int
107 | eq: Int
108 | le: Int
109 | lt: Int
110 | ge: Int
111 | gt: Int
112 | in: [Int!]
113 | }
114 |
115 | input IDInput {
116 | ne: ID
117 | eq: ID
118 | in: [ID!]
119 | }
120 | ```
121 |
122 | ## Variations
123 |
124 | ### Pagination
125 |
126 | ```graphql
127 | type Query {
128 | findNotes(filter: NoteFilter, page: PageRequest, orderBy: OrderByInput): NoteResultList!
129 | }
130 |
131 | ## Special type created as wrapper for pagination
132 | type NoteResultList {
133 | items: [Note]!
134 | offset: Int
135 | limit: Int
136 | count: Int
137 | }
138 |
139 | ## Represents page request
140 | input PageRequest {
141 | limit: Int
142 | offset: Int
143 | }
144 | ```
145 |
--------------------------------------------------------------------------------
/docs/spec-getOne.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: getOne
3 | title: Get Operation
4 | sidebar_label: Get Operation
5 | ---
6 |
7 | ## Get Operation
8 |
9 | Fetching an object by ID can be enabled by specifying an `id` argument.
10 | The ID can represent any unique field that object has.
11 |
12 | For the Note type, this can be:
13 |
14 | ```graphql
15 | getNote(id: ID!): Note
16 | ```
17 |
--------------------------------------------------------------------------------
/docs/spec-overview.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: overview
3 | title: Overview
4 | sidebar_label: Overview
5 | ---
6 |
7 | ## Areas covered
8 |
9 | This specification describes all aspects of a flexible GraphQL API suitable for relational databases.
10 |
11 | ## Focus on API
12 |
13 | GraphQLCRUD is a collection of specifications for GraphQL APIs that are abstracting from any database technologies. GraphQLCRUD is concerned with the API only and abstracts away the implementation. As such two implementations of GraphQLCRUD could choose to store data in different ways, but applications interacting with the data through the GraphQLCRUD API wouldn't be able to tell the difference.
14 |
15 | ## Schema Definition Language
16 |
17 | Examples are used throughout this spec to show the final schema generated for a specific data model. In all examples, SDL (Schema Definition Language) notation is used to define the data model. The benefit of SDL is that it is database independent, so we can use the same notation accross all supported databases.
18 |
19 | ## Naming
20 |
21 | GraphQL CRUD does not specify how fields generated for each data type must be named. It is up to each GraphQL CRUD implementation to define a naming system. The reference implementation uses the naming convention as listed in the example queries.
22 |
23 | ## Base schema
24 |
25 | The specification is based on the folllowing two models.
26 | Any additional types will be directly referenced in the schema.
27 |
28 | ```graphql
29 | type Note {
30 | id: ID!
31 | title: String!
32 | description: String
33 | comments: [Comment]!
34 | }
35 |
36 | type Comment {
37 | id: ID!
38 | text: String
39 | description: String
40 | votes: Int
41 | note: Note
42 | }
43 | ```
44 |
--------------------------------------------------------------------------------
/docs/spec-subscriptions.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: subscriptions
3 | title: Subscriptions
4 | ---
5 |
6 | ## Subscriptions
7 |
8 | Subscriptions are divided to three different groups of changes: Create, Update and Delete.
9 |
10 | Subscriptions can be used with filtering to only receive events that match the provide filter.
11 |
12 | ```graphql
13 | input NoteSubscriptionFilter {
14 | id: IDInput
15 | title: StringInput
16 | description: StringInput
17 | and: [NoteFilter!]
18 | or: [NoteFilter!]
19 | not: NoteFilter
20 | }
21 |
22 | type Subscription {
23 | newNote(filter: NoteSubscriptionFilter): Note!
24 | updatedNote(filter: NoteSubscriptionFilter): Note!
25 | deletedNote(filter: NoteSubscriptionFilter): Note!
26 | }
27 | ```
--------------------------------------------------------------------------------
/docs/spec-update.md:
--------------------------------------------------------------------------------
1 | ---
2 | id: update
3 | title: Update Operation
4 | ---
5 |
6 | ## Update Operation
7 |
8 | The update operation accepts a single input type as an argument.
9 |
10 | For example, given a `Note` type like:
11 |
12 | ```graphql
13 | type Note {
14 | id: ID!
15 | title: String!
16 | description: String
17 | comments: [Comment]!
18 | }
19 | ```
20 |
21 | The following mutation can be used:
22 |
23 | ```graphql
24 | type Mutation {
25 | updateNote(input: MutateNoteInput!): Note
26 | }
27 | ```
28 |
29 | The input type for this delete operation looks as follows:
30 |
31 | ```graphql
32 | input MutateNoteInput {
33 | # ID field is required for update
34 | id: ID!
35 | title: String
36 | description: String
37 | }
38 | ```
39 |
40 | ## Variations
41 |
42 | ### Conditional updates
43 |
44 | Conditional updates can be enabled for cases where we want to perform an update
45 | operation only after meeting certain criteria.
46 |
47 | ```graphql
48 | type Mutation {
49 | updateNote(input: MutateNoteInput!, where: UpdateNoteFilter): Note!
50 | }
51 | ```
52 |
53 | Unlike the previous example, the input type for filtering has all fields marked as optional.
54 |
55 | ```graphql
56 | input UpdateNoteFilter {
57 | title: String
58 | description: String
59 | }
60 | ```
61 |
--------------------------------------------------------------------------------
/scripts/ghpages.sh:
--------------------------------------------------------------------------------
1 | git checkout gh-pages &&
2 | git reset --hard origin/master &&
3 | rm README.md &&
4 | npm run build &&
5 | git add --all &&
6 | git commit -a -m"gh-pages update" &&
7 | git push origin +gh-pages
--------------------------------------------------------------------------------
/website/.gitignore:
--------------------------------------------------------------------------------
1 | # Dependencies
2 | /node_modules
3 |
4 | # Production
5 | /build
6 |
7 | # Generated files
8 | .docusaurus
9 | .cache-loader
10 |
11 | # Misc
12 | .DS_Store
13 | .env.local
14 | .env.development.local
15 | .env.test.local
16 | .env.production.local
17 |
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 |
--------------------------------------------------------------------------------
/website/createFiles.js:
--------------------------------------------------------------------------------
1 | const sideBars = require("./sidebars.json");
2 | const fs = require("fs");
3 | const path = require("path");
4 |
5 | if (sideBars && sideBars.docs) {
6 | for (const chapter of Object.keys(sideBars.docs)) {
7 | for (const element of sideBars.docs[chapter]) {
8 | const text = `---
9 | id: ${element}
10 | title: ${element}
11 | sidebar_label: ${element}
12 | ---
13 |
14 | ${element} TODO
15 | `;
16 | const pathToFile = path.resolve("../docs/", element + ".md");
17 | if (!fs.existsSync(pathToFile)) {
18 | fs.writeFileSync(pathToFile, text);
19 | }
20 | }
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/website/docusaurus.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const versions = require("./versions.json");
3 |
4 | module.exports = {
5 | title: "GraphQLCRUD",
6 | tagline:
7 | "Specification that extends GraphQL with common data access use cases",
8 | url: "https://graphqlcrud.org",
9 | baseUrl: "/",
10 | favicon: "img/logo.png",
11 |
12 | organizationName: "GraphQLCRUD", // Usually your GitHub org/user name.
13 | projectName: "spec", // Usually your repo name.
14 |
15 | themeConfig: {
16 | disableDarkMode: true,
17 | prism: {
18 | theme: require("prism-react-renderer/themes/github"),
19 | defaultLanguage: "javascript",
20 | },
21 | navbar: {
22 | title: "GraphQLCRUD",
23 | logo: {
24 | alt: "GraphQLCRUD Logo",
25 | src: "img/logo.png",
26 | },
27 | links: [
28 | {
29 | to: "docs/next/gettingstarted",
30 | activeBasePath: "docs",
31 | label: "Docs",
32 | position: "left",
33 | // items: [
34 | // Disable temporarily until we have version of the spec
35 | // {
36 | // label: versions[0],
37 | // to: 'docs/getting-started',
38 | // },
39 | // ...versions.slice(1).map((version) => ({
40 | // label: version,
41 | // to: `docs/${version}/getting-started`,
42 | // })),
43 | // {
44 | // label: 'Master/Unreleased',
45 | // to: 'docs/next/getting-started',
46 | // },
47 | // ],
48 | },
49 | // {
50 | // to: 'versions',
51 | // label: `v${versions[0]}`,
52 | // position: 'right',
53 | // },
54 | {
55 | href: "https://github.com/GraphQLCRUD/spec",
56 | label: "GitHub",
57 | position: "right",
58 | },
59 | ],
60 | },
61 | footer: {
62 | links: [
63 | {
64 | title: "Docs",
65 | items: [
66 | {
67 | label: "Getting Started",
68 | to: "docs/next/gettingstarted",
69 | },
70 | {
71 | label: "Releases",
72 | to: "docs/release",
73 | },
74 | ],
75 | },
76 | {
77 | title: "Community",
78 | items: [
79 | {
80 | label: "GitHub",
81 | href: "https://github.com/GraphQLCRUD/spec",
82 | },
83 | {
84 | label: "Discord",
85 | href: "https://discordapp.com/invite/mJ7j84m",
86 | },
87 | ],
88 | },
89 | ],
90 | logo: {
91 | alt: "AeroGear Logo",
92 | src: "img/aerogear.png",
93 | href: "https://aerogear.org/",
94 | },
95 | copyright: `Copyright © ${new Date().getFullYear()} AeroGear`,
96 | },
97 | },
98 | presets: [
99 | [
100 | "@docusaurus/preset-classic",
101 | {
102 | docs: {
103 | path: "../docs",
104 | routeBasePath: "docs",
105 | sidebarPath: require.resolve("./sidebars.json"),
106 | editUrl:
107 | "https://github.com/aerogear/GraphQLCRUD/edit/master/website/",
108 | },
109 | theme: {
110 | customCss: require.resolve("./src/css/custom.css"),
111 | },
112 | },
113 | ],
114 | ],
115 | };
116 |
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "graphqlcrud-docs",
3 | "private": true,
4 | "scripts": {
5 | "start": "docusaurus start",
6 | "build": "docusaurus build",
7 | "swizzle": "docusaurus swizzle",
8 | "deploy": "docusaurus deploy",
9 | "docs": "docusaurus docs:version"
10 | },
11 | "dependencies": {
12 | "@docusaurus/core": "^2.0.0-alpha.50",
13 | "@docusaurus/preset-classic": "^2.0.0-alpha.50",
14 | "gsap": "^3.2.6",
15 | "react": "^16.8.4",
16 | "react-dom": "^16.8.4",
17 | "scrollmagic": "^2.0.7",
18 | "scrollscene": "^0.0.16",
19 | "styled-components": "^5.1.0"
20 | },
21 | "version": "0.3.0"
22 | }
23 |
--------------------------------------------------------------------------------
/website/sidebars.json:
--------------------------------------------------------------------------------
1 | {
2 | "docs": {
3 | "Introduction": ["gettingstarted", "concepts", "reference"],
4 | "Specification": [
5 | "overview",
6 | "create",
7 | "update",
8 | "delete",
9 | "find",
10 | "getOne",
11 | "subscriptions"
12 | ],
13 | "Extensions": [
14 | {
15 | "Delta Queries": [
16 | "spec-datasync/delta-queries",
17 | "spec-datasync/conflict-resolution"
18 | ]
19 | }
20 | ],
21 | "Releases": ["release"]
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/website/src/components/Features/components/FeatureContent.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Content, Title } from './styled.components';
3 |
4 | export const FeatureContent = ({ title, description }) => (
5 |
6 | {title}
7 | {description}
8 |
9 | );
--------------------------------------------------------------------------------
/website/src/components/Features/components/FeatureImage.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Circle, Image } from './styled.components';
4 |
5 | export const FeatureImage = React.forwardRef(({ index, imageUrl }, ref) => {
6 | return (
7 |
8 |
9 |
10 | );
11 | });
--------------------------------------------------------------------------------
/website/src/components/Features/components/FeatureList.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { gsap } from 'gsap';
3 | import { ScrollScene } from 'scrollscene';
4 |
5 | import { FeatureImage } from './FeatureImage';
6 | import { FeatureContent } from './FeatureContent';
7 | import { Row } from '../../UI';
8 | import { FeatureColumn } from './styled.components';
9 |
10 | function useFeatureAnimation({ left, right, trigger }) {
11 | React.useEffect(() => {
12 | const timeline = gsap.timeline({ paused: true });
13 |
14 | timeline.to(left.current, {
15 | opacity: 1
16 | }).to(right.current, {
17 | opacity: 1
18 | });
19 |
20 | new ScrollScene({
21 | triggerElement: trigger.current,
22 | triggerHook: 0.5,
23 | offset: 100,
24 | duration: 300,
25 | gsap: {
26 | timeline,
27 | },
28 | });
29 | });
30 | }
31 |
32 | const Feature = React.forwardRef((props, ref) => {
33 | const { index } = props;
34 |
35 | const left = React.useRef();
36 | const right = React.useRef();
37 | const trigger = React.useRef();
38 |
39 | useFeatureAnimation({ left, right, trigger });
40 |
41 | return (
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | );
51 | });
52 |
53 | export function FeatureList({ features, refs, lineRefs}) {
54 | return features && features.length > 0 && (
55 | features.map((props, index) => {
56 | return (
57 |
64 | );
65 | })
66 | )
67 | }
--------------------------------------------------------------------------------
/website/src/components/Features/components/FeaturesHeader.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Flex } from '../../UI/Flex';
3 | import { Content, Title, HR } from './styled.components';
4 |
5 | export function FeaturesHeader() {
6 | return (
7 |
8 |
9 |
10 | Features
11 |
12 | GraphQL is a flexible query language supporting many different data access patterns.
13 | In practice, simple CRUD operations turn out to be a very common pattern. Standardising this very common pattern enables the community to build tooling specific to the common CRUD style API.
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/website/src/components/Features/components/LineConnectors.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { gsap } from 'gsap';
3 | import { ScrollScene } from 'scrollscene';
4 | import { SVG } from './styled.components';
5 | import { useWindowSize } from '../../useWindowSize';
6 |
7 | function useLineAnimation({ lineRefs }) {
8 | React.useEffect(() => {
9 | const trigger = document.getElementById('features');
10 | const timeline = gsap.timeline({ paused: true, duration: 300 });
11 |
12 | lineRefs.forEach((ref, index) => {
13 | if (ref && ref.current) {
14 | timeline.from(ref.current, {
15 | opacity: 0,
16 | delay: index * 100,
17 | duration: 250
18 | });
19 | }
20 | });
21 |
22 | new ScrollScene({
23 | triggerElement: trigger,
24 | triggerHook: 0.2,
25 | offset: 100,
26 | duration: 250,
27 | gsap: {
28 | timeline,
29 | },
30 | });
31 | })
32 | }
33 |
34 | const Line = React.forwardRef(({ p1, p2 }, ref) => {
35 | const x1 = p1.offsetLeft + (p1.offsetWidth/2);
36 | const x2 = p2.offsetLeft + (p2.offsetWidth/2);
37 | const y1 = p1.offsetTop - (p1.offsetHeight/2);
38 | const y2 = p2.offsetTop - (p2.offsetHeight/2);
39 | return
40 | });
41 |
42 | export function LineConnectors({ refs, lineRefs }) {
43 |
44 | const [allRefs, setAllRefs] = React.useState([]);
45 | useLineAnimation({ lineRefs });
46 | useWindowSize();
47 |
48 | React.useEffect(() => {
49 | setAllRefs(refs);
50 | });
51 |
52 | return (
53 |
54 | {
55 | allRefs && allRefs.length && (
56 | allRefs.map(({ current }, index) => {
57 | const next = allRefs[index+1];
58 | if (!current || next === undefined) return null;
59 | return ;
60 | })
61 | )
62 | }
63 |
64 | );
65 | };
--------------------------------------------------------------------------------
/website/src/components/Features/components/index.js:
--------------------------------------------------------------------------------
1 | export { FeatureContent } from './FeatureContent';
2 | export { FeaturesHeader } from './FeaturesHeader';
3 | export { FeatureList } from './FeatureList';
4 | export { FeatureImage } from './FeatureImage';
5 | export { LineConnectors } from './LineConnectors';
6 |
7 |
--------------------------------------------------------------------------------
/website/src/components/Features/components/styled.components.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Title = styled.h3`
4 | font-size: 2rem;
5 | font-weight: 600;
6 | `;
7 |
8 | export const Content = styled.div`
9 | text-align: center;
10 | padding: 6em 0 1em;
11 | `;
12 |
13 | export const HR = styled.hr`
14 | width: 60%;
15 | margin: 4em auto;
16 | `;
17 |
18 | export const Circle = styled.div`
19 | text-align: center;
20 | width: 250px;
21 | height: 250px;
22 | border-radius: 50%;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | background: #f2f2f2;
27 | position: relative;
28 | `;
29 |
30 | export const Image = styled.img`
31 | width: 60%;
32 | margin: 0 auto;
33 | `;
34 |
35 | export const SVG = styled.svg`
36 | margin: 0 auto;
37 | height: 100%;
38 | width: 100%;
39 | position: absolute;
40 | top: 30vh;
41 | left: 0;
42 | z-index: -1;
43 | @media(max-width:966px) {
44 | display: none;
45 | }
46 | `;
47 |
48 | const getOrder = ({ type, index }) => {
49 | if (index%2 === 0) {
50 | return type === 'image' ? 1 : 2;
51 | }
52 | return type === 'image' ? 2 : 1;
53 | }
54 |
55 | export const FeatureColumn = styled.div`
56 | display: flex;
57 | justify-content: center;
58 | align-items: center;
59 | height: 40vh;
60 | width: 50%;
61 | order: ${getOrder};
62 | opacity: 0;
63 | @media (max-width: 966px) {
64 | width: 100%;
65 | }
66 | `;
--------------------------------------------------------------------------------
/website/src/components/Features/features.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const features = [
4 | {
5 | index: 0,
6 | title: <>Patterns for GraphQL schema>,
7 | imageUrl: 'img/undraw_online_connection.svg',
8 | description: (
9 | <>
10 | Check specification for patterns and pick only those that will really
11 | work for your specific database or business requirements
12 | >
13 | ),
14 | },
15 | {
16 | index: 1,
17 | title: <>Flexibility to adapt>,
18 | imageUrl: 'img/undraw_yoga.svg',
19 | description: (
20 | <>
21 | GraphQL CRUD provides canonical versions and also other variants
22 | giving you overview for different approaches used in the most schemas
23 | >
24 | ),
25 | },
26 | {
27 | index: 2,
28 | title: <>Reference implementations>,
29 | imageUrl: 'img/undraw_abstract_x68e.svg',
30 | description: (
31 | <>
32 | Focus on your business logic and data and generate GraphQL CRUD compliant schemas in any language of your choice
33 | >
34 | ),
35 | },
36 | {
37 | index: 3,
38 | title: <>Framework agnostic>,
39 | imageUrl: 'img/undraw_code_review.svg',
40 | description: (
41 | <>
42 | GraphQL CRUD abstracts from large GraphQL vendors giving you flexibility and ability to migrate without rebuilding your clients and resolves
43 | >
44 | ),
45 | },
46 | {
47 | index: 4,
48 | title: <>Focused on productivity>,
49 | imageUrl: 'img/undraw_usability_testing.svg',
50 | description: (
51 | <>
52 | GraphQL CRUD focuses on productivity by giving developers powerful query capabilities that are not specific to any GraphQL solution providers.
53 | >
54 | ),
55 | },
56 | ];
--------------------------------------------------------------------------------
/website/src/components/Features/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { Container } from '../UI';
4 | import { features } from './features';
5 | import { Section } from './styled.components';
6 | import { FeaturesHeader, FeatureList, LineConnectors } from './components';
7 |
8 | export function Features() {
9 |
10 | const refs = features.map(() => (React.createRef()));
11 | const lineRefs = features.map(() => React.createRef());
12 |
13 | return (
14 |
15 |
16 |
17 |
{/* Scroll scene trigger point for scroll event */}
18 |
19 |
20 |
21 |
22 | );
23 | }
24 |
--------------------------------------------------------------------------------
/website/src/components/Features/styled.components.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Section = styled.section`
4 | position: relative;
5 | min-height: 300vh;
6 | width: 100%;
7 | `;
--------------------------------------------------------------------------------
/website/src/components/Features/styles.module.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * CSS files with the .module.css suffix will be treated as CSS modules
4 | * and scoped locally.
5 | */
6 |
7 |
8 | @media screen and (max-width: 966px) {
9 | .splitContainer {
10 | height: 100%;
11 | }
12 | .leftSplit, .rightSplit {
13 | width: 100%;
14 | }
15 |
16 | .leftSplit, .splitRow {
17 | height: 50vh;
18 | }
19 |
20 | .splitRow.before,
21 | .splitRow.after {
22 | display: none;
23 | height: 25vh;
24 | }
25 |
26 | }
27 |
28 | /* .circle {
29 | width: 250px;
30 | height: 250px;
31 | border-radius: 50%;
32 | display: flex;
33 | justify-content: center;
34 | align-items: center;
35 | background: #f2f2f2;
36 | } */
37 |
38 | .featureImage {
39 | height: 200px;
40 | width: 200px;
41 | margin-bottom: 1rem;
42 | }
43 |
44 | .featureTitle {
45 | font-size: 2rem;
46 | font-weight: 600;
47 | }
48 |
49 | .featureContent {
50 | /* padding: 1.5em; */
51 | margin: 0 auto;
52 | max-width: 75%;
53 | }
--------------------------------------------------------------------------------
/website/src/components/Hero/animations.js:
--------------------------------------------------------------------------------
1 | import TweenLite from 'gsap';
2 |
3 | export const animations = {
4 | logo({ logo }) {
5 | TweenLite.to(logo, 2, {
6 | opacity: 1,
7 | y: 0,
8 | ease: "elastic.out(1, 0.3)"
9 | });
10 | },
11 | title({ title }) {
12 | TweenLite.to(title, 1, {
13 | opacity: 1,
14 | x: 0,
15 | delay: 1
16 | });
17 | },
18 | tagline({ tagline }) {
19 | TweenLite.to(tagline, 1, {
20 | opacity: 1,
21 | x: 0,
22 | delay: 1.5
23 | });
24 | },
25 | cta({ cta }) {
26 | TweenLite.to(cta, .5, {
27 | opacity: 1,
28 | y: 0,
29 | delay: 2
30 | });
31 | }
32 | }
--------------------------------------------------------------------------------
/website/src/components/Hero/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useEffect } from 'react';
2 | import useBaseUrl from '@docusaurus/useBaseUrl';
3 | import Link from '@docusaurus/Link';
4 | import { animations } from './animations';
5 | import { Header, HeaderImage, Title, SubTitle, CTA } from './styled.components';
6 | import { Container } from '../UI';
7 |
8 | export function Hero({ siteConfig }) {
9 | const logo = useRef();
10 | const title = useRef();
11 | const tagline = useRef();
12 | const cta = useRef();
13 |
14 | useEffect(() => {
15 | animations.logo({ logo: logo.current });
16 | animations.title({ title: title.current });
17 | animations.tagline({ tagline: tagline.current });
18 | animations.cta({ cta: cta.current });
19 | });
20 |
21 | return (
22 |
23 |
24 |
25 |
26 |
27 | GraphQL CRUD
28 | {siteConfig.tagline}
29 |
30 |
33 | Get Started
34 |
35 |
36 |
37 |
38 | );
39 | }
--------------------------------------------------------------------------------
/website/src/components/Hero/styled.components.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Header = styled.div`
4 | height: 100vh;
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | text-align: center;
9 | padding: 4rem 0;
10 | `;
11 |
12 | export const HeaderImage = styled.div`
13 | opacity: 0;
14 | max-width: 200px;
15 | width: 60%;
16 | margin: 0 auto;
17 | margin-bottom: 1em;
18 | transform: translateY(300px);
19 | `;
20 |
21 | export const Title = styled.h1`
22 | opacity: 0;
23 | line-height: 7rem;
24 | font-weight: 900;
25 | color: #dc109b ;
26 | background: linear-gradient(60deg, #dc109b, #dc109b, #dc109b);
27 | font-size: 3rem;
28 | -webkit-text-fill-color: transparent;
29 | background-clip: text;
30 | -webkit-background-clip: text;
31 | `;
32 |
33 | export const SubTitle = styled.h2`
34 | opacity: 0;
35 | font-size: 1.5rem;
36 | font-weight: 600;
37 | transform: translateX(50px);
38 | `;
39 |
40 | export const CTA = styled.div`
41 | display: flex;
42 | align-items: center;
43 | justify-content: center;
44 | opacity: 0;
45 | transform: translateY(100);
46 | `;
--------------------------------------------------------------------------------
/website/src/components/Introduction/index.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Container, Flex, Row} from '../UI';
3 | import { Title, Paragraph, Image } from './styled.components';
4 |
5 | export function Introduction() {
6 | return (
7 |
8 |
9 |
10 |
11 |
12 |
Standard for exposing data
13 |
14 | GraphQL CRUD provides specification for common operations on top of the GraphQL.
15 | Giving developers out of the box patterns for accessing their data.
16 | Based on the data of public GraphQL APIs and patterns from major GraphQL providers
17 | GraphQL CRUD gives you ultimate guide for your common data access problems without
18 | bringing complexity or limitations to your workflow
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | );
29 | }
30 |
--------------------------------------------------------------------------------
/website/src/components/Introduction/styled.components.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Title = styled.h2`
4 | margin: 0 auto;
5 | font-weight: 800;
6 | text-transform: uppercase;
7 | @media(min-width:966px) {
8 | width: 80%;
9 | font-size: 3rem;
10 | }
11 | `;
12 |
13 | export const Paragraph = styled.p`
14 | margin-left: auto;
15 | margin-right: auto;
16 | @media(min-width:966px) {
17 | width: 80%;
18 | }
19 | `;
20 |
21 | export const Image = styled.img`
22 | max-width: 400px;
23 | margin: 0 auto;
24 | width: 300px;
25 | @media(min-width:966px) {
26 | width: 500px;
27 | }
28 | `;
--------------------------------------------------------------------------------
/website/src/components/UI/Container.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Container = styled.div`
4 | margin-left: auto;
5 | margin-right: auto;
6 | max-width: ${props => props.maxWidth};
7 | padding-left: var(--ifm-spacing-horizontal);
8 | padding-right: var(--ifm-spacing-horizontal);
9 | width: 100%;
10 | `;
11 |
12 | Container.defaultProps = {
13 | maxWidth: 'var(--ifm-container-width)'
14 | }
15 |
--------------------------------------------------------------------------------
/website/src/components/UI/Flex.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | export const Flex = styled.div`
5 | display: flex;
6 | height: ${props => props.height};
7 | min-height: ${props => props.minHeight};
8 | width: ${props => props.width};
9 | align-items: ${props => props.alignItems};
10 | justify-content: ${props => props.justifyContent};
11 | margin: ${props => props.margin};
12 | background: ${props => props.background};
13 | color: ${props => props.color};
14 | order: ${props => props.order};
15 | `;
16 |
17 | Flex.defaultProps = {
18 | width: '100%',
19 | height: '100vh',
20 | alignItems: 'center',
21 | justifyContent: 'center',
22 | margin: '0 auto'
23 | }
--------------------------------------------------------------------------------
/website/src/components/UI/Row.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Row = styled.div`
4 | @media(min-width:966px) {
5 | display: flex;
6 | align-items: center;
7 | flex-flow: row wrap;
8 | }
9 | `;
--------------------------------------------------------------------------------
/website/src/components/UI/index.js:
--------------------------------------------------------------------------------
1 | export { Container } from './Container';
2 | export { Flex } from './Flex';
3 | export { Row } from './Row';
--------------------------------------------------------------------------------
/website/src/components/Video/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import useBaseUrl from '@docusaurus/useBaseUrl';
3 |
4 | import { Flex } from '../UI';
5 | import {
6 | VideoComponent,
7 | Play,
8 | Title,
9 | Content,
10 | Modal,
11 | ModalBackground,
12 | Close,
13 | ModalContent,
14 | YouTube,
15 | IFrame
16 | } from './styled.components';
17 |
18 | function VideoModal({ open, close}) {
19 | return (
20 |
21 |
22 | Close
23 |
24 |
25 | VIDEO
36 |
37 |
38 |
39 | );
40 | }
41 |
42 | export function Video() {
43 | const [open, setOpen] = useState(false);
44 |
45 | const toggleModal = (event) => {
46 | event.preventDefault();
47 | console.log('here', open);
48 | setOpen(!open);
49 | };
50 |
51 | return (
52 | <>
53 |
54 |
55 |
56 | Offix in action
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 | >
65 | );
66 | }
--------------------------------------------------------------------------------
/website/src/components/Video/styled.components.js:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 |
3 | export const Content = styled.div`
4 | text-align: center;
5 | `;
6 |
7 | export const Title = styled.h3`
8 | font-size: 3rem;
9 | font-weight: 900;
10 | `;
11 |
12 | export const Play = styled.a`
13 | width: 35%;
14 | `;
15 |
16 | export const VideoComponent = styled.div`
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | width: 800px;
21 | height: 500px;
22 | margin: 0 auto;
23 | background:
24 | linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.4)),
25 | url(/img/offix-background.png);
26 | border-radius: 1rem;
27 | box-shadow:
28 | 0 1px 2px -2px rgba(0, 0, 0, 0.16),
29 | 0 3px 6px 0 rgba(0, 0, 0, 0.12),
30 | 0 5px 12px 4px rgba(0, 0, 0, 0.09);
31 | @media(max-width:966px) {
32 | width: 320px;
33 | height: 200px;
34 | }
35 | `;
36 |
37 | export const Modal = styled.div`
38 | position: absolute;
39 | z-index: 10;
40 | top: 0;
41 | left: 0;
42 | width: 100%;
43 | height: 100%;
44 | display: block;
45 | opacity: ${(prop) => prop.open ? 1 : 0};
46 | visibility: ${(prop) => prop.open ? 'visible' : 'hidden'};
47 | -webkit-transition: opacity 1.0s ease-in, visibility 1.0s ease-in;
48 | -moz-transition: opacity 1.0s ease-in, visibility 1.0s ease-in;
49 | -o-transition: opacity 1.0s ease-in, visibility 1.0s ease-in;
50 | transition: opacity 1.0s ease-in, visibility 1.0s ease-in;
51 | `;
52 |
53 | export const ModalBackground = styled.div`
54 | position: absolute;
55 | top: 0;
56 | left: 0;
57 | width: 100%;
58 | height: 100%;
59 | z-index: 11;
60 | background: rgba(0,0,0,0.9);
61 | `;
62 |
63 | export const Close = styled.a`
64 | position: fixed;
65 | z-index: 13;
66 | top: calc(60px + 1rem);
67 | right: 2rem;
68 | color: #fff !important;
69 | text-decoration: underline;
70 | `;
71 |
72 | export const ModalContent = styled.div`
73 | position: sticky;
74 | z-index: 12;
75 | top: 50%;
76 | transform: translateY(-50%);
77 | `;
78 |
79 | export const YouTube = styled.div`
80 | position: relative;
81 | width: 75%;
82 | padding-top: 25px;
83 | padding-bottom: 50%;
84 | margin: 0 auto;
85 | @media(max-width:966px) {
86 | width: 100%;
87 | }
88 | `;
89 |
90 | export const IFrame = styled.iframe`
91 | border: 2px solid #fff;
92 | position: absolute;
93 | top: 50%;
94 | left: 50%;
95 | height: 70%;
96 | width: 70%;
97 | transform: translateX(-50%) translateY(-50%);
98 | @media(max-width:966px) {
99 | width: 90%;
100 | height: 90%;
101 | }
102 | `;
--------------------------------------------------------------------------------
/website/src/components/useWindowSize.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export function useWindowSize() {
4 | const isClient = typeof window === 'object';
5 |
6 | function getSize() {
7 | return {
8 | width: isClient ? window.innerWidth : undefined,
9 | height: isClient ? window.innerHeight : undefined
10 | };
11 | }
12 |
13 | const [windowSize, setWindowSize] = React.useState(getSize);
14 |
15 | React.useEffect(() => {
16 | if (!isClient) {
17 | return false;
18 | }
19 |
20 | function handleResize() {
21 | setWindowSize(getSize());
22 | }
23 |
24 | window.addEventListener('resize', handleResize);
25 | return () => window.removeEventListener('resize', handleResize);
26 | }, []); // Empty array ensures that effect is only run on mount and unmount
27 |
28 | return windowSize;
29 | }
--------------------------------------------------------------------------------
/website/src/css/custom.css:
--------------------------------------------------------------------------------
1 | /* stylelint-disable docusaurus/copyright-header */
2 | /**
3 | * Any CSS included here will be global. The classic template
4 | * bundles Infima by default. Infima is a CSS framework designed to
5 | * work well for content-centric websites.
6 | */
7 |
8 | /* You can override the default Infima variables here. */
9 | :root {
10 | --ifm-color-primary: #006d8b;
11 | --ifm-color-primary-dark: #008cb2;
12 | --ifm-color-primary-darker: #0085a8;
13 | --ifm-color-primary-darkest: #006d8b;
14 | --ifm-color-primary-light: #00acda;
15 | --ifm-color-primary-lighter: #00b3e4;
16 | --ifm-color-primary-lightest: #02c9ff;
17 | --ifm-code-font-size: 95%;
18 | }
19 |
20 | #__docusaurus {
21 | position: relative;
22 | }
23 |
24 | .docusaurus-highlight-code-line {
25 | background-color: rgb(72, 77, 91);
26 | display: block;
27 | margin: 0 calc(-1 * var(--ifm-pre-padding));
28 | padding: 0 var(--ifm-pre-padding);
29 | }
30 |
31 | .navbar {
32 | box-shadow: none;
33 | }
34 |
35 | .button--rounded {
36 | border-radius: 2rem;
37 | }
--------------------------------------------------------------------------------
/website/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Layout from "@theme/Layout";
3 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
4 |
5 | import { Hero } from "../components/Hero";
6 | import { Introduction } from "../components/Introduction";
7 | import { Features } from "../components/Features";
8 |
9 | export default function Home() {
10 | const context = useDocusaurusContext();
11 | const { siteConfig = {} } = context;
12 |
13 | return (
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
--------------------------------------------------------------------------------
/website/src/pages/versions.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Layout from '@theme/Layout';
3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
4 |
5 | import versions from '../../versions.json';
6 | import { Container } from '../components/UI';
7 |
8 | export default function Home() {
9 | const context = useDocusaurusContext();
10 | const { siteConfig = {} } = context;
11 |
12 | const latestVersion = versions[0];
13 | const repoUrl = `https://github.com/${siteConfig.organizationName}/${siteConfig.projectName}`;
14 |
15 | return (
16 |
20 |
21 |
22 |
23 | {siteConfig.title} Versions
24 |
25 |
New versions of this project are released every so often.
26 |
Current version
27 |
28 |
29 |
30 | {latestVersion}
31 |
32 | {/* You are supposed to change this href where appropriate
33 | Example: href="/docs(/:language)/:id" */}
34 |
36 | Documentation
37 |
38 |
39 |
40 | Release Notes
41 |
42 |
43 |
44 |
45 |
46 | This is the latest version published to npm.
47 |
48 |
Master
49 |
50 |
51 |
52 | master
53 |
54 | {/* You are supposed to change this href where appropriate
55 | Example: href="/docs(/:language)/next/:id" */}
56 |
58 | Documentation
59 |
60 |
61 |
62 | Source Code
63 |
64 |
65 |
66 |
67 |
Past Versions
68 |
Here you can find previous versions of the documentation.
69 |
70 |
71 | {versions.map(
72 | (version, index) =>
73 | version !== latestVersion && (
74 |
75 | {version}
76 |
77 | {/* You are supposed to change this href where appropriate
78 | Example: href="/docs(/:language)/:version/:id" */}
79 |
81 | Documentation
82 |
83 |
84 |
85 |
86 | Release Notes
87 |
88 |
89 |
90 | ),
91 | )}
92 |
93 |
94 |
95 | You can find past versions of this project on{' '}
96 | GitHub .
97 |
98 |
99 |
100 |
101 | );
102 | }
103 |
--------------------------------------------------------------------------------
/website/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/.nojekyll
--------------------------------------------------------------------------------
/website/static/CNAME:
--------------------------------------------------------------------------------
1 | graphqlcrud.org
--------------------------------------------------------------------------------
/website/static/css/custom.css:
--------------------------------------------------------------------------------
1 | /* your custom css */
2 | .mainLogo {
3 | width: 30%;
4 | height: auto;
5 | margin: 0 auto;
6 | }
7 |
8 | .intro {
9 | display: flex;
10 | justify-content: center;
11 | }
12 | .introVideo {
13 | width: 80%;
14 | height: 600px;
15 | }
16 |
17 | @media only screen and (min-device-width: 360px) and (max-device-width: 736px) {
18 | }
19 |
20 | @media only screen and (min-width: 1024px) {
21 | }
22 |
23 | @media only screen and (max-width: 1023px) {
24 | }
25 |
26 | @media only screen and (min-width: 1400px) {
27 | }
28 |
29 | @media only screen and (min-width: 1500px) {
30 | }
31 |
--------------------------------------------------------------------------------
/website/static/img/aerogear.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/aerogear.png
--------------------------------------------------------------------------------
/website/static/img/browser-frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/browser-frame.png
--------------------------------------------------------------------------------
/website/static/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/favicon.ico
--------------------------------------------------------------------------------
/website/static/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/logo.png
--------------------------------------------------------------------------------
/website/static/img/pixel-frame.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/pixel-frame.png
--------------------------------------------------------------------------------
/website/static/img/play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/graphqlcrud/spec/ccd3e41b57d414cf9b8e83303b7df5fc29bfed0e/website/static/img/play.png
--------------------------------------------------------------------------------
/website/static/img/undraw_abstract_x68e.svg:
--------------------------------------------------------------------------------
1 | abstract
--------------------------------------------------------------------------------
/website/static/img/undraw_code_review.svg:
--------------------------------------------------------------------------------
1 | code review
--------------------------------------------------------------------------------
/website/static/img/undraw_contrast.svg:
--------------------------------------------------------------------------------
1 | contrast
--------------------------------------------------------------------------------
/website/static/img/undraw_data_extraction.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/static/img/undraw_design.svg:
--------------------------------------------------------------------------------
1 | design_feedback
--------------------------------------------------------------------------------
/website/static/img/undraw_online_connection.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/website/static/img/undraw_portfolio_update.svg:
--------------------------------------------------------------------------------
1 | portfolio update
--------------------------------------------------------------------------------
/website/static/img/undraw_progressive_app.svg:
--------------------------------------------------------------------------------
1 | progressive_app
--------------------------------------------------------------------------------
/website/static/img/undraw_redesign.svg:
--------------------------------------------------------------------------------
1 | redesign_feedback
--------------------------------------------------------------------------------
/website/static/img/undraw_usability_testing.svg:
--------------------------------------------------------------------------------
1 | usability testing
--------------------------------------------------------------------------------
/website/static/img/undraw_yoga.svg:
--------------------------------------------------------------------------------
1 | yoga
--------------------------------------------------------------------------------
/website/versions.json:
--------------------------------------------------------------------------------
1 | ["0.1.0"]
2 |
--------------------------------------------------------------------------------