Code Monkey home page Code Monkey logo

blitz's People

Contributors

andreasasprou avatar anteprimorac avatar beerose avatar blitzjs-bot avatar bravo-kernel avatar datner avatar dbrxnds avatar estevanjantsk avatar exkazuu avatar flybayer avatar gengjiawen avatar github-actions[bot] avatar joaojesus94 avatar justinsmid avatar kosai106 avatar leonmueller-oneandonly avatar lmisea avatar maciej-ka avatar maotora avatar medihack avatar nelsonmestevao avatar noxify avatar oltdaniel avatar paulm17 avatar selcukfatihsevinc avatar siddhsuresh avatar tmcw avatar tordans avatar trancever avatar zeko369 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blitz's Issues

Most new lines are removed from template files for plain JS apps

What is the problem?

Most new lines are removed from template files for plain JS apps

Example for db/index.js:

import { PrismaClient } from "@prisma/client"
export * from "@prisma/client"
const prisma = new PrismaClient()
export default prisma

Steps to Reproduce

  1. blitz new myapp --js
  2. Inspect myapp/db/index.js and see that it doesn't match the template at packages/generator/templates/app/db/index.ts

This also happens for blitz generate

Versions

latest canary branch

Query & Mutation Error Handling

This issue is for designing how error handling works for queries & mutation execution. Related #85

Please provide any feedback you have or if you have a suggestion for how this can be better!

Goals

  • Always handle errors the same way, regardless if function is called directly or via RPC
  • Support all possible error types:
    • Native JS errors like TypeError, SyntaxError, etc.
    • Custom errors thrown from third-party libraries like Prisma
    • User defined error cases inside the query/mutation

Solution

Serialize error objects for sending over the network. On the client, the error will be deserialized. The error will be returned from useQuery, but it will be re-thrown from mutations.

