@smartive/graphql-magic 23.11.0 → 23.12.0-next.2

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.
@@ -10,12 +10,18 @@ export const generateGraphqlApiTypes = async (dateLibrary: DateLibrary) => {
10
10
  documents: undefined,
11
11
  generates: {
12
12
  [`${generatedFolderPath}/api/index.ts`]: {
13
- plugins: ['typescript', 'typescript-resolvers', { add: { content: DATE_CLASS_IMPORT[dateLibrary] } }],
13
+ plugins: [
14
+ 'typescript',
15
+ 'typescript-resolvers',
16
+ { add: { content: DATE_CLASS_IMPORT[dateLibrary] } },
17
+ { add: { content: `import type { Time } from '@smartive/graphql-magic';` } },
18
+ ],
14
19
  },
15
20
  },
16
21
  config: {
17
22
  scalars: {
18
23
  DateTime: DATE_CLASS[dateLibrary],
24
+ Time: 'Time',
19
25
  },
20
26
  },
21
27
  });
@@ -30,7 +36,11 @@ export const generateGraphqlClientTypes = async () => {
30
36
  documents: [graphqlQueriesPath, `${generatedFolderPath}/client/mutations.ts`],
31
37
  generates: {
32
38
  [`${generatedFolderPath}/client/index.ts`]: {
33
- plugins: ['typescript', 'typescript-operations'],
39
+ plugins: [
40
+ 'typescript',
41
+ 'typescript-operations',
42
+ { add: { content: `import type { Time } from '@smartive/graphql-magic';` } },
43
+ ],
34
44
  },
35
45
  },
36
46
  config: {
@@ -43,6 +53,7 @@ export const generateGraphqlClientTypes = async () => {
43
53
  },
44
54
  scalars: {
45
55
  DateTime: 'string',
56
+ Time: 'Time',
46
57
  },
47
58
  ignoreNoDocuments: true,
48
59
  },
@@ -23,6 +23,11 @@ export const generateDBModels = (models: Models, dateLibrary: DateLibrary) => {
23
23
 
24
24
  writer.write(DATE_CLASS_IMPORT[dateLibrary]).blankLine();
25
25
 
26
+ writer.write(`type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';`);
27
+ writer.write(`type Hour = \`0\${Digit}\` | \`1\${Digit}\` | \`2\${'0' | '1' | '2' | '3'}\`;`);
28
+ writer.write(`type Minute = \`\${'0' | '1' | '2' | '3' | '4' | '5'}\${Digit}\`;`);
29
+ writer.write(`export type Time = \`\${Hour}:\${Minute}\`;`).blankLine();
30
+
26
31
  for (const [key, value] of Object.entries(PRIMITIVE_TYPES)) {
27
32
  writer.write(`export type ${key} = ${value};`).blankLine();
28
33
  }
@@ -150,6 +155,9 @@ const getFieldType = (field: EntityField, dateLibrary: DateLibrary, input?: bool
150
155
  if (field.type === 'DateTime') {
151
156
  return (input ? `(${DATE_CLASS[dateLibrary]} | string)` : DATE_CLASS[dateLibrary]) + (field.list ? '[]' : '');
152
157
  }
158
+ if (field.type === 'Time') {
159
+ return `Time${field.list ? '[]' : ''}`;
160
+ }
153
161
 
154
162
  return get(PRIMITIVE_TYPES, field.type) + (field.list ? '[]' : '');
155
163
  default: {
@@ -1565,6 +1565,9 @@ export class MigrationGenerator {
1565
1565
  case 'DateTime':
1566
1566
  col(`table.timestamp('${name}')`);
1567
1567
  break;
1568
+ case 'Time':
1569
+ col(`table.specificType('${name}', 'time without time zone')`);
1570
+ break;
1568
1571
  case 'ID':
1569
1572
  col(`table.uuid('${name}')`);
1570
1573
  break;
@@ -1666,6 +1669,11 @@ export class MigrationGenerator {
1666
1669
  return true;
1667
1670
  }
1668
1671
  }
1672
+ if (field.type === 'Time') {
1673
+ if (!['time without time zone', 'time'].includes(col.data_type)) {
1674
+ return true;
1675
+ }
1676
+ }
1669
1677
  }
1670
1678
 
1671
1679
  return false;
@@ -26,6 +26,7 @@ type FieldDefinitionBase2 =
26
26
  endOfDay?: boolean;
27
27
  endOfMonth?: boolean;
28
28
  }
29
+ | { type: 'Time' }
29
30
  | ({
30
31
  type: 'Int';
31
32
  intType?: 'currency';
@@ -112,6 +113,7 @@ export type IDFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'ID' }
112
113
  export type BooleanFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'Boolean' }>;
113
114
  export type StringFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'String' }>;
114
115
  export type DateTimeFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'DateTime' }>;
