Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[cli] merge all pull file creation into one path #544

Merged
merged 4 commits into from
Nov 26, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 84 additions & 168 deletions client/packages/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
getInstallCommand,
} from "./src/util/packageManager.js";
import { pathExists, readJsonFile } from "./src/util/fs.js";
import prettier from 'prettier';
import prettier from "prettier";

const execAsync = promisify(exec);

Expand Down Expand Up @@ -50,16 +50,6 @@ const potentialEnvs = {
vite: "VITE_INSTANT_APP_ID",
};

const noAppIdErrorMessage = `
Couldn't find an app ID.

You can either:
a. Set ${chalk.green("`INSTANT_APP_ID`")} in your .env file. [1]
b. Or provide an app ID via the CLI: ${chalk.green("`instant-cli push|pull -a <app-id>`")}.

[1] Alternatively, If you have ${chalk.green("`NEXT_PUBLIC_INSTANT_APP_ID`")}, ${chalk.green("`VITE_INSTANT_APP_ID`")}, we can detect those too!
`.trim();

const instantDashOrigin = dev
? "http://localhost:3000"
: "https://instantdb.com";
Expand Down Expand Up @@ -393,8 +383,9 @@ program.parse(process.argv);

// command actions
async function handlePush(bag, opts) {
const { ok, appId } = await detectOrCreateAppWithErrorLogging(opts);
const { ok, appId, source } = await detectOrCreateAppWithErrorLogging(opts);
if (!ok) return;
printDotEnvInfo(source, appId);
await push(bag, appId, opts);
}

Expand All @@ -408,17 +399,29 @@ async function push(bag, appId, opts) {
}
}

function printDotEnvInfo(source, appId) {
if (source === "imported" || source === "created") {
console.log(`\nPicked app ${chalk.green(appId)}!\n`);
console.log(
`To use this app automatically from now on, update your ${chalk.green("`.env`")} file:`,
);
const { catchall, ...rest } = potentialEnvs;
console.log(` ${chalk.green(catchall)}=${appId}`);
const otherEnvs = Object.values(rest);
otherEnvs.sort();
const otherEnvStr = otherEnvs.map((x) => " " + chalk.green(x)).join("\n");
console.log(`Alternative names: \n${otherEnvStr}`);
console.log(terminalLink("Dashboard", appDashUrl(appId)));
}
}

async function handlePull(bag, opts) {
const pkgAndAuthInfo = await resolvePackageAndAuthInfoWithErrorLogging();
if (!pkgAndAuthInfo) return;
const { ok, appId, appTitle, isCreated } =
await detectOrCreateAppWithErrorLogging(opts);
const { ok, appId, source } = await detectOrCreateAppWithErrorLogging(opts);
if (!ok) return;
if (isCreated) {
await handleCreatedApp(pkgAndAuthInfo, appId, appTitle);
} else {
await pull(bag, appId, pkgAndAuthInfo);
}
printDotEnvInfo(source, appId);
await pull(bag, appId, pkgAndAuthInfo);
}

async function pull(bag, appId, pkgAndAuthInfo) {
Expand Down Expand Up @@ -542,7 +545,7 @@ async function promptCreateApp() {
ok: true,
appId: id,
appTitle: title,
isCreated: true,
source: "created",
};
}

Expand All @@ -564,29 +567,32 @@ async function promptImportAppOrCreateApp() {
if (!ok) return { ok: false };
return await promptCreateApp();
}

apps.sort((a, b) => +new Date(b.created_at) - +new Date(a.created_at));
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small change, so including here:

Sorting apps by created at desc, so your newly created apps show first


const choice = await select({
message: "Which app would you like to import?",
choices: res.data.apps.map((app) => {
return { name: `${app.title} (${app.id})`, value: app.id };
}),
}).catch(() => null);
if (!choice) return { ok: false };
return { ok: true, appId: choice };
return { ok: true, appId: choice, source: "imported" };
}

