instant-cli 0.22.156 → 0.22.157-branch-cli-query.22963745357.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/__tests__/e2e/cli.e2e.test.ts +523 -1
- package/__tests__/e2e/helpers.ts +44 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +75 -1
- package/dist/index.js.map +1 -1
- package/package.json +5 -4
- package/src/index.js +88 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { randomUUID } from 'crypto';
|
|
2
3
|
import { readFile } from 'fs/promises';
|
|
3
4
|
import { join } from 'path';
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
runCli,
|
|
7
|
+
createTestProject,
|
|
8
|
+
createTempApp,
|
|
9
|
+
adminTransact,
|
|
10
|
+
createAppUser,
|
|
11
|
+
} from './helpers';
|
|
5
12
|
|
|
6
13
|
const SCHEMA_FILE = `
|
|
7
14
|
import { i } from "@instantdb/core";
|
|
@@ -494,6 +501,521 @@ export default _schema;
|
|
|
494
501
|
});
|
|
495
502
|
});
|
|
496
503
|
|
|
504
|
+
describe('query', () => {
|
|
505
|
+
it('queries data from an app', async () => {
|
|
506
|
+
const { appId, adminToken } = await createTempApp();
|
|
507
|
+
const project = await createTestProject({
|
|
508
|
+
appId,
|
|
509
|
+
schemaFile: SCHEMA_FILE,
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
try {
|
|
513
|
+
const pushResult = await runCli(['push', 'schema', '--yes'], {
|
|
514
|
+
cwd: project.dir,
|
|
515
|
+
env: {
|
|
516
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
517
|
+
INSTANT_APP_ID: appId,
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
expect(pushResult.exitCode).toBe(0);
|
|
521
|
+
|
|
522
|
+
await adminTransact(appId, adminToken, [
|
|
523
|
+
['update', 'posts', randomUUID(), { title: 'Hello', body: 'World' }],
|
|
524
|
+
['update', 'posts', randomUUID(), { title: 'Second', body: 'Post' }],
|
|
525
|
+
]);
|
|
526
|
+
|
|
527
|
+
const result = await runCli(
|
|
528
|
+
['query', '--admin', JSON.stringify({ posts: {} })],
|
|
529
|
+
{
|
|
530
|
+
cwd: project.dir,
|
|
531
|
+
env: {
|
|
532
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
533
|
+
INSTANT_APP_ID: appId,
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
expect(result.exitCode).toBe(0);
|
|
539
|
+
const data = JSON.parse(result.stdout);
|
|
540
|
+
expect(data.posts).toHaveLength(2);
|
|
541
|
+
expect(data.posts.map((p: any) => p.title).sort()).toEqual([
|
|
542
|
+
'Hello',
|
|
543
|
+
'Second',
|
|
544
|
+
]);
|
|
545
|
+
} finally {
|
|
546
|
+
await project.cleanup();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('queries with filters', async () => {
|
|
551
|
+
const { appId, adminToken } = await createTempApp();
|
|
552
|
+
const project = await createTestProject({
|
|
553
|
+
appId,
|
|
554
|
+
schemaFile: SCHEMA_FILE,
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
try {
|
|
558
|
+
const pushResult = await runCli(['push', 'schema', '--yes'], {
|
|
559
|
+
cwd: project.dir,
|
|
560
|
+
env: {
|
|
561
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
562
|
+
INSTANT_APP_ID: appId,
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
expect(pushResult.exitCode).toBe(0);
|
|
566
|
+
|
|
567
|
+
await adminTransact(appId, adminToken, [
|
|
568
|
+
['update', 'posts', randomUUID(), { title: 'Match', body: 'Yes' }],
|
|
569
|
+
['update', 'posts', randomUUID(), { title: 'Nope', body: 'No' }],
|
|
570
|
+
]);
|
|
571
|
+
|
|
572
|
+
const query = {
|
|
573
|
+
posts: { $: { where: { title: 'Match' } } },
|
|
574
|
+
};
|
|
575
|
+
const result = await runCli(
|
|
576
|
+
['query', '--admin', JSON.stringify(query)],
|
|
577
|
+
{
|
|
578
|
+
cwd: project.dir,
|
|
579
|
+
env: {
|
|
580
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
581
|
+
INSTANT_APP_ID: appId,
|
|
582
|
+
},
|
|
583
|
+
},
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
expect(result.exitCode).toBe(0);
|
|
587
|
+
const data = JSON.parse(result.stdout);
|
|
588
|
+
expect(data.posts).toHaveLength(1);
|
|
589
|
+
expect(data.posts[0].title).toBe('Match');
|
|
590
|
+
} finally {
|
|
591
|
+
await project.cleanup();
|
|
592
|
+
}
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it('returns empty arrays for entities with no data', async () => {
|
|
596
|
+
const { appId, adminToken } = await createTempApp();
|
|
597
|
+
const project = await createTestProject({
|
|
598
|
+
appId,
|
|
599
|
+
schemaFile: SCHEMA_FILE,
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const pushResult = await runCli(['push', 'schema', '--yes'], {
|
|
604
|
+
cwd: project.dir,
|
|
605
|
+
env: {
|
|
606
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
607
|
+
INSTANT_APP_ID: appId,
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
expect(pushResult.exitCode).toBe(0);
|
|
611
|
+
|
|
612
|
+
const result = await runCli(
|
|
613
|
+
['query', '--admin', JSON.stringify({ posts: {} })],
|
|
614
|
+
{
|
|
615
|
+
cwd: project.dir,
|
|
616
|
+
env: {
|
|
617
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
618
|
+
INSTANT_APP_ID: appId,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
expect(result.exitCode).toBe(0);
|
|
624
|
+
const data = JSON.parse(result.stdout);
|
|
625
|
+
expect(data.posts).toEqual([]);
|
|
626
|
+
} finally {
|
|
627
|
+
await project.cleanup();
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
it('works with --app flag', async () => {
|
|
632
|
+
const { appId, adminToken } = await createTempApp();
|
|
633
|
+
const project = await createTestProject({
|
|
634
|
+
schemaFile: SCHEMA_FILE,
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
try {
|
|
638
|
+
const pushResult = await runCli(
|
|
639
|
+
['push', 'schema', '--app', appId, '--yes'],
|
|
640
|
+
{
|
|
641
|
+
cwd: project.dir,
|
|
642
|
+
env: { INSTANT_CLI_AUTH_TOKEN: adminToken },
|
|
643
|
+
},
|
|
644
|
+
);
|
|
645
|
+
expect(pushResult.exitCode).toBe(0);
|
|
646
|
+
|
|
647
|
+
const result = await runCli(
|
|
648
|
+
['query', '--admin', '--app', appId, JSON.stringify({ posts: {} })],
|
|
649
|
+
{
|
|
650
|
+
cwd: project.dir,
|
|
651
|
+
env: { INSTANT_CLI_AUTH_TOKEN: adminToken },
|
|
652
|
+
},
|
|
653
|
+
);
|
|
654
|
+
|
|
655
|
+
expect(result.exitCode).toBe(0);
|
|
656
|
+
const data = JSON.parse(result.stdout);
|
|
657
|
+
expect(data.posts).toEqual([]);
|
|
658
|
+
} finally {
|
|
659
|
+
await project.cleanup();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
it('fails with invalid JSON', async () => {
|
|
664
|
+
const { appId, adminToken } = await createTempApp();
|
|
665
|
+
const project = await createTestProject({ appId });
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const result = await runCli(['query', '--admin', 'not valid json'], {
|
|
669
|
+
cwd: project.dir,
|
|
670
|
+
env: {
|
|
671
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
672
|
+
INSTANT_APP_ID: appId,
|
|
673
|
+
},
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
expect(result.exitCode).not.toBe(0);
|
|
677
|
+
} finally {
|
|
678
|
+
await project.cleanup();
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
it('fails without context flag', async () => {
|
|
683
|
+
const { appId, adminToken } = await createTempApp();
|
|
684
|
+
const project = await createTestProject({ appId });
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const result = await runCli(['query', JSON.stringify({ posts: {} })], {
|
|
688
|
+
cwd: project.dir,
|
|
689
|
+
env: {
|
|
690
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
691
|
+
INSTANT_APP_ID: appId,
|
|
692
|
+
},
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
expect(result.exitCode).not.toBe(0);
|
|
696
|
+
const output = result.stdout + result.stderr;
|
|
697
|
+
expect(output).toMatch(/--admin|--as-email|--as-guest/);
|
|
698
|
+
} finally {
|
|
699
|
+
await project.cleanup();
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('fails with multiple context flags', async () => {
|
|
704
|
+
const { appId, adminToken } = await createTempApp();
|
|
705
|
+
const project = await createTestProject({ appId });
|
|
706
|
+
|
|
707
|
+
try {
|
|
708
|
+
const result = await runCli(
|
|
709
|
+
[
|
|
710
|
+
'query',
|
|
711
|
+
'--as-email',
|
|
712
|
+
'alice@example.com',
|
|
713
|
+
'--as-guest',
|
|
714
|
+
JSON.stringify({ posts: {} }),
|
|
715
|
+
],
|
|
716
|
+
{
|
|
717
|
+
cwd: project.dir,
|
|
718
|
+
env: {
|
|
719
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
720
|
+
INSTANT_APP_ID: appId,
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
);
|
|
724
|
+
|
|
725
|
+
expect(result.exitCode).not.toBe(0);
|
|
726
|
+
const output = result.stdout + result.stderr;
|
|
727
|
+
expect(output).toMatch(/exactly one context/);
|
|
728
|
+
} finally {
|
|
729
|
+
await project.cleanup();
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
it('fails without auth', async () => {
|
|
734
|
+
const { appId } = await createTempApp();
|
|
735
|
+
const project = await createTestProject({ appId });
|
|
736
|
+
|
|
737
|
+
try {
|
|
738
|
+
const result = await runCli(
|
|
739
|
+
['query', '--admin', JSON.stringify({ posts: {} })],
|
|
740
|
+
{
|
|
741
|
+
cwd: project.dir,
|
|
742
|
+
env: {
|
|
743
|
+
INSTANT_CLI_AUTH_TOKEN: '',
|
|
744
|
+
INSTANT_APP_ADMIN_TOKEN: '',
|
|
745
|
+
INSTANT_ADMIN_TOKEN: '',
|
|
746
|
+
INSTANT_APP_ID: appId,
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
);
|
|
750
|
+
|
|
751
|
+
expect(result.exitCode).not.toBe(0);
|
|
752
|
+
const output = result.stdout + result.stderr;
|
|
753
|
+
expect(output).toMatch(/not logged in/i);
|
|
754
|
+
} finally {
|
|
755
|
+
await project.cleanup();
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('queries nested associations', async () => {
|
|
760
|
+
const { appId, adminToken } = await createTempApp();
|
|
761
|
+
const project = await createTestProject({
|
|
762
|
+
appId,
|
|
763
|
+
schemaFile: SCHEMA_FILE,
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
try {
|
|
767
|
+
const pushResult = await runCli(['push', 'schema', '--yes'], {
|
|
768
|
+
cwd: project.dir,
|
|
769
|
+
env: {
|
|
770
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
771
|
+
INSTANT_APP_ID: appId,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
expect(pushResult.exitCode).toBe(0);
|
|
775
|
+
|
|
776
|
+
const postId = randomUUID();
|
|
777
|
+
const commentId = randomUUID();
|
|
778
|
+
await adminTransact(appId, adminToken, [
|
|
779
|
+
['update', 'posts', postId, { title: 'My Post', body: 'Content' }],
|
|
780
|
+
['update', 'comments', commentId, { text: 'Great post!' }],
|
|
781
|
+
['link', 'posts', postId, { comments: commentId }],
|
|
782
|
+
]);
|
|
783
|
+
|
|
784
|
+
const result = await runCli(
|
|
785
|
+
['query', '--admin', JSON.stringify({ posts: { comments: {} } })],
|
|
786
|
+
{
|
|
787
|
+
cwd: project.dir,
|
|
788
|
+
env: {
|
|
789
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
790
|
+
INSTANT_APP_ID: appId,
|
|
791
|
+
},
|
|
792
|
+
},
|
|
793
|
+
);
|
|
794
|
+
|
|
795
|
+
expect(result.exitCode).toBe(0);
|
|
796
|
+
const data = JSON.parse(result.stdout);
|
|
797
|
+
expect(data.posts).toHaveLength(1);
|
|
798
|
+
expect(data.posts[0].comments).toHaveLength(1);
|
|
799
|
+
expect(data.posts[0].comments[0].text).toBe('Great post!');
|
|
800
|
+
} finally {
|
|
801
|
+
await project.cleanup();
|
|
802
|
+
}
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it('--as-email runs query as a specific user with perms applied', async () => {
|
|
806
|
+
const { appId, adminToken } = await createTempApp();
|
|
807
|
+
|
|
808
|
+
const schemaWithCreator = `
|
|
809
|
+
import { i } from "@instantdb/core";
|
|
810
|
+
const _schema = i.schema({
|
|
811
|
+
entities: {
|
|
812
|
+
posts: i.entity({
|
|
813
|
+
title: i.string(),
|
|
814
|
+
creatorEmail: i.string(),
|
|
815
|
+
}),
|
|
816
|
+
},
|
|
817
|
+
});
|
|
818
|
+
export default _schema;
|
|
819
|
+
`;
|
|
820
|
+
|
|
821
|
+
// Only allow users to view posts they created
|
|
822
|
+
const restrictedPerms = `export default {
|
|
823
|
+
posts: {
|
|
824
|
+
allow: {
|
|
825
|
+
view: "auth.email == data.creatorEmail",
|
|
826
|
+
},
|
|
827
|
+
},
|
|
828
|
+
};
|
|
829
|
+
`;
|
|
830
|
+
|
|
831
|
+
const project = await createTestProject({
|
|
832
|
+
appId,
|
|
833
|
+
schemaFile: schemaWithCreator,
|
|
834
|
+
permsFile: restrictedPerms,
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
try {
|
|
838
|
+
const pushResult = await runCli(['push', '--yes'], {
|
|
839
|
+
cwd: project.dir,
|
|
840
|
+
env: {
|
|
841
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
842
|
+
INSTANT_APP_ID: appId,
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
expect(pushResult.exitCode).toBe(0);
|
|
846
|
+
|
|
847
|
+
await createAppUser(appId, adminToken, 'alice@test.com');
|
|
848
|
+
await createAppUser(appId, adminToken, 'bob@test.com');
|
|
849
|
+
|
|
850
|
+
await adminTransact(appId, adminToken, [
|
|
851
|
+
[
|
|
852
|
+
'update',
|
|
853
|
+
'posts',
|
|
854
|
+
randomUUID(),
|
|
855
|
+
{ title: "Alice's Post", creatorEmail: 'alice@test.com' },
|
|
856
|
+
],
|
|
857
|
+
[
|
|
858
|
+
'update',
|
|
859
|
+
'posts',
|
|
860
|
+
randomUUID(),
|
|
861
|
+
{ title: "Bob's Post", creatorEmail: 'bob@test.com' },
|
|
862
|
+
],
|
|
863
|
+
]);
|
|
864
|
+
|
|
865
|
+
// Admin sees all posts
|
|
866
|
+
const adminResult = await runCli(
|
|
867
|
+
['query', '--admin', JSON.stringify({ posts: {} })],
|
|
868
|
+
{
|
|
869
|
+
cwd: project.dir,
|
|
870
|
+
env: {
|
|
871
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
872
|
+
INSTANT_APP_ID: appId,
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
);
|
|
876
|
+
expect(adminResult.exitCode).toBe(0);
|
|
877
|
+
const adminData = JSON.parse(adminResult.stdout);
|
|
878
|
+
expect(adminData.posts).toHaveLength(2);
|
|
879
|
+
|
|
880
|
+
// Alice only sees her post
|
|
881
|
+
const aliceResult = await runCli(
|
|
882
|
+
[
|
|
883
|
+
'query',
|
|
884
|
+
'--as-email',
|
|
885
|
+
'alice@test.com',
|
|
886
|
+
JSON.stringify({ posts: {} }),
|
|
887
|
+
],
|
|
888
|
+
{
|
|
889
|
+
cwd: project.dir,
|
|
890
|
+
env: {
|
|
891
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
892
|
+
INSTANT_APP_ID: appId,
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
);
|
|
896
|
+
expect(aliceResult.exitCode).toBe(0);
|
|
897
|
+
const aliceData = JSON.parse(aliceResult.stdout);
|
|
898
|
+
expect(aliceData.posts).toHaveLength(1);
|
|
899
|
+
expect(aliceData.posts[0].title).toBe("Alice's Post");
|
|
900
|
+
|
|
901
|
+
// Bob only sees his post
|
|
902
|
+
const bobResult = await runCli(
|
|
903
|
+
[
|
|
904
|
+
'query',
|
|
905
|
+
'--as-email',
|
|
906
|
+
'bob@test.com',
|
|
907
|
+
JSON.stringify({ posts: {} }),
|
|
908
|
+
],
|
|
909
|
+
{
|
|
910
|
+
cwd: project.dir,
|
|
911
|
+
env: {
|
|
912
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
913
|
+
INSTANT_APP_ID: appId,
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
);
|
|
917
|
+
expect(bobResult.exitCode).toBe(0);
|
|
918
|
+
const bobData = JSON.parse(bobResult.stdout);
|
|
919
|
+
expect(bobData.posts).toHaveLength(1);
|
|
920
|
+
expect(bobData.posts[0].title).toBe("Bob's Post");
|
|
921
|
+
} finally {
|
|
922
|
+
await project.cleanup();
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('--as-guest runs query as unauthenticated user', async () => {
|
|
927
|
+
const { appId, adminToken } = await createTempApp();
|
|
928
|
+
|
|
929
|
+
// Guests can only see posts marked as public
|
|
930
|
+
const schemaWithVisibility = `
|
|
931
|
+
import { i } from "@instantdb/core";
|
|
932
|
+
const _schema = i.schema({
|
|
933
|
+
entities: {
|
|
934
|
+
posts: i.entity({
|
|
935
|
+
title: i.string(),
|
|
936
|
+
isPublic: i.boolean(),
|
|
937
|
+
}),
|
|
938
|
+
},
|
|
939
|
+
});
|
|
940
|
+
export default _schema;
|
|
941
|
+
`;
|
|
942
|
+
|
|
943
|
+
const guestPerms = `export default {
|
|
944
|
+
posts: {
|
|
945
|
+
allow: {
|
|
946
|
+
view: "data.isPublic == true",
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
};
|
|
950
|
+
`;
|
|
951
|
+
|
|
952
|
+
const project = await createTestProject({
|
|
953
|
+
appId,
|
|
954
|
+
schemaFile: schemaWithVisibility,
|
|
955
|
+
permsFile: guestPerms,
|
|
956
|
+
});
|
|
957
|
+
|
|
958
|
+
try {
|
|
959
|
+
const pushResult = await runCli(['push', '--yes'], {
|
|
960
|
+
cwd: project.dir,
|
|
961
|
+
env: {
|
|
962
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
963
|
+
INSTANT_APP_ID: appId,
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
expect(pushResult.exitCode).toBe(0);
|
|
967
|
+
|
|
968
|
+
await adminTransact(appId, adminToken, [
|
|
969
|
+
[
|
|
970
|
+
'update',
|
|
971
|
+
'posts',
|
|
972
|
+
randomUUID(),
|
|
973
|
+
{ title: 'Public Post', isPublic: true },
|
|
974
|
+
],
|
|
975
|
+
[
|
|
976
|
+
'update',
|
|
977
|
+
'posts',
|
|
978
|
+
randomUUID(),
|
|
979
|
+
{ title: 'Private Post', isPublic: false },
|
|
980
|
+
],
|
|
981
|
+
]);
|
|
982
|
+
|
|
983
|
+
// Admin sees both
|
|
984
|
+
const adminResult = await runCli(
|
|
985
|
+
['query', '--admin', JSON.stringify({ posts: {} })],
|
|
986
|
+
{
|
|
987
|
+
cwd: project.dir,
|
|
988
|
+
env: {
|
|
989
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
990
|
+
INSTANT_APP_ID: appId,
|
|
991
|
+
},
|
|
992
|
+
},
|
|
993
|
+
);
|
|
994
|
+
expect(adminResult.exitCode).toBe(0);
|
|
995
|
+
const adminData = JSON.parse(adminResult.stdout);
|
|
996
|
+
expect(adminData.posts).toHaveLength(2);
|
|
997
|
+
|
|
998
|
+
// Guest only sees public post
|
|
999
|
+
const guestResult = await runCli(
|
|
1000
|
+
['query', '--as-guest', JSON.stringify({ posts: {} })],
|
|
1001
|
+
{
|
|
1002
|
+
cwd: project.dir,
|
|
1003
|
+
env: {
|
|
1004
|
+
INSTANT_CLI_AUTH_TOKEN: adminToken,
|
|
1005
|
+
INSTANT_APP_ID: appId,
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
);
|
|
1009
|
+
expect(guestResult.exitCode).toBe(0);
|
|
1010
|
+
const guestData = JSON.parse(guestResult.stdout);
|
|
1011
|
+
expect(guestData.posts).toHaveLength(1);
|
|
1012
|
+
expect(guestData.posts[0].title).toBe('Public Post');
|
|
1013
|
+
} finally {
|
|
1014
|
+
await project.cleanup();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
});
|
|
1018
|
+
|
|
497
1019
|
describe('info', () => {
|
|
498
1020
|
it('shows version', async () => {
|
|
499
1021
|
const result = await runCli(['info']);
|
package/__tests__/e2e/helpers.ts
CHANGED
|
@@ -114,6 +114,50 @@ export async function createTestProject(
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
export async function adminTransact(
|
|
118
|
+
appId: string,
|
|
119
|
+
adminToken: string,
|
|
120
|
+
steps: any[],
|
|
121
|
+
): Promise<void> {
|
|
122
|
+
const response = await fetch(`${apiUrl}/admin/transact`, {
|
|
123
|
+
method: 'POST',
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
Authorization: `Bearer ${adminToken}`,
|
|
127
|
+
'app-id': appId,
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ steps }),
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`Failed to transact: ${response.status} ${await response.text()}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function createAppUser(
|
|
139
|
+
appId: string,
|
|
140
|
+
adminToken: string,
|
|
141
|
+
email: string,
|
|
142
|
+
): Promise<{ userId: string; refreshToken: string }> {
|
|
143
|
+
const response = await fetch(`${apiUrl}/admin/refresh_tokens`, {
|
|
144
|
+
method: 'POST',
|
|
145
|
+
headers: {
|
|
146
|
+
'Content-Type': 'application/json',
|
|
147
|
+
Authorization: `Bearer ${adminToken}`,
|
|
148
|
+
'app-id': appId,
|
|
149
|
+
},
|
|
150
|
+
body: JSON.stringify({ email }),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Failed to create user: ${response.status} ${await response.text()}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
const data = await response.json();
|
|
158
|
+
return { userId: data.user.id, refreshToken: data.user.refresh_token };
|
|
159
|
+
}
|
|
160
|
+
|
|
117
161
|
export async function createTempApp(title = 'cli-e2e-test'): Promise<{
|
|
118
162
|
appId: string;
|
|
119
163
|
adminToken: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.js"],"names":[],"mappings":"AAgnEA,8EAQC;AA+ED;;;;;EAKE"}
|
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import { mkdir, writeFile, readFile, unlink } from 'fs/promises';
|
|
|
5
5
|
import path, { join } from 'path';
|
|
6
6
|
import { randomUUID } from 'crypto';
|
|
7
7
|
import jsonDiff from 'json-diff';
|
|
8
|
+
import JSON5 from 'json5';
|
|
8
9
|
import chalk from 'chalk';
|
|
9
10
|
import { program, Option } from 'commander';
|
|
10
11
|
import boxen from 'boxen';
|
|
@@ -419,6 +420,17 @@ program
|
|
|
419
420
|
}
|
|
420
421
|
openInBrowser(`${instantDashOrigin}/dash?s=main&app=${appId}&t=explorer`);
|
|
421
422
|
});
|
|
423
|
+
program
|
|
424
|
+
.command('query')
|
|
425
|
+
.argument('<query>', 'InstaQL query as JSON')
|
|
426
|
+
.option('-a --app <app-id>', 'App ID to query. Defaults to *_INSTANT_APP_ID in .env')
|
|
427
|
+
.option('--admin', 'Run the query as admin (bypasses permissions)')
|
|
428
|
+
.option('--as-email <email>', 'Run the query as a specific user by email')
|
|
429
|
+
.option('--as-guest', 'Run the query as an unauthenticated guest')
|
|
430
|
+
.description('Run an InstaQL query against your app.')
|
|
431
|
+
.action(async function (queryArg, opts) {
|
|
432
|
+
await handleQuery(queryArg, opts);
|
|
433
|
+
});
|
|
422
434
|
program.parse(process.argv);
|
|
423
435
|
async function handleInit(opts) {
|
|
424
436
|
const pkgAndAuthInfo = await getOrPromptPackageAndAuthInfoWithErrorLogging(opts);
|
|
@@ -496,6 +508,66 @@ async function handleInitWithoutFiles(opts) {
|
|
|
496
508
|
process.exit(1);
|
|
497
509
|
}
|
|
498
510
|
}
|
|
511
|
+
async function detectAppIdQuietly(opts) {
|
|
512
|
+
const fromOpts = await detectAppIdFromOptsWithErrorLogging(opts);
|
|
513
|
+
if (!fromOpts.ok)
|
|
514
|
+
return fromOpts;
|
|
515
|
+
if (fromOpts.appId) {
|
|
516
|
+
return { ok: true, appId: fromOpts.appId };
|
|
517
|
+
}
|
|
518
|
+
const fromEnv = detectAppIdFromEnvWithErrorLogging();
|
|
519
|
+
if (!fromEnv.ok)
|
|
520
|
+
return fromEnv;
|
|
521
|
+
if (fromEnv.found) {
|
|
522
|
+
return { ok: true, appId: fromEnv.found.value };
|
|
523
|
+
}
|
|
524
|
+
return { ok: true };
|
|
525
|
+
}
|
|
526
|
+
async function handleQuery(queryArg, opts) {
|
|
527
|
+
const contextCount = (opts.admin ? 1 : 0) + (opts.asEmail ? 1 : 0) + (opts.asGuest ? 1 : 0);
|
|
528
|
+
if (contextCount === 0) {
|
|
529
|
+
error('Please specify a context: --admin, --as-email <email>, or --as-guest');
|
|
530
|
+
return process.exit(1);
|
|
531
|
+
}
|
|
532
|
+
if (contextCount > 1) {
|
|
533
|
+
error('Please specify exactly one context: --admin, --as-email <email>, or --as-guest');
|
|
534
|
+
return process.exit(1);
|
|
535
|
+
}
|
|
536
|
+
const { ok, appId } = await detectAppIdQuietly(opts);
|
|
537
|
+
if (!ok)
|
|
538
|
+
return process.exit(1);
|
|
539
|
+
if (!appId) {
|
|
540
|
+
error('No app ID detected. Please specify one with --app or set up with `instant-cli init`');
|
|
541
|
+
return process.exit(1);
|
|
542
|
+
}
|
|
543
|
+
let query;
|
|
544
|
+
try {
|
|
545
|
+
query = JSON5.parse(queryArg);
|
|
546
|
+
}
|
|
547
|
+
catch {
|
|
548
|
+
error('Invalid JSON query argument.');
|
|
549
|
+
return process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
const headers = { 'app-id': appId };
|
|
552
|
+
if (opts.asEmail) {
|
|
553
|
+
headers['as-email'] = opts.asEmail;
|
|
554
|
+
}
|
|
555
|
+
else if (opts.asGuest) {
|
|
556
|
+
headers['as-guest'] = 'true';
|
|
557
|
+
}
|
|
558
|
+
const res = await fetchJson({
|
|
559
|
+
method: 'POST',
|
|
560
|
+
path: '/admin/query',
|
|
561
|
+
body: { query },
|
|
562
|
+
headers,
|
|
563
|
+
debugName: 'Query',
|
|
564
|
+
errorMessage: 'Failed to run query.',
|
|
565
|
+
command: 'query',
|
|
566
|
+
});
|
|
567
|
+
if (!res.ok)
|
|
568
|
+
return process.exit(1);
|
|
569
|
+
console.log(JSON.stringify(res.data, null, 2));
|
|
570
|
+
}
|
|
499
571
|
async function handlePush(bag, opts) {
|
|
500
572
|
const pkgAndAuthInfo = await enforcePackageAndAuthInfoWithErrorLogging(opts);
|
|
501
573
|
if (!pkgAndAuthInfo)
|
|
@@ -1494,9 +1566,10 @@ async function waitForAuthToken({ secret }) {
|
|
|
1494
1566
|
* @param {boolean} [options.noLogError]
|
|
1495
1567
|
* @param {string} [options.command] - The CLI command being executed (e.g., 'push', 'pull', 'login')
|
|
1496
1568
|
* @param {string} [options.authToken] - Optional auth token to use instead of reading from config
|
|
1569
|
+
* @param {Record<string, string>} [options.headers] - Extra headers to include in the request
|
|
1497
1570
|
* @returns {Promise<{ ok: boolean; data: any }>}
|
|
1498
1571
|
*/
|
|
1499
|
-
async function fetchJson({ debugName, errorMessage, path, body, method, noAuth, noLogError, command, authToken: providedAuthToken, }) {
|
|
1572
|
+
async function fetchJson({ debugName, errorMessage, path, body, method, noAuth, noLogError, command, authToken: providedAuthToken, headers: extraHeaders, }) {
|
|
1500
1573
|
const withAuth = !noAuth;
|
|
1501
1574
|
const withErrorLogging = !noLogError;
|
|
1502
1575
|
let authToken = null;
|
|
@@ -1517,6 +1590,7 @@ async function fetchJson({ debugName, errorMessage, path, body, method, noAuth,
|
|
|
1517
1590
|
'X-Instant-Source': 'instant-cli',
|
|
1518
1591
|
'X-Instant-Version': version,
|
|
1519
1592
|
...(command ? { 'X-Instant-Command': command } : {}),
|
|
1593
|
+
...extraHeaders,
|
|
1520
1594
|
},
|
|
1521
1595
|
body: body ? JSON.stringify(body) : undefined,
|
|
1522
1596
|
signal: AbortSignal.timeout(timeoutMs),
|