116
+ export type TimeFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'Time' }>;
115
117
  export type IntFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'Int' }>;
116
118
  export type FloatFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'Float' }>;
117
119
  export type UploadFieldDefinition = Extract<PrimitiveFieldDefinition, { type: 'Upload' }>;
@@ -18,6 +18,7 @@ import {
18
18
  PrimitiveFieldDefinition,
19
19
  RawEnumModelDefinition,
20
20
  StringFieldDefinition,
21
+ TimeFieldDefinition,
21
22
  UnionModelDefinition,
22
23
  UploadFieldDefinition,
23
24
  } from '..';
@@ -52,6 +53,7 @@ export type IDField = IDFieldDefinition;
52
53
  export type BooleanField = BooleanFieldDefinition;
53
54
  export type StringField = StringFieldDefinition;
54
55
  export type DateTimeField = DateTimeFieldDefinition;
56
+ export type TimeField = TimeFieldDefinition;
55
57
  export type IntField = IntFieldDefinition;
56
58
  export type FloatField = FloatFieldDefinition;
57
59
  export type UploadField = UploadFieldDefinition;
@@ -80,6 +82,10 @@ export class Models {
80
82
  kind: 'scalar',
81
83
  name: 'DateTime',
82
84
  },
85
+ {
86
+ kind: 'scalar',
87
+ name: 'Time',
88
+ },
83
89
  { kind: 'scalar', name: 'Upload' },
84
90
  {
85
91
  kind: 'raw-enum',
@@ -1,5 +1,7 @@
1
+ import { GraphQLScalarType, Kind } from 'graphql';
1
2
  import { Models } from '../models/models';
2
3
  import { and, isCreatable, isRootModel, isUpdatable, merge, not, typeToField } from '../models/utils';
4
+ import { parseTime, serializeTime } from '../utils/time';
3
5
  import { mutationResolver } from './mutations';
4
6
  import { queryResolver } from './resolver';
5
7
 
@@ -25,6 +27,19 @@ export const getResolvers = (models: Models) => {
25
27
  [`${model.pluralField}_AGGREGATE`]: queryResolver,
26
28
  })),
27
29
  ]),
30
+ Time: new GraphQLScalarType({
31
+ name: 'Time',
32
+ description: 'Time without date and timezone (HH:mm)',
33
+ serialize: (value) => (value == null ? value : serializeTime(value)),
34
+ parseValue: (value) => parseTime(value),
35
+ parseLiteral: (ast) => {
36
+ if (ast.kind !== Kind.STRING) {
37
+ throw new Error(`Invalid literal for Time scalar. Expected STRING, got ${ast.kind}.`);
38
+ }
39
+
40
+ return parseTime(ast.value);
41
+ },
42
+ }),
28
43
  };
29
44
  const mutations = [
30
45
  ...models.entities.filter(and(not(isRootModel), isCreatable)).map((model) => ({
@@ -2,3 +2,4 @@
2
2
 
3
3
  export * from './dates';
4
4
  export * from './rules';
5
+ export * from './time';
@@ -0,0 +1,32 @@
1
+ type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
2
+ type Hour = `0${Digit}` | `1${Digit}` | `2${'0' | '1' | '2' | '3'}`;
3
+ type Minute = `${'0' | '1' | '2' | '3' | '4' | '5'}${Digit}`;
4
+
5
+ export type Time = `${Hour}:${Minute}`;
6
+
7
+ const PARSE_TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)$/;
8
+ const SERIALIZE_TIME_RE = /^([01]\d|2[0-3]):([0-5]\d)(?::[0-5]\d(?:\.\d+)?)?$/;
9
+
10
+ export const parseTime = (value: unknown): Time => {
11
+ if (typeof value !== 'string') {
12
+ throw new Error(`Time must be a string in HH:mm format. Received: ${typeof value}`);
13
+ }
14
+ const match = value.match(PARSE_TIME_RE);
15
+ if (!match) {
16
+ throw new Error(`Invalid Time value "${value}". Expected HH:mm in 24-hour format.`);
17
+ }
18
+
19
+ return `${match[1]}:${match[2]}` as Time;
20
+ };
21
+
22
+ export const serializeTime = (value: unknown): Time => {
23
+ if (typeof value !== 'string') {
24
+ throw new Error(`Time must be a string in HH:mm format. Received: ${typeof value}`);
25
+ }
26
+ const match = value.match(SERIALIZE_TIME_RE);
27
+ if (!match) {
28
+ throw new Error(`Invalid Time value "${value}". Expected HH:mm or HH:mm:ss.`);
29
+ }
30
+
31
+ return `${match[1]}:${match[2]}` as Time;
32
+ };
@@ -54,6 +54,7 @@ exports[`query can be executed 1`] = `
54
54
  },
55
55
  "field": null,
56
56
  "id": "fc4e013e-4cb0-4ef8-9f2e-3d475bdf2b90",
57
+ "time": "09:15",
57
58
  "xyz": 2,
58
59
  },
59
60
  {
@@ -68,6 +69,7 @@ exports[`query can be executed 1`] = `
68
69
  },
69
70
  "field": "Some value",
70
71
  "id": "604ab55d-ec3e-4857-9f27-219158f80e64",
72
+ "time": "14:30",
71
73
  "xyz": 1,
72
74
  },
73
75
  ],