async function detectOrCreateAppWithErrorLogging(opts) {
const fromOpts = await detectAppIdFromOptsWithErrorLogging(opts);
if (!fromOpts.ok) return fromOpts;
if (fromOpts.appId) {
return { ok: true, appId: fromOpts.appId };
return { ok: true, appId: fromOpts.appId, source: "opts" };
}

const fromEnv = detectAppIdFromEnvWithErrorLogging();
if (!fromEnv.ok) return fromEnv;
if (fromEnv.found) {
const { envName, value } = fromEnv.found;
console.log(`Found ${chalk.green(envName)}: ${value}`);
return { ok: true, appId: value };
return { ok: true, appId: value, source: "env" };
}

const action = await select({
Expand All @@ -604,47 +610,15 @@ async function detectOrCreateAppWithErrorLogging(opts) {
return await promptImportAppOrCreateApp();
}

async function writeTypescript(path, content, encoding) {
async function writeTypescript(path, content, encoding) {
const prettierConfig = await prettier.resolveConfig(path);
const formattedCode = await prettier.format(content, {
...prettierConfig,
parser: 'typescript',
parser: "typescript",
});
return await writeFile(path, formattedCode, encoding);
}

async function handleCreatedApp(
{ pkgDir, instantModuleName },
appId,
appTitle,
) {
const schema = await readLocalSchemaFile();
const { perms } = await readLocalPermsFile();

console.log(chalk.green(`Successfully created your Instant app "${appId}"`));
console.log(`Please add your app ID to your .env config:`);
console.log(chalk.magenta(`INSTANT_APP_ID=${appId}`));
console.log(terminalLink("Dashboard", appDashUrl(appId)));

if (!schema) {
const schemaPath = join(pkgDir, "instant.schema.ts");
await writeTypescript(
schemaPath,
instantSchemaTmpl(appTitle, appId, instantModuleName),
"utf-8",
);
console.log("Start building your schema: " + schemaPath);
}

if (!perms) {
await writeTypescript(
join(pkgDir, "instant.perms.ts"),
examplePermsTmpl,
"utf-8",
);
}
}

async function getInstantModuleName(pkgJson) {
const deps = pkgJson.dependencies || {};
const instantModuleName = [
Expand Down Expand Up @@ -756,11 +730,6 @@ async function pullPerms(appId, { pkgDir }) {

if (!pullRes.ok) return;

if (!pullRes.data.perms || !countEntities(pullRes.data.perms)) {
console.log("No perms. Exiting.");
return;
}

if (await pathExists(join(pkgDir, "instant.perms.ts"))) {
const ok = await promptOk(
"This will ovwerwrite your local instant.perms file, OK to proceed?",
Expand All @@ -772,7 +741,7 @@ async function pullPerms(appId, { pkgDir }) {
const permsPath = join(pkgDir, "instant.perms.ts");
await writeTypescript(
permsPath,
`export default ${JSON.stringify(pullRes.data.perms, null, " ")};`,
generatePermsTypescriptFile(pullRes.data.perms || {}),
"utf-8",
);

Expand Down Expand Up @@ -999,7 +968,7 @@ async function pushSchema(appId, opts) {
if (!planRes.ok) return;

if (!planRes.data.steps.length) {
console.log("No schema changes detected. Exiting.");
console.log("No schema changes detected. Exiting.");
return;
}

Expand Down Expand Up @@ -1141,7 +1110,7 @@ async function waitForAuthToken({ secret }) {
if (authCheckRes.data?.hint.errors?.[0]?.issue === "waiting-for-user") {
continue;
}
error('Failed to authenticate ');
error("Failed to authenticate ");
prettyPrintJSONErr(authCheckRes.data);
return;
}
Expand Down Expand Up @@ -1487,116 +1456,42 @@ function detectAppIdFromEnvWithErrorLogging() {
return { ok: true, found };
}

async function getAppIdWithErrorLogging(arg) {
const fromArg = await detectAppIdFromOptsWithErrorLogging({
app: arg,
});
if (!fromArg.ok) return;
if (fromArg.appId) {
return fromArg.appId;
}
const fromEnv = detectAppIdFromEnvWithErrorLogging();
if (!fromEnv.ok) return;
if (fromEnv.found) {
const { envName, value } = fromEnv.found;
console.log(`Found ${chalk.green(envName)}: ${value}`);
return value;
}
// otherwise, instruct the user to set one of these up
error(noAppIdErrorMessage);

return;
}

function appDashUrl(id) {
return `${instantDashOrigin}/dash?s=main&t=home&app=${id}`;
}

function instantSchemaTmpl(title, id, instantModuleName) {
return /* ts */ `// ${title}
// ${appDashUrl(id)}

import { i } from "${instantModuleName ?? "@instantdb/core"}";

// Example entities and links (you can delete these!)
const graph = i.graph(
{
posts: i.entity({
name: i.string(),
content: i.string(),
}),
authors: i.entity({
userId: i.string(),
name: i.string(),
}),
tags: i.entity({
label: i.string(),
}),
},
{
authorPosts: {
forward: {
on: "authors",
has: "many",
label: "posts",
},
reverse: {
on: "posts",
has: "one",
label: "author",
},
},
postsTags: {
forward: {
on: "posts",
has: "many",
label: "tags",
},
reverse: {
on: "tags",
has: "many",
label: "posts",
},
},
},
);
function generatePermsTypescriptFile(perms) {
const rulesTxt = Object.keys(perms).length
? JSON.stringify(perms, null, 2)
: `
{
/**
* Welcome to Instant's permission system!
* Right now your rules are empty. To start filling them in, check out the docs:
* https://www.instantdb.com/docs/permissions
*
* Here's an example to give you a feel:
* posts: {
* allow: {
* view: "true",
* create: "isOwner",
* update: "isOwner",
* delete: "isOwner",
* },
* bind: ["isOwner", "data.creator == auth.uid"],
* },
*/
};
`.trim();
return `
// Docs: https://www.instantdb.com/docs/permissions

const rules = ${rulesTxt};

export default graph;
`;
export default rules;
`.trim();
}

const examplePermsTmpl = /* ts */ `export default {
authors: {
bind: ["isAuthor", "auth.id == data.userId"],
allow: {
view: "true",
create: "isAuthor",
update: "isAuthor",
delete: "isAuthor",
},
},
posts: {
bind: ["isAuthor", "auth.id in data.ref('authors.userId')"],
allow: {
view: "true",
create: "isAuthor",
update: "isAuthor",
delete: "isAuthor",
},
},
tags: {
bind: ["isOwner", "auth.id in data.ref('posts.authors.userId')"],
allow: {
view: "true",
create: "isOwner",
update: "isOwner",
delete: "isOwner",
},
},
};
`;

function generateSchemaTypescriptFile(id, schema, title, instantModuleName) {
const entitiesEntriesCode = sortedEntries(schema.blobs)
.map(([name, attrs]) => {
Expand Down Expand Up @@ -1658,13 +1553,34 @@ function generateSchemaTypescriptFile(id, schema, title, instantModuleName) {
}),
);

return `// ${title}
return `
// ${appDashUrl(id)}
// Docs: https://www.instantdb.com/docs/schema

import { i } from "${instantModuleName ?? "@instantdb/core"}";

const graph = i.graph(
${
Object.keys(schema.blobs).length === 1 &&
Object.keys(schema.blobs)[0] === "$users"
? `
// This section lets you define entities: think \`posts\`, \`comments\`, etc
// Take a look at the docs to learn more:
// https://www.instantdb.com/docs/schema#defining-entities
`.trim()
: ""
}
${indentLines(entitiesObjCode, 1)},
${
Object.keys(schema.refs).length === 0
? `
// You can define links here.
// For example, if \`posts\` should have many \`comments\`.
// More in the docs:
// https://www.instantdb.com/docs/schema#defining-links
`.trim()
: ""
}
${indentLines(JSON.stringify(linksEntriesCode, null, " "), 1)}
);

Expand Down