All error object properties will be preserved except for the stack trace (I don't think a server stack trace is helpful on the client)

On Server

No special API. Simply write normal Node.js code as you've always done.

  • You can throw any built-in or custom error object
  • Or instead you can handle errors your own way. For example, return an object from the function that has success and error properties.

Custom Errors

// /some/path/createProduct.ts
import db from 'db'

class ValidationError extends Error {
  constructor(fieldErrors) {
    this.fieldErrors = fieldErrors
  }
}

export default async function createProduct(args, ctx) {
  if (!args.name) {
    throw ValidationError({name: "Name must be required})
  }

  // ... other stuff
  return product
}

On Client

Queries

// inside page.tsx
export default function(props: {query: {id: number}}) {
  const [product, {error}] = useQuery(getProduct, {where: {id: props.query.id}})

  if (error) {
    if (error.name === 'AuthorizationError') {
      return <div>You are not authorized to view this</div>
    } else {
      return <div>Error: {error.message}</div>
    }
  }

  return <div>{product.name}</div>
}

Mutations

// inside page.tsx
try {
  await createProduct(args)
} catch (error) {
  if (error.name === 'AuthorizationError') {
    alert('You are not authorized')
  } else if (error.name === 'ValidationError') {
    setErrors(error.fieldErrors)
  }
}

Implementation

Support other ESLint configs for blitz new template

Currently blitz new generates an app with the create-react-app ESLint config.

The community is divided enough over their preferred eslint config that I think we should support a few other ones and then prompt the user to choose during blitz new generation.

Other configs to support:

  • Standard.js

If you want something besides CRA eslint or Standard.js, comment here. We can't support every variation, but we can support the top ones.

And we'll definitely appreciate help implementing this!

Improve dependency versioning of the blitz new template

What do you want and why do you want it?.
Currently most of the dependency versions in the blitz new template are defined as latest. This is not ideal.

It's much better for versions to be explicitly defined to prevent unintended dependency version changes. Unknown dependency version changes are a common source of bugs.

Currently the template is using latest to ensure the new app always has the latest version.

Possible implementation(s)

  • Change the template source to explicit versions and then add tooling to make sure they are always at the latest
  • Change the template source to explicit versions and then automatically run yarn upgrade during blitz new generation. Problem with this approach, is that we would install all the deps, then turn around and install the updated deps again.
  • Keep as latest and then during blitz new generation automatically update that to the explicit version

Feature request: Stripe Recipe

What do you want and why?

To make Blitz the first choice framework for makers, it would be great to have Stripe integration so we can quickly go from a blank slate to selling products on the site.

Possible implementation(s)

This can likely be achieved as a Blitz installer. Stripe integration I believe just requires a serverless endpoint and configuration. Might need to explore this more before suggesting how exactly it could work.

Fix awkward pause in logging during blitz new

What is the problem?

There's a long pause after the files are created, but before "Installing dependencies" is printed. This is because we are fetching the latest dependency versions.

Instead of an awkward pause, we should show some status update. Move the "installing dependencies" log above this, then add a spinner or something.

Steps to Reproduce:

  1. Install global [email protected]
  2. Run blitz new
  3. Notice a long pause after the files are created, but before "Installing dependencies" is printed

Versions:

  • Blitz: 0.5.0-canary.6

Generate API documentation

What do you want and why?

When building a full stack application, it's common for it to grow into something much more. For example, a companion mobile application is often part of a website and backend ecosystem. It would great if Blitz expands to accommodate this use case more and generate easy-to-user documentation or client which can be used to build external clients that utilise the generated Blitz API.

Possible implementation(s)

  • Generate API documentation from the API functions that Blitz generates at build time.
    • This could include all API routes and schema for query strings and POST request bodies.
  • Generate a TypeScript API client so you can just import and start calling the API externally?
    • e.g. Implementations for axios or pure fetch API?
    • Not sure how but even an ability to generate something for use outside of JS? For example what if you wanted to build your app with Flutter?
  • Include examples for common use cases
    • e.g. how to authenticate an external client

Additional context

I have something like swagger.io in mind, however I'm sure it could be far greater!

Example:
https://petstore.swagger.io/?_ga=2.158440947.899930001.1594640979-1996492643.1594640979

Cloudinary support

What do you want and why?

It would be helpful to be able to programmatically add image and video upload, manipulation, optimization, and delivery capabilities to a Blitz app via Cloudinary's REST APIs.

Possible implementation(s)

This could be a recipe installer.

Blitz needs to restart after schema change and CRUD generated

What is the problem?

TypeError: Cannot read property 'findMany' of undefined after schema changed and CRUD
generated

Steps to Reproduce:

  1. Follow initial example (User Model and generate CRUD for user)
  2. With blitz started, add a new model on the schema (i.e. Comment) .
  3. blitz db migrate
  4. blitz generate all comment
  5. go to http://localhost:3000/comments
  6. appears TypeError: Cannot read property 'findMany' of undefined error on the browser
  7. appears > Running getComments(null) ✕ getComments failed: TypeError: Cannot read property 'findMany' of undefined on the console
  8. Restart Blitz
  9. Everything OK

Versions:

debug: local
debug: pkgPath: /Users/osirvent/dev/blitzjs/tests/test2/node_modules/@blitzjs/cli

macOS Catalina darwin-x64 node-v12.16.2

blitz: 0.6.2 (global)
blitz: 0.6.2 (local)

Supporting Documentation
`×TypeError: Cannot read property 'findMany' of undefined
getComments
./app/_rpc/comments/queries/getComments.ts:4

1 | import db, {FindManyCommentArgs} from 'db'
2 |
3 | export default async function getComments(args: FindManyCommentArgs) {

4 | const comments = await db.comment.findMany(args)
5 |
6 | return comments
7 | }

at /Users/osirvent/dev/blitzjs/tests/test2/node_modules/

blitzjs/server/dist/server.cjs.development.js:1610:34
at _catch (/Users/osirvent/dev/blitzjs/tests/test2/node_modules/
blitzjs/server/dist/server.cjs.development.js:1566:18
at /Users/osirvent/dev/blitzjs/tests/test2/node_modules/
blitzjs/server/dist/server.cjs.development.js:1609:32
apiResolver
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/api-utils.js:46:15
processTicksAndRejections
internal/process/task_queues.js:97:5
async DevServer.handleApiRequest
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:464:9
async Object.fn
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:386:37
async Router.execute
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/router.js:134:32
async DevServer.run
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:506:29
async DevServer.handleRequest
/Users/osirvent/dev/blitzjs/tests/test2/node_modules/next/dist/next-server/server/next-server.js:150:20
This screen is visible only in development. It will not appear if the app crashes in production.
Open your browser’s developer console to further inspect this error.`


How To Fix

See #225 (comment)

Prompt to choose database engine on app creation

Extracted from blitz-js/legacy-framework#764. Writing this up as a new issue because blitz-js/legacy-framework#764 was closed.

What do you want and why?

A way to choose which database engine to use (Postgres or SQLite) on app creation.

Possible implementation(s)

@flybayer wrote:

I think we should add a prompt to blitz new that asks if you want sqlite or postgres (same as we do now for form library).

  • For postgres we want to change the role to be an enum instead of a string.

Additional context

@flybayer also wrote:

We are going to remove the provider = ["sqlite", "postgres"] thing because it's causing too many problems.

(I couldn’t find an issue covering this point.)

Automatic Cache Invalidation

What do you want and why?

Currently you need to manually call refetch() or mutate() on a useQuery result after doing a mutation.

We should try to come up with a way to automatically invalidate queries so that most users don't have to think about it all.

I haven't spent a lot of time thinking about this, but here's a couple options.

Based on file structure

We could automatically invalidate queries at the same folder level of the mutation.

So with this file structure,

app/products/queries/getProduct.ts
app/products/queries/getProducts.ts
app/products/mutations/createProduct.ts
app/products/mutations/updateProduct.ts
app/products/mutations/deleteProduct.ts

Calling createProduct, updateProduct, or deleteProduct would invalidate the cache for getProduct and getProducts.

But the problem with this is that app/queries/getDisplayProducts.ts would not be invalidated even thought it is also affected. But maybe this is still worth it if it solves the 80% use case.

Via Some Type of Introspection Magic

Maybe it's possible for us to compute the entire data graph at build time using a combination of the Prisma DMMF and Typescript? If possible, this would be amazing because we could accurately invalidate all queries anywhere in the app.

I think this would only work with Prisma, so we should still have some other fallback method for Blitz apps without Prisma.

On Route Change

Something else that's a possibility is busting the cache on route change, but that still requires you to think about it.

I'm super keen to hear anyone's ideas on this!

Allow installer transforms to run on multiple files

What do you want and why?

Currently the transform executor API theoretically could support the user selecting multiple files to transform, or using a detector function to dynamically select all files that pass a custom test.

Possible implementation(s)

There's two options here. First, we could continue to use the selectTargetFiles field which already exists on the API, and just require the author to do whatever they'd like to detect the total list of files to modify.

Alternatively, we could change that prop to something like detectFile which takes in a file's AST and the recast API (similar to the transform API) and returns a boolean, telling us whether or not to execute the transform on the file. In that case, we'd have to target the entire codebase recursively with the transform which makes it a bit slower, but has the benefit that it will prevent authors from having to write too much boilerplate.

There is of course the third option where we support both 😬. That's just a stretch goal though

Bug: `blitz h` doesn't display the help information

What is the problem?

blitz h should display the help information instead of this:

~/c/blitz> blitz h
You are using alpha software - if you have any problems, please open an issue here:
  https://github.com/blitz-js/blitz/issues/new/choose
Also, this CLI may be slow as it's not yet optimized for speed

 ›   Warning: h is not a blitz command.
Did you mean b? [y/n]:
 ›   Error: Run blitz help for a list of available commands.

Return 404 when there's no default export from a query/mutation file

What do you want and why?

I think sometimes it's useful having code that's not automatically exposed as an endpoint, but it's sitting there in a query or a mutation file. For example, when doing trunk based development you want the code in place, but not yet exposed to the world.

A common way to do this are feature flags, but those carry their own issues, another common approach I take is to code the feature, but hide the endpoint. In rails this is easy to do because a controller needs to be connected on the routes file.

Possible implementation(s)

I think this should be as simple as returning 404 if there's no default export on a query or a mutation. Currently it tries to apply a middleware to an undefined object and it fails.

Additional context

N/A

Fix blitz internal dev issues with dependencies not building in correct order

What is the problem?

We have a pretty rough situation with our blitz dev environment when running yarn dev.

When running yarn build, all packages are built in the proper order and everything works great. Dependent packages are automatically built before parent packages.

However, this does not work right with yarn dev. Everything is built in a random order resulting in errors like this:

@blitzjs/cli: src/commands/install.ts(3,25): error TS7016: Could not find a declaration file for module '@blitzjs/installer'. '/Users/b/c/blitz/packages/installer/dist/index.js' implicitly has an 'any' type.
@blitzjs/cli:   Try `npm install @types/blitzjs__installer` if it exists or add a new declaration (.d.ts) file containing `declare module '@blitzjs/installer';`
@blitzjs/cli: 8:58:46 PM - Found 1 error. Watching for file changes.

As a workaround thus far, we have package.json scripts like this to ensure packages are built in the proper order. But we're now adding more and more packages and it's getting out of hand.

    "predev": "wait-on ../core/dist/packages/core/src/index.d.ts && wait-on ../server/dist/packages/server/src/index.d.ts && wait-on ../generator/dist/packages/generator/src/index.d.ts && wait-on ../cli/lib/src/index.js",

Steps to Reproduce

  1. Remove all the predev package.json scripts
  2. yarn reset
  3. yarn dev

Possible Fixes

  1. Ideally we can fix this without all the wait-on stuff. I don't know if this is a lerna thing or a tsdx thing. I think maybe lerna? If so, maybe it's as simple as adding a certain configuration or cli flag. I don't know.
  2. The other way is to write a custom node script that abstracts away the manual wait-on stuff. The node script can be called via a package.json script, and it would automatically detect the other blitz package dependencies and wait for those to be built.

useSession() during SSR

What do you want and why?

useSession() should also return data during SSR.

Possible implementation(s)

Fetch the session data from the incoming request, make it available to useSession.

Additional context

First discussed during the React Summit discussion.

Blitz start is broken

Problem

  1. Checkout canary branch
  2. yarn install and yarn build
  3. cd examples/vanilla-next
  4. yarn start

You get the following error:

~/c/b/e/vanilla-next> yarn start
yarn run v1.19.1
$ blitz start
[ wait ]  starting the development server ...
[ info ]  waiting on http://localhost:3000 ...
ReferenceError: _launchEditorFn is not defined
    at Object.<anonymous> (/Users/b/c/blitz/node_modules/next/dist/compiled/launch-editor/index.js:2:77)
    at Module._compile (internal/modules/cjs/loader.js:1158:30)
    at Module._compile (/Users/b/c/blitz/node_modules/pirates/lib/index.js:99:24)
    at Module._extensions..js (internal/modules/cjs/loader.js:1178:10)
    at Object.newLoader [as .js] (/Users/b/c/blitz/node_modules/pirates/lib/index.js:104:7)
    at Module.load (internal/modules/cjs/loader.js:1002:32)
    at Function.Module._load (internal/modules/cjs/loader.js:901:14)
    at Module.require (internal/modules/cjs/loader.js:1044:19)
    at require (internal/modules/cjs/helpers.js:77:18)
    at Object.<anonymous> (/Users/b/c/blitz/node_modules/next/dist/server/lib/error-overlay-middleware.js:1:166)

[Feature Request]: Add i18n

What do you want and why?

i18n is immensely needed in any web framework.

Possible implementation(s)

The Nextjs community are yet to agree on the best possible way to implement i18n while keeping SSR / SSG / Serverless working as expected.

Can this be solved by Blitz?

Additional context

vercel/next.js#10651

Implement the `mutate` function for `useQuery`

What do you want and why?

We need to add a mutate function that's returned from useQuery. This allows you to manually update the query cache without refetching from the network.

  • mutate must be fully typed for Typescript usage. The input type is the same as the query function return type.
  • After being called, mutate must automatically trigger a refetch for the initial query to ensure it has the correct data
export default function (props}) {
  const [product, {mutate}] = useQuery(getProduct, {where: {id: props.id}})

  return (
    <Formik
      initialValues={product}
      onSubmit={async (values) => {
        try {
          const product = await updateProduct(values)
          mutate(product)
        } catch (error) {
          alert("Error saving product")
        }
      }}>
      {/* ... */}
    </Formik>
  )
}

Prevent `blitz db migrate` from attempting to create new migration files in production.

What is the problem?

Currently blitz db migrate always runs prisma migrate save and prisma migrate up.

migrate up will create new migration files if needed and migrate up will apply existing migration files.

For safety, migrate save should only run if NODE_ENV !== 'production'. But migrate up should always run.

Here's the line of code where this happens: https://github.com/blitz-js/blitz/blob/canary/packages/cli/src/commands/db.ts#L43

Blitz file path replacement in logs is broken

What is the problem?

Blitz server isn't swapping out the file paths properly in the Next.js output. See the wrong output in the log below.

Steps to Reproduce:

  1. Delete examples/store/node_modules/@prisma/client
  2. Run blitz build in the store example

Versions:

  • Blitz: canary
  • OS: macOS

Supporting Documentation

~/c/b/e/store> blitz b
✔ Blitz is ready
Warning: You have enabled experimental feature(s).
Experimental features are not covered by semver, and may cause unexpected or broken application behavior. Use them at your own risk.

Creating an optimized production build

Failed to compile.

/Users/b/c/blitz/examples/store/.blitz/caches/build/app/_rpc/products/mutations/createProduct.ts
ERROR in /Users/b/c/blitz/examples/store/.blitz/caches/build/app/_rpc/products/mutations/createProduct.ts(1,13):
1:13 Module '"../../../../../../../../../../../../../Users/b/c/blitz/examples/store/.blitz/caches/build/db"' has no exported member 'ProductCreateArgs'. Did you mean to use 'import ProductCreateArgs from "../../../../../../../../../../../../../Users/b/c/blitz/examples/store/.blitz/caches/build/db"' instead?
  > 1 | import db, {ProductCreateArgs} from 'db'
      |             ^
    2 |
    3 | export default async function createProduct(args: ProductCreateArgs) {
    4 |   const product = await db.product.create(args)


> Build error occurred
Error: > Build failed because of webpack errors
    at build (/Users/b/c/blitz/node_modules/next/dist/build/index.js:13:900)

Need a tiny refactor to the new app template's index page

What do you want and why?

The new app template's index.tsx has this:

const modelSnippet = `model Project {
  id      Int      @default(autoincrement()) @id
  name    String
}`

const migrateSnippet = `$ blitz db migrate
$ blitz generate all project`

And it's very confusing for new folks because it looks like it may be part of blitz code. But in reality it's just a string for display on the webpage.

So we need to just inline that string into the JSX where it's used, instead of having them as separate variables.

Here's the lines that need moved: https://github.com/blitz-js/blitz/blob/canary/packages/generator/templates/app/app/pages/index.tsx#L3-L9

Make dependency updates run in parallel during blitz new

What do you want and why do you want it?.

Inside packages/cli/src/generators/app.tsx we have the following:

    await replaceDependencies(pkg, this.destinationPath(), pkgDependencies, 'dependencies')
    await replaceDependencies(pkg, this.destinationPath(), pkgDevDependencies, 'devDependencies')

But that needs to be changed to use Promise.all()

This makes blitz new unnecessarily slow

Add `AppProps` to new app template in `_app.tsx`

What do you want and why?

Update this file to include AppProps like this:

import { AppProps } from 'blitz'

export default function MyApp({ Component, pageProps }: AppProps) {
  return <Component {...pageProps} />
}

Open source veterans, please leave this issue for someone new to Blitz and/or open-source :)

Mutation / Query Mocking utilities

What do you want and why?

Commonly, API calls will be mocked out in tests in order to decouple and speed them up.
Since one of Blitz core concepts is to hide network calls and replace them by (opaque) RPC, the natural counterpart would be to just mock out the generated network fetching logic instead.

The interface could look like this:

import ProjectsList from "./ProjectsList";
import getProjects from "./queries/getProjects";
import { mockQuery } from "blitz/test";

const getProjectsMock = mockQuery(
  getProjects,
  // mock implementation
  async () => {
    return [{ id: 1, name: "Blitz" }];
  } 
);

describe("ProjectsList", () => {
  it("calls `getProjects`", () => {
    const comp = render(() => <ProjectsList />);
    expect(getProjects).toHaveBeenCalled();
  })
})

Possible implementation(s)

Since Blitz already replaces queries with generated code, we could just generate a "sleeping mock" during testing.
By "Sleeping", i mean: mocking would be disabled by default, but could be enabled using mockQuery.

This would circumvent all Test-Runner mocking mechanisms and thus doesn't run into the problem of mocks needing to be hoisted (see Jest hoisting jest.mock).

[Easy] Upgrade Next to 9.4.4

What do you want and why?

Upgrade Next to 9.4.4 in all the package.json's in our repo.

NOTE: this is super easy change, so veterans, please leave it for a new Blitz contributor or someone new to contributing to open source :)

[Feature] Healthcheck Endpoint

What do you want and why?

Applications commonly require status endpoints for monitoring purposes.
It'd be great if Blitz supported that out of the box, preferably with included health checks of all necessary resources.

GET /api/health

200 OK
{ "isHealthy": true, "resources": { "redis": true, "db": true } }

Registering a new resource to be health-checked could work like this:

import { registerHealthCheck } from "blitz/health";

registerHealthCheck(
  "redis",
  async () => {
    const response = await redis.ping();
    return response === "pong";
  }
);

We could readily bundle healthchecks with officially supported resources, e.g. the Prisma Datasource, Job Queues or connected SMTP endpoints.

Possible implementation(s)

Add the /api/health API route, which will check against all registered resources when called.
The specific endpoint could be altered in blitz.config.js.

Storybook (or similar) Recipe

What do you want and why?

I prefer to develop reusable components in isolation using Storybook so support in Blitz would be very useful.

According to https://2019.stateofjs.com/testing/ Storybook is significantly more popular than alternatives such as Styleguidist.

Some teams might also benefit from publishing their Storybook as a helpful guide for other developers.

Possible implementation(s)

  • Built-in Blitz support which would require adding the dependencies and config files to the generator templates
  • As an installer

Given that Storybook is a popular choice and that it doesn't impose any restrictions on the user, I think built-in support could be best based on the "Convention over Configuration" argument.

Additional context

Support vanilla Javascript for the blitz new template

You can use vanilla JS right now in a Blitz app by creating new files in JS or converting the existing files to JS.

However, currently blitz new generates all Typescript files. We need to add a prompt during blitz new for selecting Typescript or plain Javascript.

The main unknown on implementing this is how to minimize code duplication for template files. Can we write all templates in TS and then have a tool that strips all the TS stuff?

Note: we also need this same JS support for all the blitz generate commands.

RPC Specification

This issue is for defining the RPC specification used for Blitz queries and mutations. Each query and mutation can be used by third parties, so the specification must be clear and documented publicly.

This initial specification is my first pass. I haven't thought real deeply about this, so please help me refine it!

  • Define the specification
  • Implement versioning (#98)
  • Add comprehensive tests (#99)
  • Add specification to the Blitz docs (blitz-js/blitzjs.com#21)

Specification

This specification is used for both queries and mutations. From an HTTP perspective, there is no difference between a query and mutation because they are both function calls.

Details

HEAD

This will be used to warm the lambda. For example, when a component renders on the client that uses a mutation, it will issue a HEAD request to the mutation endpoint immediately on page load so that there's a much higher chance the actual mutation request will hit a warm lambda.

  • Must always return HTTP 200

POST

Used to actually execute the RPC

  • Request body must be JSON with the following keys:
    • params (required) - This will be provided as the first argument to the query or mutation
    • version (optional) - String containing the version of the built RPC client
  • Response body is JSON with the following keys:
    • result - the exact result returned from the query/mutation function or null if there was an error
    • error - null if success, otherwise an object.

Example:

// Valid request
{
  params: {id: 1},
}

// Success response
{
  result: {id: 1, name: "Brandon"},
  error: null
}

Responses

HTTP Response Code Response result value Response error value
HEAD 200 - -
Valid request 200 - -
Invalid request, not JSON 400 null {message: "Request body is not valid JSON"}
Invalid request, missing params 400 null {message: "Request body is missing the 'params' key"}
Execution success 200 Exact value returned from query/mutation function null
Execution error 200 null TBD
All other HTTP verbs, like GET 404 - -

Versioning

Any time you have client-server communication, there is a very real risk of the client & server code becoming out of sync and causing an error. For example, let's say you have a mutation function whose input is a single object. You decide to change the mutation input to be a single array. You make the change and deploy it. Your server code is now updated, but all your users still have the old client code running in the browser. So it's likely you now have client code sending an object to your mutation which is going to error because it's expecting an array.

Now one obvious solution is to only make changes that are backwards compatible, but sometimes you can't do this.

The other solution is to track the version of your client code and your server code. If there is a version mismatch, you can display a friendly message to the user, telling them they need to reload the webpage to continue.

Blitz is well positioned to provide this, so let's do.

  • During the build, we generate a unique version number that's embedded in both the rpc client and the server handlers.
  • The RPC client will automatically send it's version to the server in the request body.
  • The server will check for a version match.
  • If the versions don't match, we let the handler run as usual, resulting in execution success or error. But we also need to return an indicator in the response about whether the versions match. Maybe we can add a versionMismatch to the error object if execution fails. Then the Blitz dev can check that field in their error handler if they want.

Notes

  • I'll create another issue for defining how error handling works. I.e. how queries and mutations can return error data to the client.

Form Multipart Management/Resolvers should accept Files

What do you want and why?

In application development to upload a file is a really common thing to do. (Say for a user avatar upload uploading a video etc). Often especially in a serverless context, we then need to stream it on to a third party asset storage solution such as S3 or cloudinary. Some services provide unsigned options but this can lead to security issues. In any case there is a fair bit management required to successfully accept a file in the browser send the file to the server or serverless function parse it to a stream then send it elsewhere. Currently to accomplish this in Blitz we need to create a custom api route and because fetch has no progress event use XmlHTTPRequest or a lib that is based on it to manage the upload process.

On the clientside it might be normal to use something like React Dropzone to get the file ready in the browser or you might use a native file input and assemble it for form submission using the FormData API. In anycase you would manipulate file blobs using the browsers File API.

On the server side You need to use a library such as formidable or busboy to handle parsing the file to a stream and then piping that stream to S3 or Cloudinary or to disk. Really what we need to be able to do is write a mutation that can accept file objects in the browser and receive them as streams on the server.

Possible implementation(s)

The browser File API is a Blob which has a stream method so we might want to have a similar API on the server and provide any files passed as a mutation's argument properties to be re assembled as File-like objects on the Node end.

import uploadFile from 'app/projects/mutations/uploadFile'

const {getRootProps, getInputProps} = useDropzone({onDrop: (acceptedFiles) => {
  // an instant upload in this case
  uploadFile({file:acceptedFiles[0]}, {
    onUploadProgress(event) {
      const uploadedBytes = event.loaded / event.total;
      console.log(`Uploaded ${uploadedBytes} bytes`);
    }
  }) 
}})
export default async function uploadFile({ file }: UploadFileInput) {
  const [uploadStream, donePromise] = getCloudinaryUploadStreamWithDonePromise(file.name)
  file.stream().pipe(uploadStream)
  await donePromise
  return 'ok'
}

To do this we should probably use busboy on the server as that does not do any intermediate file storage. See article

Client RPC Transformation

I'm sure there are many ways we could do this, but here's what I'm thinking:

Overview

Essentially, every usage of a query or mutation inside a React component anywhere in the project should be replaced with a fetch call.

  • Blitz exports an RPC client
  • During build time, the direct query/mutation call is replaced with a call to the rpc client.

App code:

import getProduct from 'app/product/queries/getProduct'

const product = await getProduct({where: {id: 1}})

Built code:

import {rpc} from 'blitz'

const product = await rpc('/api/product/queries/getProduct')({where: {id: 1}})

But query/mutation usage in server side code should not use RPC. It should still use the query/mutation directly

App code:

import {useQuery} from 'blitz'
import getProduct from 'app/product/queries/getProduct'

export const getStaticProps = async context => {
  const product = await getProduct({where: {id: context.params?.id}})
  return {props: {product}}
}

export default function(props) {
  const [product2] = useQuery(getProduct, {where: {id: props.id})
  return (
    <div>
      <h1>{props.product.name}</h1>
    </div>
  )
}

Built code:

import {useQuery, rpc} from 'blitz'
import getProduct from 'app/product/queries/getProduct'

export const getStaticProps = async context => {
  const product = await getProduct({where: {id: context.params?.id}})
  return {props: {product}}
}

export default function(props) {
  const [product2] = useQuery(rpc('/api/product/queries/getProduct'), {where: {id: props.id})
  return (
    <div>
      <h1>{props.product.name}</h1>
    </div>
  )
}

Implementation

I think this is a pretty straightforward babel transformation, right? The only part I'm not sure about is how we can know the file path to the query/mutation from inside the babel transformation. Maybe we first need to compile a manifest file of all queries/mutations that the babel transform can read from.

Upgrade react-query to latest version

What do you want and why?

Upgrade react-query to the latest version.

It's specified in packages/core/package.json


Veterans, please leave for someone new to Blitz/open-source

Enable full dependency resolution in AddDependencyExecutor

What do you want and why?

Currently the AddDependencyExecutor just blindly installs the version specified in the executor's metadata, which isn't exactly what we want - we want a pinned version.

Possible implementation(s)

We should use the same dependency resolution logic we use in the blitz new command to resolve the latest valid version for each dependency based on the version string passed in (that may require pulling that logic out into a utility package for sharing, or just copy/pasting it.

Server Bugs Tracking Issue

As I'm working on coding up the new architecture, I'm discovering a bunch of issues with our server package. So I'm listing them here instead of creating a bunch of independant issues.

  • Recursive path issue .blitz/caches/dev/.blitz/caches/dev
  • Bunch of UnhandledPromiseRejectionWarning errors
  • Rarely detects file changes. When it does, it's quite slow to detect and recompile
  • Top level pages dir should be optional (currently fails w/o it)
  • Should use routes dir instead of pages dir
  • blitz build and blitz start -p both fail
  • app/pages doesn't get copied to the built pages directory

Write tests for generator classes

What do you want and why?

Our current generators are all untested. Since these are such core logic, that's pretty scary. We should write some tests that run the generators and verify that the output file structure and file contents match what we're expecting.

Possible implementation(s)

For file contents we should be able to use jest snapshots, for file structure, simple string matching should suffice.

  • App Generator
  • Model Generator
  • Mutation Generator
  • Page Generator
  • Query Generator

Define Query & Mutation Usage (useQuery, cache invalidation, etc.)

We need to define exactly how queries and mutations are used in Blitz app code.

  • Decide whether to go all in on Suspense (yes)
  • SSR usage (must go through middleware)
  • Intelligent cache invalidation

Requirements

  • Using a query/mutation in server code (like getStaticProps) should directly use the query/mutation. It should not make an RPC call to another lambda
  • Using a query/mutation in client code should use automatically be transformed to make an RPC call to a Lambda (server code is not bundled with client code)
  • Support all the nice features of react-query/swr for auto caching, refetching, stale-while-revalidate semantics, revalidate on window focus, etc.
// routes/product/[id]/edit.tsx
import {useQuery, Router} from 'blitz'
import getProduct from '/app/products/queries/getProduct'
import updateProduct from '/app/products/mutations/updateProduct'
import {Formik} from 'formik'

export default function(props) {
  const [product] = useQuery(getProduct, {where: {id: props.query.id}})

  return (
    <div>
      <h1>{product.name}</h1>
      <Formik
        initialValues={product}
        onSubmit={async values => {
          try {
            const {id} = await updateProduct(values)
            Router.push('/products/[id]', `/products/${id}`)
          } catch (error) {
            alert('Error saving product')
          }
        }}>
        {({handleSubmit}) => <form onSubmit={handleSubmit}></form>}
      </Formik>
    </div>
  )
}

Queries

In a React Component

Blitz provides a useQuery hook. The first argument is a query function. The second argument is the input to the query function.

  • We use react-query under the hood for implementing this hook.
import getProduct from '/app/products/queries/getProduct'

export default function(props: {query: {id: number}}) {
  const [product] = useQuery(getProduct, {where: {id: props.query.id}})
  
  return <div>{product.name}</div>
}

On the Server

In server-side code, a query function can be called directly without useQuery

import getProduct from '/app/products/queries/getProduct'

export const getStaticProps = async context => {
  const product = await getProduct({where: {id: context.params?.id}})
  return {props: {product}}
}

export default function({product}) {
  return <div>{product.name}</div>
}

Cache Key

  • Blitz automatically generates the query cache key
  • Most Blitz users will never need to know about the cache key.
  • For the above query, the cache key would be something like ["/api/products/queries/getProduct", {where: {id: props.query.id}]

React Suspense & Concurrent Mode

Blitz apps have concurrent mode enabled by default, so suspense for data fetching is also our default.

SSR Suspense

Our current plan is to not support SSR for useQuery. We're hoping the official new concurrent mode SSR renderer is out within a few months.

  • We could use a package like react-async-ssr until the official suspense SSR renderer is released
  • Or we can do fancy stuff and preload all the query data into the cache before calling renderToString
  • Or we can just not support SSR for useQuery

Dependent Queries

The second query will not execute until the second function argument can be called without throwing.

const [user] = useQuery(getUser, {where: {id: props.query.id}})
const [products] = useQuery(getProducts, () => ({where: {userId: user.id}}))

Prefetching (not implemented: blitz-js/legacy-framework#821)

  • All queries are automatically cached, so manually calling a query function will cache it's data

Both of the following will cache the getProduct query.

const product = await getProduct({where: {id: props.id}})
<button onMouseEnter={() => getProduct({where: {id: productId}})}>
  View Product
</button>

Pagination

Use the usePaginatedQuery hook

import {Suspense, useState} from 'react'
import {Link, BlitzPage, usePaginatedQuery} from 'blitz'
import getProducts from 'app/products/queries/getProducts'

const ITEMS_PER_PAGE = 3

const Products = () => {
  const [page, setPage] = useState(0)
  const [products] = usePaginatedQuery(getProducts, {
    skip: ITEMS_PER_PAGE * page,
    first: ITEMS_PER_PAGE,
  })

  return (
    <div>
      {products.map((product) => (
        <p key={product.id}>
          <Link href="/products/[handle]" as={`/products/${product.handle}`}>
            <a>{product.name}</a>
          </Link>
        </p>
      ))}
      <button disabled={page === 0} onClick={() => setPage(page - 1)}>
        Previous
      </button>
      <button disabled={products.length !== ITEMS_PER_PAGE} onClick={() => setPage(page + 1)}>
        Next
      </button>
    </div>
  )
}

const Page: BlitzPage = function () {
  return (
    <div>
      <h1>Products - Paginated</h1>
      <Suspense fallback={<div>Loading...</div>}>
        <Products />
      </Suspense>
    </div>
  )
}
export default Page

Infinite Loading (not implemented: blitz-js/legacy-framework#819)

import { useInfiniteQuery } from 'blitz'
import getProducts from '/app/products/queries/getProducts'

function Products(props) {
  const [
    groupedProducts,
    {
      isFetching,
      isFetchingMore,
      fetchMore,
      canFetchMore,
    }
  ] = useInfiniteQuery(
    getProducts, 
    (page = {first: 100, skip: 0}) => ({where: {storeId: props.storeId}, ...page}),
    {
      getFetchMore: (lastGroup, allGroups) => lastGroup.nextPage,
    }
  )

  return (
    <>
      {groupedProducts.map((group, i) => (
        <React.Fragment key={i}>
          {group.products.map(product => (
            <p key={product.id}>{product.name}</p>
          ))}
        </React.Fragment>
      ))}

      <div>
        <button
          onClick={() => fetchMore()}
          disabled={!canFetchMore || isFetchingMore}
        >
          {isFetchingMore
            ? 'Loading more...'
            : canFetchMore
            ? 'Load More'
            : 'Nothing more to load'}
        </button>
      </div>

      <div>{isFetching && !isFetchingMore ? 'Fetching...' : null}</div>
    </>
  )
}

SSR

Queries can be used for SSR with the ssrQuery function. ssrQuery will run the appropriate middleware.

import {ssrQuery} from 'blitz'
import getProduct from '/app/products/queries/getProduct'

export const getServerSideProps = async ({params, req, res}) => {
  const product = await ssrQuery(getProduct, {where: {id: params.id}}, {req, res}))
  return {props: {product}}
}

export default function({product}) {
  return <div>{product.name}</div>
}

Window-Focus Refetching

If a user leaves your application and returns to stale data, you usually want to trigger an update in the background to update any stale queries. Blitz useQuery will do this automatically for you, but there will be an option to disable it.

Optimistic Updates

I think optimistic updates should very rarely be used. The UX for end-users is very tricky to get right with optimistic updates.

Therefore, I think optimistic updates should not have first-class support in Blitz. If users need this, they can use directly use react-query or some other method.

Advanced Features & Config

We accept any react-query config item as the third argument to useQuery.

These include refetchInterval, initialData, retry, retryDelay, cacheTime, etc.

Mutations

Mutations are called directly. There is no useMutation hook like react-query has.

import updateProduct from '/app/products/mutations/updateProduct'

try {
  const product = await updateProduct({name: 'something'})
} catch (error) {
  alert('Error saving product')
}

Cache Invalidation (not implemented)

  1. On Route Transition
  2. refetch
  3. mutate

On Route Transition

  • Router.push and Router.replace accepts a refetchQueries option, that when true, will cause all queries on the destination page to be invalidated.
export default function(props: {query: {id: number}}) {
  const [product, {mutate}] = useQuery(getProduct, {where: {id: props.query.id}})

  return (
    <Formik
      initialValues={product}
      onSubmit={async values => {
        try {
          const {id} = await updateProduct(values)
          Router.push('/products/[id]', `/products/${id}`, {refetchQueries: true})
        } catch (error) {
          alert('Error saving product')
        }
      }}>
      {/* ... */}
    </Formik>
  )
}

refetch

  • useQuery returns a refetch function you can use to trigger a query reload
export default function(props: {query: {id: number}}) {
  const [product, {refetch}] = useQuery(getProduct, {where: {id: props.query.id}})

  return (
    <Formik
      initialValues={product}
      onSubmit={async values => {
        try {
          const product = await updateProduct(values)
          refetch()
        } catch (error) {
          alert('Error saving product')
        }
      }}>
      {/* ... */}
    </Formik>
  )
}

mutate

  • useQuery returns a mutate function you can use to manually update the cache
  • Calling mutate will automatically trigger a refetch for the initial query to ensure it has the correct data
  • mutate will be fully typed for Typescript usage
export default function(props: {query: {id: number}}) {
  const [product, {mutate}] = useQuery(getProduct, {where: {id: props.query.id}})

  return (
    <Formik
      initialValues={product}
      onSubmit={async values => {
        try {
          const product = await updateProduct(values)
          mutate(product)
        } catch (error) {
          alert('Error saving product')
        }
      }}>
      {/* ... */}
    </Formik>
  )
}

Related Blitz Issues

  • Client RPC Transformation: #88
  • Query & Mutation Error Handling: #87

Long unresponsive delay after starting `blitz console`

What is the problem?

There's a several second delay after starting blitz console where it is unresponsive.

The loading spinner appears saying "loading code" which is fine and correct, but after that finishes and the prompt appears, it's frozen.

Maybe there's something being loaded after the spinner is marked as success?

Steps to Reproduce

  1. blitz console
  2. Try to type stuff as soon as the prompt appears

Versions

0.12.0

Add a spinner during `blitz new` when prettier is running

What do you want and why?

Currently there's a awkward pause during blitz new after dependencies are installed. It makes the user thing something is broken.

However, it's just prettier running. So we need to add a spinner so the user knows what is happening.

Possible implementation(s)

Right here is the code where prettier runs

You need to add something like this:

const spinner = log.spinner(log.withBrand('Formatting your code')).start()

// on success
spinner.succeed()

// on error
spinner.fail(chalk.yellow.bold("We had an error running Prettier, but don't worry your app will still run fine :)"))

Veterans, please leave this for someone new to Blitz or open-source :)

Implement `useQuery` cache invalidation on manual route change

Note: I'm not 100% sure about adding this feature, but I wanted to open an issue for it so we can have a discussion about it.

I think the mutate() function (#582) might be better for most cases so that the cache is instantly updated with latest data instead of being totally cleared.

What do you want and why?

It's a very common pattern to have an edit form on /[id]/new or /[id]/edit. And after a successful submission, the user is redirected to /[id] or /index.

This feature enables automatic cache invalidation in that case so the user doesn't have to manually call refetch() or mutate().

  • Router.push and Router.replace accepts a invalidateQueries option, that when true, will cause all queries on the destination page to be invalidated so that stale data will not display.
  • Must also work with useRouter() and withRouter()
import {Router} from 'blitz'

export default function (props: {query: {id: number}}) {
  const [product] = useQuery(getProduct, {where: {id: props.query.id}})

  return (
    <Formik
      initialValues={product}
      onSubmit={async (values) => {
        try {
          const {id} = await updateProduct(values)
          Router.push("/products/[id]", `/products/${id}`, {invalidateQueries: true})
        } catch (error) {
          alert("Error saving product")
        }
      }}>
      {/* ... */}
    </Formik>
  )
}

Possible implementation

We need to add custom wrappers around Router, useRouter and withRouter that Next provides.

Conflict checker can't be disabled

What do you want and why do you want it?.
Currently, modifying existing files in the CLI always triggers the conflict checker. This means that generation is restricted to new files, as any line additions to existing files will require manual resolution by the user. We should have a way to temporarily disable the conflict checker for generation commands that will be manually appending to files

Possible implementation(s)
I see two avenues.

  1. We can create an UnsafeGenerator abstract class, which will be extended by Generator. Generators that are yeoman generators will continue to inherit from Generator, and generators that need to manually write output to existing files can use UnsafeGenerator
  2. We can add a method to the ConflictChecker to disable its checks temporarily. Generator can expose disableConflictChecker and enableConfictChecker methods which subclasses can call inline to disable the checker. This has the added benefit of a more explicit API, but it means the barrier to disabling the conflict checker is much lower, which I'm not sure we want.

Unsupported blitz command prints error instead of help message

The below should print the blitz help information, saying that this command is not supported.

~/c/b/e/store> yarn blitz hacker
yarn run v1.19.1
$ /Users/b/c/blitz/node_modules/.bin/blitz hacker
DEBUG: monorepo
Path: /Users/b/c/blitz/node_modules/@blitzjs/cli
(node:87628) TypeError Plugin: @blitzjs/cli: Cannot read property 'dim' of undefined
module: @oclif/[email protected]
task: runHook command_not_found
plugin: @blitzjs/cli
root: /Users/b/c/blitz/packages/cli
See more details with DEBUG=*
 ›   Error: command hacker not found
error Command failed with exit code 2.

Add ability to run blitz start production without building code

What do you want and why?

Services like render have different memory limits for build vs run step. So we need build the code in the build step and then only run the code in the run step. But currently blitz start --production rebuilds the code even if the code is already built.

Possible implementation(s)

We could add a new command or command argument.

Or

We can change blitz start --production to use the built assets if they exist. We would add some type of checksum that makes it easy to verify that the code has not changed since the code was built. If the code has changed, then we run a new build.

I really think we should do the later.

cc @ryardley thoughts on this?

`blitz console` is slow to load - switch to esbuild loader

What is the problem?

Starting blitz console has a long hangup time after "Loading your Code" is finished and before you can start typing.

It gets much worse as you add more queries and mutations.

Steps to Reproduce

  1. blitz new myapp
  2. cd myapp
  3. blitz g resource project
  4. blitz g resource task
  5. blitz g resource comment
  6. blitz db migrate
  7. blitz console
  8. Try to start typing immediately once the > prompt appears. Notice that you can't type for a bit of time.

Other

The main problem is loadBlitz, which for some reason is super slow on first start. Running .reload after initial start (which re-runs loadBlitz) is quite fast.

You can add a log statement here and see how slow it is for each module.

We are already using a lazy require loader which apparently isn't working here.

Probably we need to add a proxy object for each dependency, that only loads the module when it's accessed. But maybe there's a better way.

Update April 2021

See #1098 (comment)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.