@@ -333,3 +335,20 @@ exports[`query processes reverseFilters correctly 1`] = `
333
335
  ],
334
336
  }
335
337
  `;
338
+
339
+ exports[`query returns Time values for SomeObject 1`] = `
340
+ {
341
+ "manyObjects": [
342
+ {
343
+ "id": "fc4e013e-4cb0-4ef8-9f2e-3d475bdf2b90",
344
+ "time": "09:15",
345
+ "xyz": 2,
346
+ },
347
+ {
348
+ "id": "604ab55d-ec3e-4857-9f27-219158f80e64",
349
+ "time": "14:30",
350
+ "xyz": 1,
351
+ },
352
+ ],
353
+ }
354
+ `;
@@ -12,6 +12,7 @@ describe('query', () => {
12
12
  id
13
13
  field
14
14
  xyz
15
+ time
15
16
  another {
16
17
  id
17
18
  manyObjects(where: { id: "${SOME_ID}" }) {
@@ -26,6 +27,22 @@ describe('query', () => {
26
27
  });
27
28
  });
28
29
 
30
+ it('returns Time values for SomeObject', async () => {
31
+ await withServer(async (request) => {
32
+ expect(
33
+ await request(gql`
34
+ query GetTimes {
35
+ manyObjects(where: { another: { id: "${ANOTHER_ID}" } }, orderBy: [{ xyz: DESC }]) {
36
+ id
37
+ xyz
38
+ time
39
+ }
40
+ }
41
+ `),
42
+ ).toMatchSnapshot();
43
+ });
44
+ });
45
+
29
46
  it('processes reverseFilters correctly', async () => {
30
47
  await withServer(async (request) => {
31
48
  expect(
@@ -39,5 +39,6 @@ exports[`resolvers are generated correctly 1`] = `
39
39
  "Reaction": {
40
40
  "__resolveType": [Function],
41
41
  },
42
+ "Time": "Time",
42
43
  }
43
44
  `;
@@ -1,7 +1,6 @@
1
1
  import { Knex } from 'knex';
2
2
  import { pick } from 'lodash';
3
3
  import { getColumnName, isInTable, modelNeedsTable } from '../../../src';
4
- import { SeedData } from '../../generated/db';
5
4
  import { models } from '../models';
6
5
 
7
6
  export const ADMIN_ID = '04e45b48-04cf-4b38-bb25-b9af5ae0b2c4';
@@ -16,7 +15,7 @@ export const QUESTION_ID = '3d0f3254-282f-4f1f-95e3-c1f699f3c1e5';
16
15
  export const ANSWER_ID = 'f2d7b3f1-8ea1-4c2c-91ec-024432da1b0d';
17
16
  export const REVIEW_ID = '817c55de-2f77-4159-bd44-9837d868f889';
18
17
 
19
- export const seed: SeedData = {
18
+ export const seed = {
20
19
  User: [
21
20
  {
22
21
  id: ADMIN_ID,
@@ -42,6 +41,7 @@ export const seed: SeedData = {
42
41
  float: 0,
43
42
  list: ['A'],
44
43
  xyz: 1,
44
+ time: '14:30',
45
45
  },
46
46
  {
47
47
  id: SOME_ID_2,
@@ -50,6 +50,7 @@ export const seed: SeedData = {
50
50
  float: 0.5,
51
51
  list: ['B'],
52
52
  xyz: 2,
53
+ time: '09:15',
53
54
  },
54
55
  {
55
56
  id: SOME_ID_3,
@@ -57,6 +58,7 @@ export const seed: SeedData = {
57
58
  float: 0.5,
58
59
  list: ['B'],
59
60
  xyz: 2,
61
+ time: '23:45',
60
62
  },
61
63
  {
62
64
  id: SOME_ID_4,
@@ -64,6 +66,7 @@ export const seed: SeedData = {
64
66
  float: 0.5,
65
67
  list: ['B'],
66
68
  xyz: 2,
69
+ time: '00:05',
67
70
  },
68
71
  ],
69
72
  Question: [
@@ -107,6 +107,12 @@ const modelDefinitions: ModelDefinitions = [
107
107
  orderable: true,
108
108
  filterable: true,
109
109
  },
110
+ {
111
+ name: 'time',
112
+ type: 'Time',
113
+ creatable: true,
114
+ updatable: true,
115
+ },
110
116
  ],
111
117
  },
112
118
  {